diff --git a/README.md b/README.md index 85ca0af..d0aed72 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Each package has its own release history, tracked by tags named `-v projects, IReadOnlyDictionary tags, IReadOnlyList? commits = null) { @@ -331,9 +344,9 @@ private static PlanGenerator CreateGenerator(IReadOnlyList proj => CreateGenerator(new StubDiscoverer(projects), commits, tags); private static PlanGenerator CreateGenerator(StubDiscoverer discoverer, IGitCommitReader commits, - IReadOnlyDictionary tags) + IReadOnlyDictionary tags, IDirectoryBumpCalculator? directoryCalculator = null) => new(discoverer, commits, new StubTagReader(tags), new StubGraphBuilder(), - new StubDirectoryBumpCalculator(), new StubPackageBumpCalculator()); + directoryCalculator ?? new StubDirectoryBumpCalculator(), new StubPackageBumpCalculator()); private static async Task AssertVersions(ReleasePlan plan, params (string PackageId, string Version)[] expected) { @@ -425,8 +438,12 @@ public IReadOnlyDictionary> Buil private sealed class StubDirectoryBumpCalculator : IDirectoryBumpCalculator { - public Dictionary Calculate(IEnumerable commits, IEnumerable directories) + public string? SolutionDirectory { get; private set; } + + public Dictionary Calculate(IEnumerable commits, IEnumerable directories, + string solutionDirectory) { + SolutionDirectory = solutionDirectory; Dictionary bumps = directories.ToDictionary(directory => directory, _ => SemverBump.None, StringComparer.Ordinal); diff --git a/src/PackageCascade.Tests.Unit/Internal/Planning/SemVer/DirectoryBumpCalculatorTests.cs b/src/PackageCascade.Tests.Unit/Internal/Planning/SemVer/DirectoryBumpCalculatorTests.cs index 29748a6..1913d22 100644 --- a/src/PackageCascade.Tests.Unit/Internal/Planning/SemVer/DirectoryBumpCalculatorTests.cs +++ b/src/PackageCascade.Tests.Unit/Internal/Planning/SemVer/DirectoryBumpCalculatorTests.cs @@ -17,7 +17,7 @@ public async Task RootSharedFileChangeBumpsEveryDirectory(string path) { GitCommitDetails commit = new("commit", "build: update shared settings", [path]); - Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A", "tools/B"]); + Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A", "tools/B"], "."); using (Assert.Multiple()) { @@ -26,6 +26,41 @@ public async Task RootSharedFileChangeBumpsEveryDirectory(string path) } } + [Test] + [Arguments("Directory.Build.props")] + [Arguments("Directory.Build.targets")] + [Arguments("Directory.Packages.props")] + [Arguments("global.json")] + [Arguments("NuGet.Config")] + public async Task SolutionSharedFileChangeBumpsEveryDirectory(string filename) + { + GitCommitDetails commit = new("commit", "build: update shared settings", ["src/" + filename]); + + Dictionary bumps = new DirectoryBumpCalculator().Calculate( + [commit], ["src/A", "src/B"], "src"); + + using (Assert.Multiple()) + { + await Assert.That(bumps["src/A"]).IsEqualTo(SemverBump.Patch); + await Assert.That(bumps["src/B"]).IsEqualTo(SemverBump.Patch); + } + } + + [Test] + public async Task RepositoryRootSharedFileDoesNotBumpProjectsForNestedSolution() + { + GitCommitDetails commit = new("commit", "build: update shared settings", ["Directory.Build.props"]); + + Dictionary bumps = new DirectoryBumpCalculator().Calculate( + [commit], ["src/A", "src/B"], "src"); + + using (Assert.Multiple()) + { + await Assert.That(bumps["src/A"]).IsEqualTo(SemverBump.None); + await Assert.That(bumps["src/B"]).IsEqualTo(SemverBump.None); + } + } + [Test] [Arguments("Update shared settings", "Patch")] [Arguments("fix: correct shared settings", "Patch")] @@ -35,7 +70,7 @@ public async Task RootSharedFileUsesCommitBumpWithPatchMinimum(string message, s { GitCommitDetails commit = new("commit", message, ["Directory.Build.props"]); - Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A", "src/B"]); + Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A", "src/B"], "."); using (Assert.Multiple()) { @@ -54,7 +89,7 @@ public async Task NestedSharedFilenameOnlyAffectsItsOwnDirectory(string path) { GitCommitDetails commit = new("commit", "fix: correct settings", [path]); - Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A", "src/B"]); + Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A", "src/B"], "."); using (Assert.Multiple()) { @@ -72,7 +107,7 @@ public async Task OtherRootFilesDoNotBumpEveryDirectory(string path) { GitCommitDetails commit = new("commit", "fix: update repository file", [path]); - Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A"]); + Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A"], "."); await Assert.That(bumps["src/A"]).IsEqualTo(SemverBump.None); } @@ -89,7 +124,7 @@ public async Task CombinesSharedAndPackageChangesUsingStrongestBump(bool reverse ]; Dictionary bumps = new DirectoryBumpCalculator().Calculate( - reverseOrder ? commits.Reverse() : commits, ["src/A", "src/B"]); + reverseOrder ? commits.Reverse() : commits, ["src/A", "src/B"], "."); using (Assert.Multiple()) { @@ -109,7 +144,7 @@ public async Task MapsConventionalCommitToBump(string message, string expected) { GitCommitDetails commit = new("commit", message, ["src/A/file.txt"]); - Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A"]); + Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A"], "."); await Assert.That(bumps["src/A"].ToString()).IsEqualTo(expected); } @@ -123,7 +158,7 @@ public async Task UsesStrongestRelevantBump() new("second", "fix: repair", ["src/A/fix.cs"]), ]; - Dictionary bumps = new DirectoryBumpCalculator().Calculate(commits, ["src/A"]); + Dictionary bumps = new DirectoryBumpCalculator().Calculate(commits, ["src/A"], "."); await Assert.That(bumps["src/A"]).IsEqualTo(SemverBump.Minor); } @@ -133,7 +168,7 @@ public async Task DoesNotMatchSiblingDirectory() { GitCommitDetails commit = new("commit", "feat: add feature", ["src/AB/file.cs"]); - Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A"]); + Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A"], "."); await Assert.That(bumps["src/A"]).IsEqualTo(SemverBump.None); } @@ -143,7 +178,7 @@ public async Task MatchesProjectAtRepositoryRoot() { GitCommitDetails commit = new("commit", "fix: repair", ["Api.cs"]); - Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["."]); + Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["."], "."); await Assert.That(bumps["."]).IsEqualTo(SemverBump.Patch); } @@ -153,7 +188,7 @@ public async Task AcceptsPlatformDirectorySeparators() { GitCommitDetails commit = new("commit", "fix: repair", ["src/A/file.cs"]); - Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], [@"src\A"]); + Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], [@"src\A"], "."); await Assert.That(bumps[@"src\A"]).IsEqualTo(SemverBump.Patch); } @@ -167,7 +202,7 @@ public async Task ReturnsBumpForEachSuppliedDirectory() new("second", "fix: repair B", ["src/B/file.cs"]), ]; - Dictionary bumps = new DirectoryBumpCalculator().Calculate(commits, ["src/A", "src/B", "src/C"]); + Dictionary bumps = new DirectoryBumpCalculator().Calculate(commits, ["src/A", "src/B", "src/C"], "."); using (Assert.Multiple()) { diff --git a/src/PackageCascade/Internal/Planning/PlanGenerator.cs b/src/PackageCascade/Internal/Planning/PlanGenerator.cs index 887c598..8c5b8a5 100644 --- a/src/PackageCascade/Internal/Planning/PlanGenerator.cs +++ b/src/PackageCascade/Internal/Planning/PlanGenerator.cs @@ -37,11 +37,13 @@ public ReleasePlan Generate( string solutionPath, string configuration = "Release") { + string solutionDirectory = Path.GetRelativePath(repositoryPath, Path.GetDirectoryName(Path.GetFullPath(solutionPath))!) + .Replace('\\', '/'); IReadOnlyList projects = _packageDiscoverer.Discover(solutionPath, configuration); IReadOnlyDictionary> graph = _graphBuilder.Build(projects); IReadOnlyDictionary tags = _tagReader.ReadLatest(repositoryPath, projects.Select(project => project.PackageId)); - Dictionary directChanges = GetDirectChanges(repositoryPath, projects, tags); + Dictionary directChanges = GetDirectChanges(repositoryPath, solutionDirectory, projects, tags); Dictionary directoryBumps = directChanges.ToDictionary(entry => GetDirectory(repositoryPath, entry.Key), entry => entry.Value.Bump, StringComparer.Ordinal); IReadOnlyDictionary bumps = _bumpCalculator.Calculate(repositoryPath, graph, directoryBumps); @@ -53,9 +55,7 @@ public ReleasePlan Generate( repositoryPath, layers, selected, graph, tags, directChanges, versions); IReadOnlyList referencedPackages = GetReferencedPackages(repositoryPath, selected, graph, tags); - string workingDirectory = Path.GetRelativePath(repositoryPath, Path.GetDirectoryName(Path.GetFullPath(solutionPath))!) - .Replace('\\', '/'); - return new(packages, configuration, referencedPackages, workingDirectory); + return new(packages, configuration, referencedPackages, solutionDirectory); } private static IReadOnlyList GetReferencedPackages( @@ -89,6 +89,7 @@ private static IReadOnlyList GetReferencedPackages( private Dictionary GetDirectChanges( string repositoryPath, + string solutionDirectory, IEnumerable projects, IReadOnlyDictionary tags) { @@ -109,20 +110,20 @@ private Dictionary GetDirectChanges( commitsByBaseline.Add(tag.CommitSha, commits); } - changes.Add(project, GetDirectChange(commits, GetDirectory(repositoryPath, project))); + changes.Add(project, GetDirectChange(commits, GetDirectory(repositoryPath, project), solutionDirectory)); } return changes; } - private DirectChange GetDirectChange(IEnumerable commits, string directory) + private DirectChange GetDirectChange(IEnumerable commits, string directory, string solutionDirectory) { SemverBump strongestBump = SemverBump.None; List commitShas = []; foreach (GitCommitDetails commit in commits) { - SemverBump bump = _directoryCalculator.Calculate([commit], [directory])[directory]; + SemverBump bump = _directoryCalculator.Calculate([commit], [directory], solutionDirectory)[directory]; if (bump == SemverBump.None) { continue; diff --git a/src/PackageCascade/Internal/Planning/SemVer/DirectoryBumpCalculator.cs b/src/PackageCascade/Internal/Planning/SemVer/DirectoryBumpCalculator.cs index b484d74..5dd7815 100644 --- a/src/PackageCascade/Internal/Planning/SemVer/DirectoryBumpCalculator.cs +++ b/src/PackageCascade/Internal/Planning/SemVer/DirectoryBumpCalculator.cs @@ -6,10 +6,12 @@ namespace PackageCascade.Internal.Planning.SemVer; internal sealed partial class DirectoryBumpCalculator : IDirectoryBumpCalculator { - public Dictionary Calculate(IEnumerable commits, IEnumerable directories) + public Dictionary Calculate(IEnumerable commits, IEnumerable directories, + string solutionDirectory) { Dictionary bumps = new(StringComparer.Ordinal); Dictionary directoryPrefixes = new(StringComparer.Ordinal); + string solutionPrefix = solutionDirectory is "." or "" ? "" : solutionDirectory.TrimEnd('/', '\\').Replace('\\', '/') + "/"; foreach (string directory in directories) { @@ -20,8 +22,8 @@ public Dictionary Calculate(IEnumerable co foreach (GitCommitDetails commit in commits) { SemverBump bump = GetBump(commit.Message); - bool changesRootSharedFile = commit.ChangedPaths.Any(IsRootSharedFile); - if (changesRootSharedFile && bump < SemverBump.Patch) + bool changesSolutionSharedFile = commit.ChangedPaths.Any(path => IsSolutionSharedFile(path, solutionPrefix)); + if (changesSolutionSharedFile && bump < SemverBump.Patch) { bump = SemverBump.Patch; } @@ -31,18 +33,18 @@ public Dictionary Calculate(IEnumerable co continue; } - ApplyBumpToAffectedDirectories(commit, bump, changesRootSharedFile, directoryPrefixes, bumps); + ApplyBumpToAffectedDirectories(commit, bump, changesSolutionSharedFile, directoryPrefixes, bumps); } return bumps; } - private static void ApplyBumpToAffectedDirectories(GitCommitDetails commit, SemverBump bump, bool changesRootSharedFile, + private static void ApplyBumpToAffectedDirectories(GitCommitDetails commit, SemverBump bump, bool changesSolutionSharedFile, IReadOnlyDictionary directoryPrefixes, Dictionary bumps) { foreach ((string directory, string prefix) in directoryPrefixes) { - if ((changesRootSharedFile || commit.ChangedPaths.Any(path => path.StartsWith(prefix, StringComparison.Ordinal))) + if ((changesSolutionSharedFile || commit.ChangedPaths.Any(path => path.StartsWith(prefix, StringComparison.Ordinal))) && bump > bumps[directory]) { bumps[directory] = bump; @@ -50,7 +52,8 @@ private static void ApplyBumpToAffectedDirectories(GitCommitDetails commit, Semv } } - private static bool IsRootSharedFile(string path) => path is + private static bool IsSolutionSharedFile(string path, string solutionPrefix) => + path.StartsWith(solutionPrefix, StringComparison.Ordinal) && path[solutionPrefix.Length..] is "Directory.Build.props" or "Directory.Build.targets" or "Directory.Packages.props" or diff --git a/src/PackageCascade/Internal/Planning/SemVer/IDirectoryBumpCalculator.cs b/src/PackageCascade/Internal/Planning/SemVer/IDirectoryBumpCalculator.cs index aebd953..e733666 100644 --- a/src/PackageCascade/Internal/Planning/SemVer/IDirectoryBumpCalculator.cs +++ b/src/PackageCascade/Internal/Planning/SemVer/IDirectoryBumpCalculator.cs @@ -4,5 +4,6 @@ namespace PackageCascade.Internal.Planning.SemVer; internal interface IDirectoryBumpCalculator { - public Dictionary Calculate(IEnumerable commits, IEnumerable directories); + public Dictionary Calculate(IEnumerable commits, IEnumerable directories, + string solutionDirectory); }