Skip to content

Commit

Permalink
Add a command to diff single assemblies (#7)
Browse files Browse the repository at this point in the history
* Add a command to diff single assemblies
* Make the command write to a file/console
* Let's do v1.2
  • Loading branch information
mattleibow authored Nov 3, 2019
1 parent d5bccf8 commit 0af330a
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 1 deletion.
225 changes: 225 additions & 0 deletions api-tools/DiffCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
using Mono.Options;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace Mono.ApiTools
{
public class DiffCommand : BaseCommand
{
private const int DefaultSaveBufferSize = 1024;

private static readonly Encoding UTF8NoBOM = new UTF8Encoding(false, true);

public DiffCommand()
: base("diff", "ASSEMBLY1 ASSEMBLY2", "Compare two assemblies.")
{
}

public List<string> Assemblies { get; set; } = new List<string>();

public string OutputPath { get; set; }

public bool IgnoreNonbreaking { get; set; }

protected override OptionSet OnCreateOptions() => new OptionSet
{
{ "o|output=", "The output file path", v => OutputPath = v },
{ "ignore-nonbreaking", "Ignore the non-breaking changes and just output breaking changes", v => IgnoreNonbreaking = true },
};

protected override bool OnValidateArguments(IEnumerable<string> extras)
{
var hasError = false;

var assemblies = extras.Where(p => !string.IsNullOrEmpty(p)).ToArray();

foreach (var ass in assemblies)
{
if (File.Exists(ass))
{
Assemblies.Add(ass);
}
else
{
Console.Error.WriteLine($"{Program.Name}: Assembly does not exist: `{ass}`.");
hasError = true;
}
}

if (Assemblies.Count == 0)
{
Console.Error.WriteLine($"{Program.Name}: At least one assembly is required.");
hasError = true;
}

if (Assemblies.Count != 2)
{
Console.Error.WriteLine($"{Program.Name}: Exactly two assemblies are required.");
hasError = true;
}

return !hasError;
}

protected override bool OnInvoke(IEnumerable<string> extras)
{
if (Program.Verbose)
Console.WriteLine($"Running a diff on '{Assemblies[0]}' vs '{Assemblies[1]}'...");

using (var oldStream = File.OpenRead(Assemblies[0]))
using (var newStream = File.OpenRead(Assemblies[1]))
{
DiffAssembliesAsync(newStream, oldStream).Wait();
}

return true;
}

private async Task DiffAssembliesAsync(Stream newStream, Stream oldStream)
{
// create the api xml
var oldApiXml = GenerateAssemblyApiInfo(oldStream);
var newApiXml = GenerateAssemblyApiInfo(newStream);

// make sure the assembly names are the same for the comparison
string assemblyName;
(newApiXml, assemblyName) = await RenameAssemblyAsync(oldApiXml, newApiXml);

// generate the diff
using var diffStream = GenerateDiff(oldApiXml, newApiXml, assemblyName);
await FixBugsAsync(diffStream);

if (!string.IsNullOrEmpty(OutputPath))
{
var dir = Path.GetDirectoryName(OutputPath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);

// write the file
using var file = File.Create(OutputPath);
await diffStream.CopyToAsync(file);
}
else
{
// write to console out
using var md = new StreamReader(diffStream);
var contents = await md.ReadToEndAsync();
await Console.Out.WriteAsync(contents);
}

// we are done
if (Program.Verbose)
Console.WriteLine($"Diff complete of '{assemblyName}'.");
}

private async Task FixBugsAsync(Stream diffStream)
{
// TODO: there are two bugs in this version of mono-api-html
string contents;
using (var md = new StreamReader(diffStream, UTF8NoBOM, false, DefaultSaveBufferSize, true))
{
contents = await md.ReadToEndAsync();
}

// 1. the <h4> doesn't look pretty in the markdown
contents = contents.Replace("<h4>", "> ");
contents = contents.Replace("</h4>", Environment.NewLine);

// 2. newlines are inccorrect on Windows: https://github.com/mono/mono/pull/9918
contents = contents.Replace("\r\r", "\r");

// write the contents back to the stream
diffStream.SetLength(0);
using (var writer = new StreamWriter(diffStream, UTF8NoBOM, DefaultSaveBufferSize, true))
{
writer.Write(contents);
}
diffStream.Position = 0;
}

private Stream GenerateAssemblyApiInfo(Stream assemblyStream)
{
var config = new ApiInfoConfig
{
IgnoreResolutionErrors = true
};

var info = new MemoryStream();

using (var writer = new StreamWriter(info, UTF8NoBOM, DefaultSaveBufferSize, true))
{
ApiInfo.Generate(assemblyStream, writer, config);
}

assemblyStream.Position = 0;
info.Position = 0;

return info;
}

private Stream GenerateDiff(Stream oldApiXml, Stream newApiXml, string assemblyName)
{
var config = new ApiDiffFormattedConfig
{
Formatter = ApiDiffFormatter.Markdown,
IgnoreNonbreaking = IgnoreNonbreaking
};

var diff = new MemoryStream();

using (var writer = new StreamWriter(diff, UTF8NoBOM, DefaultSaveBufferSize, true))
{
ApiDiffFormatted.Generate(oldApiXml, newApiXml, writer, config);
}

if (diff.Length == 0)
{
using var writer = new StreamWriter(diff, UTF8NoBOM, DefaultSaveBufferSize, true);
writer.WriteLine($"# API diff: {assemblyName}.dll");
writer.WriteLine();
writer.WriteLine($"## {assemblyName}.dll");
writer.WriteLine();
writer.WriteLine($"> No changes.");
}

oldApiXml.Position = 0;
newApiXml.Position = 0;
diff.Position = 0;

return diff;
}

private async Task<(Stream, string)> RenameAssemblyAsync(Stream oldApiXml, Stream newApiXml, CancellationToken cancellationToken = default)
{
var oldDoc = await XDocument.LoadAsync(oldApiXml, LoadOptions.None, cancellationToken);
var assemblyName = oldDoc.Root.Element("assembly").Attribute("name").Value;
oldApiXml.Position = 0;

var newDoc = await XDocument.LoadAsync(newApiXml, LoadOptions.None, cancellationToken);
var newAssembly = newDoc.Root.Element("assembly");
var newName = newAssembly.Attribute("name");

if (newName.Value != assemblyName)
{
if (Program.Verbose)
Console.WriteLine($"WARNING: Assembly name changed from '{assemblyName}' to '{newName.Value}'.");
newName.Value = assemblyName;

newApiXml.Dispose();

newApiXml = new MemoryStream();
await newDoc.SaveAsync(newApiXml, SaveOptions.None, cancellationToken);
}

newApiXml.Position = 0;

return (newApiXml, assemblyName);
}
}
}
1 change: 1 addition & 0 deletions api-tools/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ static int Main(string[] args)
{ "v|verbose", "Use a more verbose output", _ => Verbose = true },
"",
"Available commands:",
new DiffCommand(),
new NuGetDiffCommand(),
};
return commands.Run(args);
Expand Down
2 changes: 1 addition & 1 deletion azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pool:
name: 'Hosted Windows 2019 with VS2019'

variables:
BASE_VERSION: 1.1.0
BASE_VERSION: 1.2.0
BUILD_NUMBER: $[counter(format('{0}_{1}', variables['BASE_VERSION'], variables['Build.SourceBranch']), 1)]
PACKAGE_VERSION: $(BASE_VERSION).$(BUILD_NUMBER)
CONFIGURATION: 'Release'
Expand Down

0 comments on commit 0af330a

Please sign in to comment.