Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option for comparing NuGet structure and NuGet metadata. #17

Merged
merged 2 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions Mono.ApiTools.NuGetDiff.Tests/PackageComparerTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Mono.Cecil;
using NuGet.Frameworks;
using NuGet.Packaging;
using NuGet.Packaging.Core;
using NuGet.Versioning;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -188,6 +189,45 @@ public async Task TestCompareTwoLocalFiles()
Assert.NotEmpty(diff.UnchangedAssemblies);
}

[Fact]
public async Task TestComparePackageStructureAndMetadata()
{
var comparer = new NuGetDiff();
comparer.SearchPaths.AddRange(searchPaths);
comparer.SaveNuGetStructureDiff = true;
comparer.IgnoreResolutionErrors = true;

// Ensure diff is producing results
var diff = await comparer.GenerateAsync(FormsPackageId, FormsV20Number1, FormsV30Number2);

Assert.NotEmpty(diff.AddedFiles);
Assert.NotEmpty(diff.RemovedFiles);
Assert.NotEmpty(diff.MetadataDiff);

// Check output markdown file
var oldPackage = new PackageIdentity(FormsPackageId, NuGetVersion.Parse(FormsV20Number1));
var newPackage = new PackageIdentity(FormsPackageId, NuGetVersion.Parse(FormsV30Number2));
var tempOutput = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());

await comparer.SaveCompleteDiffToDirectoryAsync(oldPackage, newPackage, tempOutput);

var results = await File.ReadAllLinesAsync(Path.Combine(tempOutput, "nuget-diff.md"));

// Spot check a few expected lines
Assert.Contains("### Changed Metadata", results);
Assert.Contains("- <authors>Xamarin Inc.</authors>", results);
Assert.Contains("+ <authors>Microsoft</authors>", results);
Assert.Contains("### Added/Removed File(s)", results);
Assert.Contains("- lib/portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10/Xamarin.Forms.Core.dll", results);
Assert.Contains("+ lib/netstandard2.0/Xamarin.Forms.Core.dll", results);

try
{
// Try to delete temp dir, but don't error if it fails
Directory.Delete(tempOutput, true);
} catch { }
}

[Fact]
public async Task TestCompletePackageDiffIsGeneratedCorrectlyWithoutAllReferencesAndNoOldVersion()
{
Expand Down
62 changes: 62 additions & 0 deletions Mono.ApiTools.NuGetDiff/NuGetDiff.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ public NuGetDiff(string sourceUrl)

public bool SaveNuGetXmlDiff { get; set; } = true;

public bool SaveNuGetStructureDiff { get; set; } = false;

public string ApiInfoFileExtension { get; set; } = DefaultApiInfoFileExtension;

public string HtmlDiffFileExtension { get; set; } = DefaultHtmlDiffFileExtension;
Expand Down Expand Up @@ -239,6 +241,21 @@ public async Task<NuGetDiffResult> GenerateAsync(PackageArchiveReader oldReader,
}
}

// these results aren't really interesting if the entire package is brand new
if (oldReader is not null)
{
// simple diff of which files *exist* in the NuGet, sizes and file content are not considered
var oldFiles = oldReader?.GetFiles() ?? Enumerable.Empty<string>();
var newFiles = newReader?.GetFiles() ?? Enumerable.Empty<string>();

// ignore some internal NuGet files
result.AddedFiles.AddRange(newFiles.Except(oldFiles).Where(f => Path.GetExtension(f) != ".psmdcp" && Path.GetExtension(f) != ".p7s"));
result.RemovedFiles.AddRange(oldFiles.Except(newFiles).Where(f => Path.GetExtension(f) != ".psmdcp" && Path.GetExtension(f) != ".p7s"));

// changed NuGet metadata
result.MetadataDiff.AddRange(NuGetSpecDiff.Generate(oldReader, newReader, true));
}

return result;

