diff --git a/NuKeeper.Inspection.Tests/Sort/PackageInProjectTopologicalSortTests.cs b/NuKeeper.Inspection.Tests/Sort/PackageInProjectTopologicalSortTests.cs deleted file mode 100644 index cc33dce6a..000000000 --- a/NuKeeper.Inspection.Tests/Sort/PackageInProjectTopologicalSortTests.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using NSubstitute; -using NuKeeper.Abstractions.Logging; -using NuKeeper.Abstractions.NuGet; -using NuKeeper.Abstractions.RepositoryInspection; -using NuKeeper.Inspection.Sort; -using NUnit.Framework; - -namespace NuKeeper.Inspection.Tests.Sort -{ - [TestFixture] - public class PackageInProjectTopologicalSortTests - { - [Test] - public void CanSortEmptyList() - { - var items = new List(); - - var sorter = new PackageInProjectTopologicalSort(Substitute.For()); - - var sorted = sorter.Sort(items) - .ToList(); - - Assert.That(sorted, Is.Not.Null); - Assert.That(sorted, Is.Empty); - } - - [Test] - public void CanSortOneItem() - { - var items = new List - { - PackageFor("foo", "1.2.3", "bar{sep}fish.csproj"), - }; - - var sorter = new PackageInProjectTopologicalSort(Substitute.For()); - - var sorted = sorter.Sort(items) - .ToList(); - - AssertIsASortOf(sorted, items); - Assert.That(sorted[0], Is.EqualTo(items[0])); - } - - [Test] - public void CanSortTwoUnrelatedItems() - { - var items = new List - { - PackageFor("foo", "1.2.3", "bar{sep}fish.csproj"), - PackageFor("bar", "2.3.4", "project2{sep}p2.csproj") - }; - - var logger = Substitute.For(); - - var sorter = new PackageInProjectTopologicalSort(logger); - - var sorted = sorter.Sort(items) - .ToList(); - - AssertIsASortOf(sorted, items); - logger.Received(1).Detailed("No dependencies between items, no need to sort on dependencies"); - } - - [Test] - public void CanSortTwoRelatedItemsinCorrectOrder() - { - var aProj = PackageFor("foo", "1.2.3", "someproject{sep}someproject.csproj"); - var testProj = PackageFor("bar", "2.3.4", "someproject.tests{sep}someproject.tests.csproj", aProj); - - var items = new List - { - testProj, - aProj - }; - - var logger = Substitute.For(); - - var sorter = new PackageInProjectTopologicalSort(logger); - - var sorted = sorter.Sort(items) - .ToList(); - - AssertIsASortOf(sorted, items); - Assert.That(sorted[0], Is.EqualTo(items[0])); - Assert.That(sorted[1], Is.EqualTo(items[1])); - - logger.DidNotReceive().Detailed("No dependencies between items, no need to sort on dependencies"); - logger.Received(1).Detailed("Sorted 2 projects by dependencies but no change made"); - } - - [Test] - public void CanSortTwoRelatedItemsinReverseOrder() - { - var aProj = PackageFor("foo", "1.2.3", "someproject{sep}someproject.csproj"); - var testProj = PackageFor("bar", "2.3.4", "someproject.tests{sep}someproject.tests.csproj", aProj); - - var items = new List - { - aProj, - testProj - }; - - var logger = Substitute.For(); - - var sorter = new PackageInProjectTopologicalSort(logger); - - var sorted = sorter.Sort(items) - .ToList(); - - AssertIsASortOf(sorted, items); - Assert.That(sorted[0], Is.EqualTo(testProj)); - Assert.That(sorted[1], Is.EqualTo(aProj)); - - logger.Received(1).Detailed(Arg.Is(s => - s.StartsWith("Resorted 2 projects by dependencies,", StringComparison.OrdinalIgnoreCase))); - } - - [Test] - public void CanSortWithCycle() - { - var aProj = PackageFor("foo", "1.2.3", "someproject{sep}someproject.csproj"); - var testProj = PackageFor("bar", "2.3.4", "someproject.tests{sep}someproject.tests.csproj", aProj); - // fake a circular ref - aproj is a new object but the same file path as above - aProj = PackageFor("foo", "1.2.3", "someproject{sep}someproject.csproj", testProj); - - var items = new List - { - aProj, - testProj - }; - - var logger = Substitute.For(); - - var sorter = new PackageInProjectTopologicalSort(logger); - - var sorted = sorter.Sort(items) - .ToList(); - - AssertIsASortOf(sorted, items); - logger.Received(1).Minimal(Arg.Is( - s => s.StartsWith("Cannot sort by dependencies, cycle found at item", StringComparison.OrdinalIgnoreCase))); - } - - private static void AssertIsASortOf(List sorted, List original) - { - Assert.That(sorted, Is.Not.Null); - Assert.That(sorted, Is.Not.Empty); - Assert.That(sorted.Count, Is.EqualTo(original.Count)); - CollectionAssert.AreEquivalent(sorted, original); - } - - private static PackageInProject PackageFor(string packageId, string packageVersion, - string relativePath, PackageInProject refProject = null) - { - relativePath = relativePath.Replace("{sep}", $"{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase); - var basePath = "c_temp" + Path.DirectorySeparatorChar + "test"; - - var refs = new List(); - - if (refProject != null) - { - refs.Add(refProject.Path.FullName); - } - - var packageVersionRange = PackageVersionRange.Parse(packageId, packageVersion); - - return new PackageInProject(packageVersionRange, - new PackagePath(basePath, relativePath, PackageReferenceType.ProjectFile), - refs); - } - } -} diff --git a/NuKeeper.Inspection.Tests/Sort/ProjectReferenceTopologicalSortTests.cs b/NuKeeper.Inspection.Tests/Sort/ProjectReferenceTopologicalSortTests.cs new file mode 100644 index 000000000..764ebd84c --- /dev/null +++ b/NuKeeper.Inspection.Tests/Sort/ProjectReferenceTopologicalSortTests.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using NSubstitute; +using NuKeeper.Abstractions.Logging; +using NuKeeper.Abstractions.NuGet; +using NuKeeper.Abstractions.RepositoryInspection; +using NuKeeper.Inspection.Sort; +using NUnit.Framework; + +namespace NuKeeper.Inspection.Tests.Sort +{ + [TestFixture] + public class ProjectReferenceTopologicalSortTests + { + [Test] + public void CanSortEmptyList() + { + var dictionary = new SortedDictionary>(); + var readOnlyDictionary = new ReadOnlyDictionary>(dictionary); + + var sorter = new ProjectReferenceTopologicalSort(Substitute.For()); + + var sorted = sorter.Sort(readOnlyDictionary) + .ToList(); + + Assert.That(sorted, Is.Not.Null); + Assert.That(sorted, Is.Empty); + } + + [Test] + public void CanSortOneItem() + { + var dictionary = new SortedDictionary> + { + { "a", new List() } + }; + + var readOnlyDictionary = new ReadOnlyDictionary>(dictionary); + + var sorter = new ProjectReferenceTopologicalSort(Substitute.For()); + + var sorted = sorter.Sort(readOnlyDictionary) + .ToList(); + + AssertIsASortOf(sorted, readOnlyDictionary); + Assert.That(sorted[0], Is.EqualTo(readOnlyDictionary.First().Key)); + } + + [Test] + public void CanSortTwoUnrelatedItems() + { + var dictionary = new SortedDictionary>(new InverseStringComparer()) + { + { "a", new List() }, + { "b", new List() } + }; + + var readOnlyDictionary = new ReadOnlyDictionary>(dictionary); + + var logger = Substitute.For(); + + var sorter = new ProjectReferenceTopologicalSort(logger); + + var sorted = sorter.Sort(readOnlyDictionary) + .ToList(); + + AssertIsASortOf(sorted, readOnlyDictionary); + logger.Received(1).Detailed("No dependencies between items, no need to sort on dependencies"); + } + + [Test] + public void CanSortTwoRelatedItemsinCorrectOrder() + { + var dictionary = new SortedDictionary> + { + { "a", new List + { + "someproject{sep}someproject.csproj" + } + } + , + { "b", new List + { + "someproject{sep}someotherproject.csproj", + "a" + } + } + }; + + var readOnlyDictionary = new ReadOnlyDictionary>(dictionary); + + var logger = Substitute.For(); + + var sorter = new ProjectReferenceTopologicalSort(logger); + + var sorted = sorter.Sort(readOnlyDictionary) + .ToList(); + + AssertIsASortOf(sorted, readOnlyDictionary); + + logger.DidNotReceive().Detailed("No dependencies between items, no need to sort on dependencies"); + logger.Received(1).Detailed("Sorted 2 projects by project dependencies but no change made"); + } + + [Test] + public void CanSortTwoRelatedItemsinReverseOrder() + { + const string projectAName = "a"; + const string projectBName = "b"; + + var dictionary = new SortedDictionary>(new InverseStringComparer()) + { + { projectBName, new List + { + "someproject{sep}someotherproject.csproj", + projectAName + } + }, + { projectAName, new List + { + "someproject{sep}someproject.csproj" + } + } + }; + + var readOnlyDictionary = new ReadOnlyDictionary>(dictionary); + + var logger = Substitute.For(); + + var sorter = new ProjectReferenceTopologicalSort(logger); + + var sorted = sorter.Sort(readOnlyDictionary) + .ToList(); + + AssertIsASortOf(sorted, readOnlyDictionary); + + Assert.That(sorted[0], Is.EqualTo(projectAName)); + Assert.That(sorted[1], Is.EqualTo(projectBName)); + + logger.Received(1).Detailed(Arg.Is(s => + s.StartsWith("Resorted 2 projects by project dependencies,", StringComparison.OrdinalIgnoreCase))); + } + + [Test] + public void CanSortFourRelatedItemsWithTransitiveProjectDependency() + { + const string projectAName = "a"; + const string transitiveProjectName = "transitive"; + const string anotherTransitiveProjectName = "anotherTransitive"; + const string projectBName = "b"; + + var dictionary = new SortedDictionary>() + { + { projectBName, new List + { + "someproject{sep}someotherproject.csproj", + anotherTransitiveProjectName + } + }, + { projectAName, new List + { + "someproject{sep}someproject.csproj" + } + }, + { + transitiveProjectName, new List + { + projectAName + } + }, + { + anotherTransitiveProjectName, new List + { + transitiveProjectName + } + }, + }; + + var readOnlyDictionary = new ReadOnlyDictionary>(dictionary); + + var logger = Substitute.For(); + + var sorter = new ProjectReferenceTopologicalSort(logger); + + var sorted = sorter.Sort(readOnlyDictionary) + .ToList(); + + AssertIsASortOf(sorted, readOnlyDictionary); + + Assert.That(sorted[0], Is.EqualTo(projectAName)); + Assert.That(sorted[1], Is.EqualTo(transitiveProjectName)); + Assert.That(sorted[2], Is.EqualTo(anotherTransitiveProjectName)); + Assert.That(sorted[3], Is.EqualTo(projectBName)); + + logger.Received(1).Detailed(Arg.Is(s => + s.StartsWith("Resorted 4 projects by project dependencies,", StringComparison.OrdinalIgnoreCase))); + } + + [Test] + public void CanSortWithCycle() + { + const string projectAName = "a"; + const string projectBName = "b"; + + var dictionary = new SortedDictionary>(new InverseStringComparer()) + { + { projectBName, new List + { + "someproject{sep}someotherproject.csproj", + projectAName + } + }, + { projectAName, new List + { + "someproject{sep}someproject.csproj", + projectBName + } + } + }; + + var readOnlyDictionary = new ReadOnlyDictionary>(dictionary); + + var logger = Substitute.For(); + + var sorter = new ProjectReferenceTopologicalSort(logger); + + var sorted = sorter.Sort(readOnlyDictionary) + .ToList(); + + AssertIsASortOf(sorted, readOnlyDictionary); + + logger.Received(1).Minimal(Arg.Is( + s => s.StartsWith("Cannot sort by dependencies, cycle found at item", StringComparison.OrdinalIgnoreCase))); + } + + private static void AssertIsASortOf(List sorted, IReadOnlyDictionary> original) + { + Assert.That(sorted, Is.Not.Null); + Assert.That(sorted, Is.Not.Empty); + Assert.That(sorted.Count, Is.EqualTo(original.Count)); + CollectionAssert.AreEquivalent(sorted, original.Keys); + } + + private static void AssertIsASortOf(List sorted, List original) + { + Assert.That(sorted, Is.Not.Null); + Assert.That(sorted, Is.Not.Empty); + Assert.That(sorted.Count, Is.EqualTo(original.Count)); + CollectionAssert.AreEquivalent(sorted, original); + } + + private static PackageInProject PackageFor(string packageId, string packageVersion, + string relativePath, PackageInProject refProject = null) + { + relativePath = relativePath.Replace("{sep}", $"{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase); + var basePath = "c_temp" + Path.DirectorySeparatorChar + "test"; + + var refs = new List(); + + if (refProject != null) + { + refs.Add(refProject.Path.FullName); + } + + var packageVersionRange = PackageVersionRange.Parse(packageId, packageVersion); + + return new PackageInProject(packageVersionRange, + new PackagePath(basePath, relativePath, PackageReferenceType.ProjectFile), + refs); + } + + private class InverseStringComparer : IComparer + { + public int Compare(string x, string y) + { + var compare = string.Compare(x, y, StringComparison.OrdinalIgnoreCase); + + if (compare == 1) + return -1; + + if (compare == -1) + return 1; + + return compare; + } + } + + } +} diff --git a/NuKeeper.Inspection/RepositoryInspection/ProjectFileReader.cs b/NuKeeper.Inspection/RepositoryInspection/ProjectFileReader.cs index cd88e3fbc..b5aefc494 100644 --- a/NuKeeper.Inspection/RepositoryInspection/ProjectFileReader.cs +++ b/NuKeeper.Inspection/RepositoryInspection/ProjectFileReader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Xml.Linq; @@ -13,6 +14,9 @@ public class ProjectFileReader : IPackageReferenceFinder private const string VisualStudioLegacyProjectNamespace = "http://schemas.microsoft.com/developer/msbuild/2003"; private readonly INuKeeperLogger _logger; private readonly PackageInProjectReader _packageInProjectReader; + private readonly Dictionary> _projectReferences = new(); + + public IReadOnlyDictionary> ProjectReferences => GetProjectReferences(); public ProjectFileReader(INuKeeperLogger logger) { @@ -66,6 +70,15 @@ public IReadOnlyCollection Read(Stream fileContents, string ba .Select(el => MakeProjectPath(el, path.FullName)) .ToList(); + var pathString = path.FullName; + foreach (var referencedProjectPath in projectRefs) + { + if (!_projectReferences.ContainsKey(pathString)) + _projectReferences[pathString] = new(); + + _projectReferences[pathString].Add(referencedProjectPath); + } + var packageRefs = itemGroups.SelectMany(ig => ig.Elements(ns + "PackageReference")); projectFileResults.AddRange( packageRefs @@ -90,6 +103,13 @@ public IReadOnlyCollection Read(Stream fileContents, string ba return projectFileResults; } + private IReadOnlyDictionary> GetProjectReferences() + { + var projectReferences = _projectReferences.ToDictionary(project => project.Key, project => (IReadOnlyCollection)project.Value); + + return new ReadOnlyDictionary>(projectReferences); + } + private static string MakeProjectPath(XElement el, string currentPath) { var relativePath = el.Attribute("Include")?.Value; diff --git a/NuKeeper.Inspection/RepositoryInspection/RepositoryScanner.cs b/NuKeeper.Inspection/RepositoryInspection/RepositoryScanner.cs index 2216e6258..ab83d85d7 100644 --- a/NuKeeper.Inspection/RepositoryInspection/RepositoryScanner.cs +++ b/NuKeeper.Inspection/RepositoryInspection/RepositoryScanner.cs @@ -1,38 +1,134 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; using NuKeeper.Abstractions.Inspections.Files; +using NuKeeper.Abstractions.Logging; using NuKeeper.Abstractions.RepositoryInspection; +using NuKeeper.Inspection.Sort; namespace NuKeeper.Inspection.RepositoryInspection { public class RepositoryScanner : IRepositoryScanner { + private readonly INuKeeperLogger _logger; private readonly IReadOnlyCollection _finders; private readonly IDirectoryExclusions _directoryExclusions; + private ProjectFileReader _projectFileReader; + public RepositoryScanner( + INuKeeperLogger logger, ProjectFileReader projectFileReader, PackagesFileReader packagesFileReader, NuspecFileReader nuspecFileReader, DirectoryBuildTargetsReader directoryBuildTargetsReader, IDirectoryExclusions directoryExclusions) { + _logger = logger; + _finders = new IPackageReferenceFinder[] { projectFileReader, packagesFileReader, nuspecFileReader, directoryBuildTargetsReader }; _directoryExclusions = directoryExclusions; + + _projectFileReader = projectFileReader; } public IReadOnlyCollection FindAllNuGetPackages(IFolder workingFolder) { - return _finders + var packages = _finders .SelectMany(f => FindPackages(workingFolder, f)) .ToList(); + + var projectSorter = new ProjectReferenceTopologicalSort(_logger); + + var sortedProjects = projectSorter.Sort(BuildReferencesToAllRelevantPackageSources(packages)) + .ToList(); + + sortedProjects.Reverse(); + + var packagesInProjects = GetPackagesByReferenceSource(packages); + + return sortedProjects.Where(project => packagesInProjects.ContainsKey(project)).SelectMany(project => packagesInProjects[project]).ToList(); + } + + private ReadOnlyDictionary> BuildReferencesToAllRelevantPackageSources(List packages) + { + var references = _projectFileReader.ProjectReferences.ToDictionary(kvp => kvp.Key, kvp => new HashSet(kvp.Value)); + + var packagesByType = GroupPackagesByReferenceType(packages); + + foreach (var packageReferenceTypeCollection in packagesByType) + { + var packageReferencesForCurrentType = packageReferenceTypeCollection.Value; + + AddEmptyReferenceForMissingFiles(references, packageReferencesForCurrentType); + } + + foreach (var packageReferenceTypeCollection in packagesByType) + { + var packageReferenceType = packageReferenceTypeCollection.Key; + var packageReferencesForCurrentType = packageReferenceTypeCollection.Value; + + switch (packageReferenceType) + { + case PackageReferenceType.DirectoryBuildTargets: + AddReferencesForDirectoryBuildProps(references, packageReferencesForCurrentType); + break; + case PackageReferenceType.PackagesConfig: + AddReferencesForPackagesConfig(references, packageReferencesForCurrentType); + break; + default: + break; + } + } + + return new ReadOnlyDictionary>(references.ToDictionary(project => project.Key, project => (IReadOnlyCollection)project.Value)); + } + + private static void AddEmptyReferenceForMissingFiles(Dictionary> references, IEnumerable packageReferenceFiles) + { + foreach (var projectFile in packageReferenceFiles) + { + if (!references.ContainsKey(projectFile)) + { + references.Add(projectFile, new()); + } + } + } + + private void AddReferencesForPackagesConfig(Dictionary> references, IEnumerable packagesConfigFiles) + { + foreach (var packagesConfigFile in packagesConfigFiles) + { + var currentDirectory = Path.GetDirectoryName(packagesConfigFile); + var projectsInCurrentDirectory = references.Where(projectReference => Path.GetDirectoryName(projectReference.Key) == currentDirectory && _projectFileReader.GetFilePatterns().Any(pattern => projectReference.Key.EndsWith(pattern, StringComparison.InvariantCultureIgnoreCase))); + + foreach (var projectInCurrentDirectory in projectsInCurrentDirectory) + { + references[projectInCurrentDirectory.Key].Add(packagesConfigFile); + } + } + } + + private static void AddReferencesForDirectoryBuildProps(Dictionary> projectReferences, IEnumerable directoryBuildPropsFiles) + { + foreach (var directoryBuildPropsFile in directoryBuildPropsFiles) + { + var path = Path.GetDirectoryName(directoryBuildPropsFile); + foreach (var project in projectReferences) + { + if (project.Key != directoryBuildPropsFile && project.Key.Contains(path)) + { + projectReferences[project.Key].Add(directoryBuildPropsFile); + } + } + } } private IEnumerable FindPackages(IFolder workingFolder, @@ -50,7 +146,40 @@ private IEnumerable FindPackages(IFolder workingFolder, workingFolder.FullPath, GetRelativeFileName( workingFolder.FullPath, - f.FullName))); + f.FullName))).ToList(); + } + + private static Dictionary> GroupPackagesByReferenceType(List packages) + { + var packagesByReferenceType = new Dictionary>(); + + foreach (var package in packages) + { + if (!packagesByReferenceType.ContainsKey(package.Path.PackageReferenceType)) + packagesByReferenceType.Add(package.Path.PackageReferenceType, new()); + + var packagesOfCurrentReferenceType = packagesByReferenceType[package.Path.PackageReferenceType]; + + packagesOfCurrentReferenceType.Add(package.Path.FullName); + } + + return packagesByReferenceType; + } + + private static Dictionary> GetPackagesByReferenceSource(List packages) + { + var packagesProjectsInfos = new Dictionary>(); + + foreach (var package in packages) + { + var referencingProject = package.Path.FullName; + if (!packagesProjectsInfos.ContainsKey(referencingProject)) + packagesProjectsInfos.Add(referencingProject, new()); + + packagesProjectsInfos[referencingProject].Add(package); + } + + return packagesProjectsInfos; } private static string GetRelativeFileName(string rootDir, string fileName) diff --git a/NuKeeper.Inspection/Sort/PackageInProjectTopologicalSort.cs b/NuKeeper.Inspection/Sort/PackageInProjectTopologicalSort.cs deleted file mode 100644 index 9802e1c52..000000000 --- a/NuKeeper.Inspection/Sort/PackageInProjectTopologicalSort.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NuKeeper.Abstractions.Logging; -using NuKeeper.Abstractions.RepositoryInspection; - -namespace NuKeeper.Inspection.Sort -{ - public class PackageInProjectTopologicalSort - { - private readonly INuKeeperLogger _logger; - - public PackageInProjectTopologicalSort(INuKeeperLogger logger) - { - _logger = logger; - } - - public IEnumerable Sort(IReadOnlyCollection input) - { - var topo = new TopologicalSort(_logger, Match); - - var inputMap = input.Select(p => - new SortItemData(p, ProjectDeps(p, input))) - .ToList(); - - var sorted = topo.Sort(inputMap) - .ToList(); - - sorted.Reverse(); - - ReportSort(input.ToList(), sorted); - - return sorted; - } - - private bool Match(PackageInProject a, PackageInProject b) - { - return a.Path.FullName == b.Path.FullName; - } - - private static IReadOnlyCollection ProjectDeps(PackageInProject selected, - IReadOnlyCollection all) - { - var deps = selected.ProjectReferences; - return all.Where(i => deps.Any(d => d == i.Path.FullName)).ToList(); - } - - private void ReportSort(IList input, IList output) - { - bool hasChange = false; - - for (int i = 0; i < output.Count; i++) - { - if (input[i] != output[i]) - { - hasChange = true; - var firstChange = output[i]; - var originalIndex = input.IndexOf(firstChange); - _logger.Detailed($"Resorted {output.Count} projects by dependencies, first change is {firstChange.Path.RelativePath} moved to position {i} from {originalIndex}."); - break; - } - } - - if (!hasChange) - { - _logger.Detailed($"Sorted {output.Count} projects by dependencies but no change made"); - } - } - } -} diff --git a/NuKeeper.Inspection/Sort/ProjectReferenceTopologicalSort.cs b/NuKeeper.Inspection/Sort/ProjectReferenceTopologicalSort.cs new file mode 100644 index 000000000..950ff2e5e --- /dev/null +++ b/NuKeeper.Inspection/Sort/ProjectReferenceTopologicalSort.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; +using NuKeeper.Abstractions.Logging; + +namespace NuKeeper.Inspection.Sort +{ + public class ProjectReferenceTopologicalSort + { + private readonly INuKeeperLogger _logger; + + public ProjectReferenceTopologicalSort(INuKeeperLogger logger) + { + _logger = logger; + } + + public IEnumerable Sort(IReadOnlyDictionary> input) + { + var topo = new TopologicalSort(_logger, Match); + + var inputMap = input.Select(p => + new SortItemData(p.Key, GetProjectReferences(p.Key, input))) + .ToList(); + + var sorted = topo.Sort(inputMap) + .ToList(); + + ReportSort(inputMap.Select(m => m.Item).ToList(), sorted); + + return sorted; + } + + private bool Match(string projectPathA, string projectPathB) + { + return projectPathA == projectPathB; + } + + private static IEnumerable GetProjectReferences(string project, IReadOnlyDictionary> projectReferenceMap) + { + if (!projectReferenceMap.ContainsKey(project)) + return Enumerable.Empty(); + + return projectReferenceMap[project]; + } + + private void ReportSort(IList input, IList output) + { + bool hasChange = false; + + for (int i = 0; i < output.Count; i++) + { + if (input[i] != output[i]) + { + hasChange = true; + var firstChange = output[i]; + var originalIndex = input.IndexOf(firstChange); + _logger.Detailed($"Resorted {output.Count} projects by project dependencies, first change is {firstChange} moved to position {i} from {originalIndex}."); + break; + } + } + + if (!hasChange) + { + _logger.Detailed($"Sorted {output.Count} projects by project dependencies but no change made"); + } + } + } +} diff --git a/NuKeeper.Integration.Tests/RepositoryInspection/RepositoryScannerTests.cs b/NuKeeper.Integration.Tests/RepositoryInspection/RepositoryScannerTests.cs index cedc95825..1ad7c3d07 100644 --- a/NuKeeper.Integration.Tests/RepositoryInspection/RepositoryScannerTests.cs +++ b/NuKeeper.Integration.Tests/RepositoryInspection/RepositoryScannerTests.cs @@ -261,6 +261,7 @@ private IRepositoryScanner MakeScanner() { var logger = NukeeperLogger; return new RepositoryScanner( + logger, new ProjectFileReader(logger), new PackagesFileReader(logger), new NuspecFileReader(logger), diff --git a/NuKeeper.Update/UpdateRunner.cs b/NuKeeper.Update/UpdateRunner.cs index 405c96161..fa5faacc2 100644 --- a/NuKeeper.Update/UpdateRunner.cs +++ b/NuKeeper.Update/UpdateRunner.cs @@ -45,7 +45,7 @@ public async Task Update(PackageUpdateSet updateSet, NuGetSources sources) throw new ArgumentNullException(nameof(updateSet)); } - var sortedUpdates = Sort(updateSet.CurrentPackages); + var sortedUpdates = updateSet.CurrentPackages; _logger.Detailed($"Updating '{updateSet.SelectedId}' to {updateSet.SelectedVersion} in {sortedUpdates.Count} projects"); @@ -61,13 +61,6 @@ await updateCommand.Invoke(current, } } - private IReadOnlyCollection Sort(IReadOnlyCollection packages) - { - var sorter = new PackageInProjectTopologicalSort(_logger); - return sorter.Sort(packages) - .ToList(); - } - private IReadOnlyCollection GetUpdateCommands( PackageReferenceType packageReferenceType) {