diff --git a/cyclonedx/Commands/Convert/DiffCommand.cs b/cyclonedx/Commands/Convert/DiffCommand.cs index 52e8748..14c2cc9 100644 --- a/cyclonedx/Commands/Convert/DiffCommand.cs +++ b/cyclonedx/Commands/Convert/DiffCommand.cs @@ -47,81 +47,61 @@ public static async Task Diff( if (componentVersions) { - result.ComponentVersions = new DiffItem(); - - // there is some complexity here, components could be included - // multiple times, with the same or different versions + result.ComponentVersions = new Dictionary>(); // make a copy of components that are still to be processed var fromComponents = new List(fromBom.Components); var toComponents = new List(toBom.Components); - // ditch component versions that are in both - foreach (var component in fromBom.Components) + // unchanged component versions + // loop over the toBom and fromBom Components list as we will be modifying the fromComponents list + foreach (var fromComponent in fromBom.Components) { - if (toBom.Components.Count(c => c.Name == component.Name && c.Version == component.Version) > 0) + // if component version is in both SBOMs + if (toBom.Components.Count(toComponent => + toComponent.Group == fromComponent.Group + && toComponent.Name == fromComponent.Name + && toComponent.Version == fromComponent.Version + ) > 0) { - fromComponents.RemoveAll(c => c.Name == component.Name && c.Version == component.Version); - toComponents.RemoveAll(c => c.Name == component.Name && c.Version == component.Version); + var componentIdentifier = $"{fromComponent.Group}:{fromComponent.Name}"; + + if (!result.ComponentVersions.ContainsKey(componentIdentifier)) + { + result.ComponentVersions.Add(componentIdentifier, new DiffItem()); + } + + result.ComponentVersions[componentIdentifier].Unchanged.Add(fromComponent); + + fromComponents.RemoveAll(c => c.Group == fromComponent.Group && c.Name == fromComponent.Name && c.Version == fromComponent.Version); + toComponents.RemoveAll(c => c.Group == fromComponent.Group && c.Name == fromComponent.Name && c.Version == fromComponent.Version); } } - // find obviously added components - // take a copy of toComponents as we are modifying it + // added component versions foreach (var component in new List(toComponents)) { - if (fromComponents.Count(c => c.Name == component.Name) == 0) + var componentIdentifier = $"{component.Group}:{component.Name}"; + if (!result.ComponentVersions.ContainsKey(componentIdentifier)) { - result.ComponentVersions.Added.Add(component); - toComponents.RemoveAll(c => c.Name == component.Name && c.Version == component.Version); + result.ComponentVersions.Add(componentIdentifier, new DiffItem()); } + + result.ComponentVersions[componentIdentifier].Added.Add(component); } - // find obviously removed components - // take a copy of fromComponents as we are modifying it + // removed components versions foreach (var component in new List(fromComponents)) { - if (toComponents.Count(c => c.Name == component.Name) == 0) + var componentIdentifier = $"{component.Group}:{component.Name}"; + if (!result.ComponentVersions.ContainsKey(componentIdentifier)) { - result.ComponentVersions.Removed.Add(component); - fromComponents.RemoveAll(c => c.Name == component.Name && c.Version == component.Version); + result.ComponentVersions.Add(componentIdentifier, new DiffItem()); } - } - // now we should have modified components left over - // - // but this situation is possible (or the reverse) - // - // From: - // Component A v1.0.0 - // - // To: - // Component A v1.0.1 - // Component A v1.0.2 - // - // (╯°□°)╯︵ ┻━┻ - // - // we'll treat the first match as modified, and any others as - // added or removed - - foreach (var fromComponent in new List(fromComponents)) - { - var toComponent = toComponents.FirstOrDefault(c => c.Name == fromComponent.Name); - if (toComponent != null) - { - result.ComponentVersions.Modified.Add(new ModifiedDiffItem - { - From = fromComponent, - To = toComponent - }); - - fromComponents.RemoveAll(c => c.Name == fromComponent.Name && c.Version == fromComponent.Version); - toComponents.RemoveAll(c => c.Name == toComponent.Name && c.Version == toComponent.Version); - } + result.ComponentVersions[componentIdentifier].Removed.Add(component); } - // now everything left over we'll treat as added or removed - result.ComponentVersions.Removed.AddRange(fromComponents); - result.ComponentVersions.Added.AddRange(toComponents); + } if (outputFormat == StandardOutputFormat.json) @@ -151,33 +131,35 @@ public static async Task Diff( { Console.WriteLine("Component versions that have changed:"); Console.WriteLine(); - if ( - result.ComponentVersions.Removed.Count == 0 - && result.ComponentVersions.Modified.Count == 0 - && result.ComponentVersions.Removed.Count == 0 - ) - { - Console.WriteLine("None"); - } - else + + var changes = false; + foreach (var entry in result.ComponentVersions) { - foreach (var component in result.ComponentVersions.Added) + var componentDiffItem = entry.Value; + if (componentDiffItem.Added.Count > 0 || componentDiffItem.Removed.Count > 0) { - Console.WriteLine($"+ {component.Name}@{component.Version}"); - Console.WriteLine(); - } - foreach (var component in result.ComponentVersions.Modified) - { - Console.WriteLine($"- {component.From.Name}@{component.From.Version}"); - Console.WriteLine($"+ {component.To.Name}@{component.To.Version}"); - Console.WriteLine(); - } - foreach (var component in result.ComponentVersions.Removed) - { - Console.WriteLine($"- {component.Name}@{component.Version}"); + changes = true; + foreach (var component in componentDiffItem.Removed) + { + Console.WriteLine($"- {component.Group} {component.Name} @ {component.Version}"); + } + foreach (var component in componentDiffItem.Unchanged) + { + Console.WriteLine($"= {component.Group} {component.Name} @ {component.Version}"); + } + foreach (var component in componentDiffItem.Added) + { + Console.WriteLine($"+ {component.Group} {component.Name} @ {component.Version}"); + } Console.WriteLine(); } } + + if (!changes) + { + Console.WriteLine("None"); + } + Console.WriteLine(); } } diff --git a/cyclonedx/Models/DiffResult.cs b/cyclonedx/Models/DiffResult.cs index ffee114..a15dd77 100644 --- a/cyclonedx/Models/DiffResult.cs +++ b/cyclonedx/Models/DiffResult.cs @@ -3,21 +3,16 @@ namespace CycloneDX.CLI.Models { - public class ModifiedDiffItem - { - public T From { get; set; } - public T To { get; set; } - } - public class DiffItem { public List Added { get; set; } = new List(); - public List> Modified { get; set; } = new List>(); public List Removed { get; set; } = new List(); + public List Unchanged { get; set; } = new List(); } public class DiffResult { - public DiffItem ComponentVersions { get; set; } + // default to nulls. A value, even if it is empty is to indicate that this option has been invoked. + public Dictionary> ComponentVersions { get; set; } = null; } }