(string path, string name)[] GetFrameworkAssemblies(NuGetFramework fw, IEnumerable<FrameworkSpecificGroup> items)
Expand Down Expand Up @@ -498,6 +515,51 @@ public async Task SaveCompleteDiffToDirectoryAsync(PackageArchiveReader oldReade
xPackageDiff.Save(diffPath);
}

// save the NuGet structure diff
if (SaveNuGetStructureDiff)
{
var diffPath = Path.Combine(outputDirectory, "nuget-diff.md");

if (packageDiff.AddedFiles.Any() || packageDiff.RemovedFiles.Any() || packageDiff.MetadataDiff.Any ())
{
Directory.CreateDirectory(outputDirectory);

using var file = File.Create(diffPath);
using var sw = new StreamWriter(file);

sw.WriteLine($"## {(packageDiff.OldIdentity ?? packageDiff.NewIdentity).Id}");
sw.WriteLine();

if (packageDiff.MetadataDiff.Any())
{
sw.WriteLine("### Changed Metadata");
sw.WriteLine();
sw.WriteLine("```");

foreach (var entry in packageDiff.MetadataDiff)
sw.WriteLine(entry.ToFormattedString());

sw.WriteLine("```");
sw.WriteLine();
}

if (packageDiff.AddedFiles.Any() || packageDiff.RemovedFiles.Any())
{
sw.WriteLine("### Added/Removed File(s)");
sw.WriteLine();
sw.WriteLine("```");

foreach (var entry in packageDiff.RemovedFiles)
sw.WriteLine("- " + entry);
foreach (var entry in packageDiff.AddedFiles)
sw.WriteLine("+ " + entry);

sw.WriteLine("```");
sw.WriteLine();
}
}
}

IEnumerable<(string newA, string oldA)> GetAllAssemblies(NuGetFramework framework)
{
if (packageDiff.UnchangedAssemblies.TryGetValue(framework, out var unchangedAssemblies))
Expand Down
8 changes: 8 additions & 0 deletions Mono.ApiTools.NuGetDiff/NuGetDiffResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ public class NuGetDiffResult

public Dictionary<NuGetFramework, (string newPath, string oldPath)[]> SimilarAssemblies { get; set; }

// file diff

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

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

public List<NuGetSpecDiff.ElementDiff> MetadataDiff { get; } = new List<NuGetSpecDiff.ElementDiff>();

public NuGetFramework[] GetAllFrameworks()
{
return
Expand Down
121 changes: 121 additions & 0 deletions Mono.ApiTools.NuGetDiff/NuGetSpecDiff.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using NuGet.Packaging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;

namespace Mono.ApiTools
{
public class NuGetSpecDiff
{
static readonly Regex tag = new Regex("artifact_versioned=(?<GroupId>.+)?:(?<ArtifactId>.+?):(?<Version>.+)\\s?", RegexOptions.Compiled);

public static IEnumerable<ElementDiff> Generate(PackageArchiveReader oldReader, PackageArchiveReader newReader, bool skipVersionMetadata)
{
// make a copy so we aren't modifying the *real* nuspec
var oldMetadata = new XDocument(oldReader?.NuspecReader?.Xml ?? XDocument.Parse("<package><metadata /></package>")).StripAllNamespaces();
var newMetadata = new XDocument(newReader?.NuspecReader?.Xml ?? XDocument.Parse("<package><metadata /></package>")).StripAllNamespaces();

if (oldMetadata.Element("package")?.Element("metadata") is not XElement oldXml || newMetadata.Element("package")?.Element("metadata") is not XElement newXml)
throw new ArgumentException("Malformed Nuspec xml");

if (skipVersionMetadata)
{
StripVersionMetadata(oldMetadata);
StripVersionMetadata(newMetadata);
}

var allElements = oldXml.Elements().Concat(newXml.Elements()).Select(x => x.Name.LocalName).Distinct();
var allDiffs = new List<ElementDiff>();

foreach (var element in allElements)
{
var diff = new ElementDiff(oldXml.Element(element), newXml.Element(element));

if (diff.Type != DiffType.None)
allDiffs.Add(diff);
}


return allDiffs;
}

static void StripVersionMetadata(XDocument xml)
{
if (xml.Element("package")?.Element("metadata") is not XElement metadata)
return;

// strip metadata that changes on every release, like version and git branch/commit
metadata.Element("version")?.Remove();

if (metadata.Element("repository") is XElement repo)
{
repo.Attribute("branch")?.Remove();
repo.Attribute("commit")?.Remove();
}

// strip a custom tag that AndroidX/GPS uses that contains a version number
// ex: artifact_versioned=com.google.code.gson:gson:2.10.1
if (metadata.Element("tags") is XElement tags)
{
var match = tag.Match(tags.Value);

if (match.Success)
tags.Value = tags.Value.Replace(match.Groups["Version"].Value, "[Version]");
}
}

public class ElementDiff
{
public XElement OldElement { get; }
public XElement NewElement { get; }

public ElementDiff(XElement oldElement, XElement newElement)
{
OldElement = oldElement;
NewElement = newElement;
}

public string Name => OldElement?.Name?.LocalName ?? NewElement?.Name?.LocalName;

public DiffType Type
{
get
{
if (OldElement is null)
return DiffType.Added;
if (NewElement is null)
return DiffType.Removed;
if (OldElement.Value == NewElement.Value)
return DiffType.None;

return DiffType.Modified;
}
}

public string ToFormattedString()
{
switch (Type)
{
case DiffType.Added:
return NewElement.GetPrefixedString("+");
case DiffType.Removed:
return OldElement.GetPrefixedString("-");
case DiffType.Modified:
return $"{OldElement.GetPrefixedString("-")}{Environment.NewLine}{NewElement.GetPrefixedString("+")}";
}

return string.Empty;
}
}

public enum DiffType
{
None,
Added,
Removed,
Modified
}
}
}
33 changes: 33 additions & 0 deletions Mono.ApiTools.NuGetDiff/XElementExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.IO;
using System.Text;
using System.Xml.Linq;

namespace Mono.ApiTools
{
internal static class XElementExtensions
{
internal static XDocument StripAllNamespaces(this XDocument document)
{
foreach (var node in document.Root.DescendantsAndSelf())
node.Name = node.Name.LocalName;

return document;
}

internal static string GetPrefixedString(this XElement element, string prefix)
=> element.ToString().PrefixLines(prefix);

internal static string PrefixLines(this string str, string prefix)
{
var sb = new StringBuilder();

using var sr = new StringReader(str);

while (sr.ReadLine() is string line)
sb.AppendLine($"{prefix} {line}");

return sb.ToString().Trim();
}
}
}
5 changes: 5 additions & 0 deletions api-tools/NuGetDiffCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public NuGetDiffCommand()

public string SourceUrl { get; set; } = "https://api.nuget.org/v3/index.json";

public bool CompareNuGetStructure { get; set; }

protected override OptionSet OnCreateOptions() => new OptionSet
{
{ "cache=", "The package cache directory", v => PackageCache = v },
Expand All @@ -50,6 +52,7 @@ public NuGetDiffCommand()
{ "search-path=", "A search path directory", v => SearchPaths.Add(v) },
{ "source=", "The NuGet URL source", v => SourceUrl = v },
{ "version=", "The version of the package to compare", v => Version = v },
{ "compare-nuget-structure", "Compare NuGet metadata and file contents", v => CompareNuGetStructure = true },
};

protected override bool OnValidateArguments(IEnumerable<string> extras)
Expand Down Expand Up @@ -203,6 +206,8 @@ private async Task DiffPackage(NuGetDiff comparer, PackageArchiveReader reader,
comparer.IgnoreResolutionErrors = true; // we don't care if frameowrk/platform types can't be found
comparer.MarkdownDiffFileExtension = ".diff.md";
comparer.IgnoreNonBreakingChanges = false;
comparer.SaveNuGetStructureDiff = CompareNuGetStructure;

await comparer.SaveCompleteDiffToDirectoryAsync(olderReader, reader, diffRoot);

// run the diff with just the breaking changes
Expand Down