From d5c77ae0b3757987be8be0852598e3d735caa711 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Wed, 23 Oct 2019 17:07:46 +0100 Subject: [PATCH 01/25] Add path filtering support to version.json --- .../BuildIntegrationTests.cs | 12 +- .../GitExtensionsTests.cs | 96 ++++++------- .../VersionFileTests.cs | 10 +- .../VersionOracleTests.cs | 6 +- src/NerdBank.GitVersioning/GitExtensions.cs | 127 ++++++++++++------ src/NerdBank.GitVersioning/VersionOptions.cs | 8 ++ src/NerdBank.GitVersioning/VersionOracle.cs | 2 +- 7 files changed, 156 insertions(+), 105 deletions(-) diff --git a/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs b/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs index a3268df9..70e7d4a2 100644 --- a/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs +++ b/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs @@ -183,7 +183,7 @@ public async Task GetBuildVersion_In_Git_But_Head_Lacks_VersionFile() this.WriteVersionFile("3.4"); Assumes.True(repo.Index[VersionFile.JsonFileName] == null); var buildResult = await this.BuildAsync(); - Assert.Equal("3.4.0." + repo.Head.Commits.First().GetIdAsVersion().Revision, buildResult.BuildVersion); + Assert.Equal("3.4.0." + repo.Head.Commits.First().GetIdAsVersion(TODO).Revision, buildResult.BuildVersion); Assert.Equal("3.4.0+" + repo.Head.Commits.First().Id.Sha.Substring(0, VersionOptions.DefaultGitCommitIdShortFixedLength), buildResult.AssemblyInformationalVersion); } @@ -208,7 +208,7 @@ public async Task GetBuildVersion_In_Git_No_VersionFile_At_All() var repo = new Repository(this.RepoPath); // do not assign Repo property to avoid commits being generated later repo.Commit("empty", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); var buildResult = await this.BuildAsync(); - Assert.Equal("0.0.0." + repo.Head.Commits.First().GetIdAsVersion().Revision, buildResult.BuildVersion); + Assert.Equal("0.0.0." + repo.Head.Commits.First().GetIdAsVersion(TODO).Revision, buildResult.BuildVersion); Assert.Equal("0.0.0+" + repo.Head.Commits.First().Id.Sha.Substring(0, VersionOptions.DefaultGitCommitIdShortFixedLength), buildResult.AssemblyInformationalVersion); } @@ -288,7 +288,7 @@ public async Task GetBuildVersion_StableRelease() var buildResult = await this.BuildAsync(); this.AssertStandardProperties(VersionOptions.FromVersion(new Version(majorMinorVersion)), buildResult); - Version version = this.Repo.Head.Commits.First().GetIdAsVersion(); + Version version = this.Repo.Head.Commits.First().GetIdAsVersion(TODO); Assert.Equal($"{version.Major}.{version.Minor}.{buildResult.GitVersionHeight}", buildResult.NuGetPackageVersion); } @@ -976,10 +976,10 @@ private static RestoreEnvironmentVariables ApplyEnvironmentVariables(IReadOnlyDi private void AssertStandardProperties(VersionOptions versionOptions, BuildResults buildResult, string relativeProjectDirectory = null) { - int versionHeight = this.Repo.GetVersionHeight(relativeProjectDirectory); - Version idAsVersion = this.Repo.GetIdAsVersion(relativeProjectDirectory); + int versionHeight = this.Repo.GetVersionHeight(TODO, relativeProjectDirectory); + Version idAsVersion = this.Repo.GetIdAsVersion(TODO, relativeProjectDirectory); string commitIdShort = this.CommitIdShort; - Version version = this.Repo.GetIdAsVersion(relativeProjectDirectory); + Version version = this.Repo.GetIdAsVersion(TODO, relativeProjectDirectory); Version assemblyVersion = GetExpectedAssemblyVersion(versionOptions, version); var additionalBuildMetadata = from item in buildResult.BuildResult.ProjectStateAfterBuild.GetItems("BuildMetadata") select item.EvaluatedInclude; diff --git a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs index 231c6a38..0dea26cd 100644 --- a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs +++ b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs @@ -23,8 +23,8 @@ public GitExtensionsTests(ITestOutputHelper Logger) public void GetHeight_EmptyRepo() { Branch head = this.Repo.Head; - Assert.Throws(() => head.GetHeight()); - Assert.Throws(() => head.GetHeight(c => true)); + Assert.Throws(() => head.GetHeight(TODO)); + Assert.Throws(() => head.GetHeight(TODO, c => true)); } [Fact] @@ -33,11 +33,11 @@ public void GetHeight_SinglePath() var first = this.Repo.Commit("First", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); var second = this.Repo.Commit("Second", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); var third = this.Repo.Commit("Third", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - Assert.Equal(3, this.Repo.Head.GetHeight()); - Assert.Equal(3, this.Repo.Head.GetHeight(c => true)); + Assert.Equal(3, this.Repo.Head.GetHeight(TODO)); + Assert.Equal(3, this.Repo.Head.GetHeight(TODO, c => true)); - Assert.Equal(2, this.Repo.Head.GetHeight(c => c != first)); - Assert.Equal(1, this.Repo.Head.GetHeight(c => c != second)); + Assert.Equal(2, this.Repo.Head.GetHeight(TODO, c => c != first)); + Assert.Equal(1, this.Repo.Head.GetHeight(TODO, c => c != second)); } [Fact] @@ -56,13 +56,13 @@ public void GetHeight_Merge() this.Repo.Merge(secondCommit, new Signature("t", "t@t.com", DateTimeOffset.Now), new MergeOptions { FastForwardStrategy = FastForwardStrategy.NoFastForward }); // While we've created 8 commits, the tallest height is only 7. - Assert.Equal(7, this.Repo.Head.GetHeight()); + Assert.Equal(7, this.Repo.Head.GetHeight(TODO)); // Now stop enumerating early on just one branch of the ancestry -- the number should remain high. - Assert.Equal(7, this.Repo.Head.GetHeight(c => c != secondCommit)); + Assert.Equal(7, this.Repo.Head.GetHeight(TODO, c => c != secondCommit)); // This time stop in both branches of history, and verify that we count the taller one. - Assert.Equal(3, this.Repo.Head.GetHeight(c => c != secondCommit && c != branchCommits[2])); + Assert.Equal(3, this.Repo.Head.GetHeight(TODO, c => c != secondCommit && c != branchCommits[2])); } [Fact] @@ -72,7 +72,7 @@ public void GetVersionHeight() var second = this.Repo.Commit("Second", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); this.WriteVersionFile(); var third = this.Repo.Commit("Third", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - Assert.Equal(2, this.Repo.Head.GetVersionHeight()); + Assert.Equal(2, this.Repo.Head.GetVersionHeight(TODO)); } [Fact] @@ -81,44 +81,44 @@ public void GetVersionHeight_VersionJsonHasUnrelatedHistory() // Emulate a repo that used version.json for something else. string versionJsonPath = Path.Combine(this.RepoPath, "version.json"); File.WriteAllText(versionJsonPath, @"{ ""unrelated"": false }"); - Assert.Equal(0, this.Repo.GetVersionHeight()); // exercise code that handles the file not yet checked in. + Assert.Equal(0, this.Repo.GetVersionHeight(TODO)); // exercise code that handles the file not yet checked in. Commands.Stage(this.Repo, versionJsonPath); this.Repo.Commit("Add unrelated version.json file.", this.Signer, this.Signer); - Assert.Equal(0, this.Repo.GetVersionHeight()); // exercise code that handles a checked in file. + Assert.Equal(0, this.Repo.GetVersionHeight(TODO)); // exercise code that handles a checked in file. // And now the repo has decided to use this package. this.WriteVersionFile(); - Assert.Equal(1, this.Repo.Head.GetVersionHeight()); - Assert.Equal(1, this.Repo.GetVersionHeight()); + Assert.Equal(1, this.Repo.Head.GetVersionHeight(TODO)); + Assert.Equal(1, this.Repo.GetVersionHeight(TODO)); // Also emulate case of where the related version.json was just changed to conform, // but not yet checked in. this.Repo.Reset(ResetMode.Mixed, this.Repo.Head.Tip.Parents.Single()); - Assert.Equal(0, this.Repo.GetVersionHeight()); + Assert.Equal(0, this.Repo.GetVersionHeight(TODO)); } [Fact] public void GetVersionHeight_VersionJsonHasParsingErrorsInHistory() { this.WriteVersionFile(); - Assert.Equal(1, this.Repo.GetVersionHeight()); + Assert.Equal(1, this.Repo.GetVersionHeight(TODO)); // Now introduce a parsing error. string versionJsonPath = Path.Combine(this.RepoPath, "version.json"); File.WriteAllText(versionJsonPath, @"{ ""version"": ""1.0"""); // no closing curly brace for parsing error - Assert.Equal(0, this.Repo.GetVersionHeight()); + Assert.Equal(0, this.Repo.GetVersionHeight(TODO)); Commands.Stage(this.Repo, versionJsonPath); this.Repo.Commit("Add broken version.json file.", this.Signer, this.Signer); - Assert.Equal(0, this.Repo.GetVersionHeight()); + Assert.Equal(0, this.Repo.GetVersionHeight(TODO)); // Now fix it. this.WriteVersionFile(); - Assert.Equal(1, this.Repo.GetVersionHeight()); + Assert.Equal(1, this.Repo.GetVersionHeight(TODO)); // And emulate fixing it without having checked in yet. this.Repo.Reset(ResetMode.Mixed, this.Repo.Head.Tip.Parents.Single()); - Assert.Equal(0, this.Repo.GetVersionHeight()); + Assert.Equal(0, this.Repo.GetVersionHeight(TODO)); } [Theory] @@ -145,8 +145,8 @@ public void GetVersionHeight_ProgressAndReset(string version1, string version2, new VersionOptions { Version = semanticVersion2 }, repoRelativeSubDirectory); - int height2 = this.Repo.Head.GetVersionHeight(repoRelativeSubDirectory); - int height1 = this.Repo.Head.Commits.Skip(1).First().GetVersionHeight(repoRelativeSubDirectory); + int height2 = this.Repo.Head.GetVersionHeight(TODO, repoRelativeSubDirectory); + int height1 = this.Repo.Head.Commits.Skip(1).First().GetVersionHeight(repoRelativeSubDirectory, TODO); this.Logger.WriteLine("Height 1: {0}", height1); this.Logger.WriteLine("Height 2: {0}", height2); @@ -176,7 +176,7 @@ public void GetIdAsVersion_ReadsMajorMinorFromVersionTxt() this.WriteVersionFile("4.8"); var firstCommit = this.Repo.Commits.First(); - Version v1 = firstCommit.GetIdAsVersion(); + Version v1 = firstCommit.GetIdAsVersion(TODO); Assert.Equal(4, v1.Major); Assert.Equal(8, v1.Minor); } @@ -187,7 +187,7 @@ public void GetIdAsVersion_ReadsMajorMinorFromVersionTxtInSubdirectory() this.WriteVersionFile("4.8", relativeDirectory: @"foo\bar"); var firstCommit = this.Repo.Commits.First(); - Version v1 = firstCommit.GetIdAsVersion(@"foo\bar"); + Version v1 = firstCommit.GetIdAsVersion(TODO, @"foo\bar"); Assert.Equal(4, v1.Major); Assert.Equal(8, v1.Minor); } @@ -198,7 +198,7 @@ public void GetIdAsVersion_MissingVersionTxt() this.AddCommits(); var firstCommit = this.Repo.Commits.First(); - Version v1 = firstCommit.GetIdAsVersion(); + Version v1 = firstCommit.GetIdAsVersion(TODO); Assert.Equal(0, v1.Major); Assert.Equal(0, v1.Minor); } @@ -210,7 +210,7 @@ public void GetIdAsVersion_VersionFileNeverCheckedIn_3Ints() var expectedVersion = new Version(1, 1, 0); var unstagedVersionData = VersionOptions.FromVersion(expectedVersion); string versionFilePath = VersionFile.SetVersion(this.RepoPath, unstagedVersionData); - Version actualVersion = this.Repo.GetIdAsVersion(); + Version actualVersion = this.Repo.GetIdAsVersion(TODO); Assert.Equal(expectedVersion.Major, actualVersion.Major); Assert.Equal(expectedVersion.Minor, actualVersion.Minor); Assert.Equal(expectedVersion.Build, actualVersion.Build); @@ -227,7 +227,7 @@ public void GetIdAsVersion_VersionFileNeverCheckedIn_2Ints() var expectedVersion = new Version(1, 1); var unstagedVersionData = VersionOptions.FromVersion(expectedVersion); string versionFilePath = VersionFile.SetVersion(this.RepoPath, unstagedVersionData); - Version actualVersion = this.Repo.GetIdAsVersion(); + Version actualVersion = this.Repo.GetIdAsVersion(TODO); Assert.Equal(expectedVersion.Major, actualVersion.Major); Assert.Equal(expectedVersion.Minor, actualVersion.Minor); Assert.Equal(0, actualVersion.Build); // height is 0 since the change hasn't been committed. @@ -242,7 +242,7 @@ public void GetIdAsVersion_VersionFileChangedOnDisk() this.AddCommits(); // Verify that we're seeing the original version. - Version actualVersion = this.Repo.GetIdAsVersion(); + Version actualVersion = this.Repo.GetIdAsVersion(TODO); Assert.Equal(1, actualVersion.Major); Assert.Equal(2, actualVersion.Minor); Assert.Equal(2, actualVersion.Build); @@ -252,7 +252,7 @@ public void GetIdAsVersion_VersionFileChangedOnDisk() string versionFile = VersionFile.SetVersion(this.RepoPath, new Version("1.3")); // Verify that HEAD reports whatever is on disk at the time. - actualVersion = this.Repo.GetIdAsVersion(); + actualVersion = this.Repo.GetIdAsVersion(TODO); Assert.Equal(1, actualVersion.Major); Assert.Equal(3, actualVersion.Minor); Assert.Equal(0, actualVersion.Build); @@ -260,7 +260,7 @@ public void GetIdAsVersion_VersionFileChangedOnDisk() // Now commit it and verify the height advances 0->1 this.CommitVersionFile(versionFile, "1.3"); - actualVersion = this.Repo.GetIdAsVersion(); + actualVersion = this.Repo.GetIdAsVersion(TODO); Assert.Equal(1, actualVersion.Major); Assert.Equal(3, actualVersion.Minor); Assert.Equal(1, actualVersion.Build); @@ -305,13 +305,13 @@ public void GetIdAsVersion_Roundtrip(string version, string assemblyVersion, int for (int i = 0; i < commits.Length; i++) { commits[i] = this.Repo.Commit($"Commit {i + 1}", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - versions[i] = commits[i].GetIdAsVersion(repoRelativeSubDirectory); + versions[i] = commits[i].GetIdAsVersion(TODO, repoRelativeSubDirectory); this.Logger.WriteLine($"Commit {commits[i].Id.Sha.Substring(0, 8)} as version: {versions[i]}"); } for (int i = 0; i < commits.Length; i++) { - Assert.Equal(commits[i], this.Repo.GetCommitFromVersion(versions[i], repoRelativeSubDirectory)); + Assert.Equal(commits[i], this.Repo.GetCommitFromVersion(versions[i], TODO, repoRelativeSubDirectory)); // Also verify that we can find it without the revision number. // This is important because stable, publicly released NuGet packages @@ -319,7 +319,7 @@ public void GetIdAsVersion_Roundtrip(string version, string assemblyVersion, int // But folks who specify a.b.c version numbers don't have any unique version component for the commit at all without the 4th integer. if (semanticVersion.Version.Build == -1) { - Assert.Equal(commits[i], this.Repo.GetCommitFromVersion(new Version(versions[i].Major, versions[i].Minor, versions[i].Build), repoRelativeSubDirectory)); + Assert.Equal(commits[i], this.Repo.GetCommitFromVersion(new Version(versions[i].Major, versions[i].Minor, versions[i].Build), TODO, repoRelativeSubDirectory)); } } } @@ -344,10 +344,10 @@ public void GetIdAsVersion_Roundtrip_UnstableOffset(int startingOffset, int offs { versionOptions.VersionHeightOffset += offsetStepChange; commits[i] = this.WriteVersionFile(versionOptions); - versions[i] = commits[i].GetIdAsVersion(); + versions[i] = commits[i].GetIdAsVersion(TODO); commits[i + 1] = this.Repo.Commit($"Commit {i + 1}", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - versions[i + 1] = commits[i + 1].GetIdAsVersion(); + versions[i + 1] = commits[i + 1].GetIdAsVersion(TODO); this.Logger.WriteLine($"Commit {commits[i].Id.Sha.Substring(0, 8)} as version: {versions[i]}"); this.Logger.WriteLine($"Commit {commits[i + 1].Id.Sha.Substring(0, 8)} as version: {versions[i + 1]}"); @@ -386,20 +386,20 @@ public void GetIdAsVersion_Roundtrip_WithSubdirectoryVersionFiles() this.InitializeSourceControl(); Commit head = this.Repo.Head.Commits.First(); - Version rootVersionActual = head.GetIdAsVersion(); - Version subPathVersionActual = head.GetIdAsVersion(subPathRelative); + Version rootVersionActual = head.GetIdAsVersion(TODO); + Version subPathVersionActual = head.GetIdAsVersion(TODO, subPathRelative); // Verify that the versions calculated took the path into account. Assert.Equal(rootVersionExpected.Version.Version.Minor, rootVersionActual?.Minor); Assert.Equal(subPathVersionExpected.Version.Version.Minor, subPathVersionActual?.Minor); // Verify that we can find the commit given the version and path. - Assert.Equal(head, this.Repo.GetCommitFromVersion(rootVersionActual)); - Assert.Equal(head, this.Repo.GetCommitFromVersion(subPathVersionActual, subPathRelative)); + Assert.Equal(head, this.Repo.GetCommitFromVersion(rootVersionActual, TODO)); + Assert.Equal(head, this.Repo.GetCommitFromVersion(subPathVersionActual, TODO, subPathRelative)); // Verify that mismatching path and version results in a null value. - Assert.Null(this.Repo.GetCommitFromVersion(rootVersionActual, subPathRelative)); - Assert.Null(this.Repo.GetCommitFromVersion(subPathVersionActual)); + Assert.Null(this.Repo.GetCommitFromVersion(rootVersionActual, TODO, subPathRelative)); + Assert.Null(this.Repo.GetCommitFromVersion(subPathVersionActual, TODO)); } [Fact] @@ -408,7 +408,7 @@ public void GetIdAsVersion_FitsInsideCompilerConstraints() this.WriteVersionFile("2.5"); var firstCommit = this.Repo.Commits.First(); - Version version = firstCommit.GetIdAsVersion(); + Version version = firstCommit.GetIdAsVersion(TODO); this.Logger.WriteLine(version.ToString()); // The C# compiler produces a build warning and truncates the version number if it exceeds 0xfffe, @@ -427,12 +427,12 @@ public void GetIdAsVersion_MigrationFromVersionTxtToJson() var jsonCommit = this.WriteVersionFile("4.8"); Assert.True(File.Exists(Path.Combine(this.RepoPath, "version.json"))); - Version v1 = txtCommit.GetIdAsVersion(); + Version v1 = txtCommit.GetIdAsVersion(TODO); Assert.Equal(4, v1.Major); Assert.Equal(8, v1.Minor); Assert.Equal(1, v1.Build); - Version v2 = jsonCommit.GetIdAsVersion(); + Version v2 = jsonCommit.GetIdAsVersion(TODO); Assert.Equal(4, v2.Major); Assert.Equal(8, v2.Minor); Assert.Equal(2, v2.Build); @@ -448,9 +448,9 @@ public void TestBiggerRepo() { foreach (var commit in this.Repo.Head.Commits) { - var version = commit.GetIdAsVersion(); + var version = commit.GetIdAsVersion(TODO); this.Logger.WriteLine($"commit {commit.Id} got version {version}"); - var backAgain = this.Repo.GetCommitFromVersion(version); + var backAgain = this.Repo.GetCommitFromVersion(version, TODO); Assert.Equal(commit, backAgain); } } @@ -475,9 +475,9 @@ private void VerifyCommitsWithVersion(Commit[] commits) for (int i = 0; i < commits.Length; i++) { - Version encodedVersion = commits[i].GetIdAsVersion(); + Version encodedVersion = commits[i].GetIdAsVersion(TODO); Assert.Equal(i + 1, encodedVersion.Build); - Assert.Equal(commits[i], this.Repo.GetCommitFromVersion(encodedVersion)); + Assert.Equal(commits[i], this.Repo.GetCommitFromVersion(encodedVersion, TODO)); } } } diff --git a/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs b/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs index 7e959d04..8508c044 100644 --- a/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs +++ b/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs @@ -440,13 +440,13 @@ public void VersionJson_Inheritance(bool commitInSourceControl, bool bareRepo) // The version height should be the same for all those that inherit the version from the base, // even though the inheriting files were introduced in successive commits. - Assert.Equal(totalCommits, operatingRepo.GetVersionHeight()); - Assert.Equal(totalCommits, operatingRepo.GetVersionHeight("foo")); - Assert.Equal(totalCommits, operatingRepo.GetVersionHeight(@"foo\bar")); + Assert.Equal(totalCommits, operatingRepo.GetVersionHeight(TODO)); + Assert.Equal(totalCommits, operatingRepo.GetVersionHeight(TODO, "foo")); + Assert.Equal(totalCommits, operatingRepo.GetVersionHeight(TODO, @"foo\bar")); // These either don't inherit, or inherit but reset versions, so the commits were reset. - Assert.Equal(2, operatingRepo.GetVersionHeight("noInherit")); - Assert.Equal(1, operatingRepo.GetVersionHeight("inheritWithVersion")); + Assert.Equal(2, operatingRepo.GetVersionHeight(TODO, "noInherit")); + Assert.Equal(1, operatingRepo.GetVersionHeight(TODO, "inheritWithVersion")); } } } diff --git a/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs b/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs index 2e567108..d159d95d 100644 --- a/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs +++ b/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs @@ -101,18 +101,18 @@ public void VersionHeightResetsWithVersionSpecChanges(string initial, string nex var oracle = VersionOracle.Create(this.RepoPath); Assert.Equal(11, oracle.VersionHeight); - Assert.Equal(11, this.Repo.Head.GetVersionHeight()); + Assert.Equal(11, this.Repo.Head.GetVersionHeight(TODO)); options.Version = SemanticVersion.Parse(next); this.WriteVersionFile(options); oracle = VersionOracle.Create(this.RepoPath); Assert.Equal(1, oracle.VersionHeight); - Assert.Equal(1, this.Repo.Head.GetVersionHeight()); + Assert.Equal(1, this.Repo.Head.GetVersionHeight(TODO)); foreach (var commit in this.Repo.Head.Commits) { - var versionFromId = commit.GetIdAsVersion(); + var versionFromId = commit.GetIdAsVersion(TODO); Assert.Contains(commit, this.Repo.GetCommitsFromVersion(versionFromId)); } } diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index 01f7f135..85e2bc87 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -38,9 +38,12 @@ public static class GitExtensions /// /// The commit to measure the height of. /// The repo-relative project directory for which to calculate the version. + /// /// Optional base version to calculate the height. If not specified, the base version will be calculated by scanning the repository. /// The height of the commit. Always a positive integer. - public static int GetVersionHeight(this Commit commit, string repoRelativeProjectDirectory = null, Version baseVersion = null) + public static int GetVersionHeight(this Commit commit, + string repoRelativeProjectDirectory = null, IEnumerable pathFilters = null, + Version baseVersion = null) { Requires.NotNull(commit, nameof(commit)); Requires.Argument(repoRelativeProjectDirectory == null || !Path.IsPathRooted(repoRelativeProjectDirectory), nameof(repoRelativeProjectDirectory), "Path should be relative to repo root."); @@ -58,7 +61,7 @@ public static int GetVersionHeight(this Commit commit, string repoRelativeProjec var versionHeightPosition = versionOptions.VersionHeightPosition; if (versionHeightPosition.HasValue) { - int height = commit.GetHeight(c => CommitMatchesVersion(c, baseSemVer, versionHeightPosition.Value, repoRelativeProjectDirectory)); + int height = commit.GetHeight(pathFilters, c => CommitMatchesVersion(c, baseSemVer, versionHeightPosition.Value, repoRelativeProjectDirectory)); return height; } @@ -72,9 +75,11 @@ public static int GetVersionHeight(this Commit commit, string repoRelativeProjec /// (or HEAD for bare repositories). /// /// The repo with the working copy / HEAD to measure the height of. + /// /// The repo-relative project directory for which to calculate the version. /// The height of the repo at HEAD. Always a positive integer. - public static int GetVersionHeight(this Repository repo, string repoRelativeProjectDirectory = null) + public static int GetVersionHeight(this Repository repo, IEnumerable pathFilters, + string repoRelativeProjectDirectory = null) { if (repo == null) { @@ -95,7 +100,7 @@ public static int GetVersionHeight(this Repository repo, string repoRelativeProj } // No special changes in the working directory, so apply regular logic. - return GetVersionHeight(repo.Head, repoRelativeProjectDirectory); + return GetVersionHeight(repo.Head, pathFilters, repoRelativeProjectDirectory); } /// @@ -104,11 +109,13 @@ public static int GetVersionHeight(this Repository repo, string repoRelativeProj /// that set the version to the value at the tip of the . /// /// The branch to measure the height of. + /// /// The repo-relative project directory for which to calculate the version. /// The height of the branch till the version is changed. - public static int GetVersionHeight(this Branch branch, string repoRelativeProjectDirectory = null) + public static int GetVersionHeight(this Branch branch, IEnumerable pathFilters, + string repoRelativeProjectDirectory = null) { - return GetVersionHeight(branch.Commits.First(), repoRelativeProjectDirectory); + return GetVersionHeight(branch.Commits.First(), repoRelativeProjectDirectory, pathFilters); } /// @@ -116,18 +123,20 @@ public static int GetVersionHeight(this Branch branch, string repoRelativeProjec /// the specified commit and the most distant ancestor (inclusive). /// /// The commit to measure the height of. + /// /// - /// A function that returns false when we reach a commit that - /// should not be included in the height calculation. - /// May be null to count the height to the original commit. + /// A function that returns false when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. /// /// The height of the commit. Always a positive integer. - public static int GetHeight(this Commit commit, Func continueStepping = null) + public static int GetHeight(this Commit commit, IEnumerable pathFilters, + Func continueStepping = null) { Requires.NotNull(commit, nameof(commit)); var heights = new Dictionary(); - return GetCommitHeight(commit, heights, continueStepping); + return GetCommitHeight(commit, heights, pathFilters, continueStepping); } /// @@ -135,15 +144,17 @@ public static int GetHeight(this Commit commit, Func continueStepp /// the specified branch's head and the most distant ancestor (inclusive). /// /// The branch to measure the height of. + /// /// - /// A function that returns false when we reach a commit that - /// should not be included in the height calculation. - /// May be null to count the height to the original commit. + /// A function that returns false when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. /// /// The height of the branch. - public static int GetHeight(this Branch branch, Func continueStepping = null) + public static int GetHeight(this Branch branch, IEnumerable pathFilters, + Func continueStepping = null) { - return GetHeight(branch.Commits.First(), continueStepping); + return GetHeight(branch.Commits.First(), pathFilters, continueStepping); } /// @@ -189,10 +200,11 @@ public static Commit GetCommitFromTruncatedIdInteger(this Repository repo, int t /// so that the original commit can be found later. /// /// The commit whose ID and position in history is to be encoded. + /// /// The repo-relative project directory for which to calculate the version. /// - /// The version height, previously calculated by a call to - /// with the same value for . + /// The version height, previously calculated by a call to + /// with the same value for . /// /// /// A version whose and @@ -203,7 +215,8 @@ public static Commit GetCommitFromTruncatedIdInteger(this Repository repo, int t /// the height of the git commit while the /// component is the first four bytes of the git commit id (forced to be a positive integer). /// - public static Version GetIdAsVersion(this Commit commit, string repoRelativeProjectDirectory = null, int? versionHeight = null) + public static Version GetIdAsVersion(this Commit commit, IEnumerable pathFilters, + string repoRelativeProjectDirectory = null, int? versionHeight = null) { Requires.NotNull(commit, nameof(commit)); Requires.Argument(repoRelativeProjectDirectory == null || !Path.IsPathRooted(repoRelativeProjectDirectory), nameof(repoRelativeProjectDirectory), "Path should be relative to repo root."); @@ -212,7 +225,7 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj if (!versionHeight.HasValue) { - versionHeight = GetVersionHeight(commit, repoRelativeProjectDirectory); + versionHeight = GetVersionHeight(commit, repoRelativeProjectDirectory, pathFilters); } return GetIdAsVersionHelper(commit, versionOptions, versionHeight.Value); @@ -223,10 +236,11 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj /// so that the original commit can be found later. /// /// The repo whose ID and position in history is to be encoded. + /// /// The repo-relative project directory for which to calculate the version. /// - /// The version height, previously calculated by a call to - /// with the same value for . + /// The version height, previously calculated by a call to + /// with the same value for . /// /// /// A version whose and @@ -237,7 +251,8 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj /// the height of the git commit while the /// component is the first four bytes of the git commit id (forced to be a positive integer). /// - public static Version GetIdAsVersion(this Repository repo, string repoRelativeProjectDirectory = null, int? versionHeight = null) + public static Version GetIdAsVersion(this Repository repo, IEnumerable pathFilters, + string repoRelativeProjectDirectory = null, int? versionHeight = null) { Requires.NotNull(repo, nameof(repo)); @@ -249,29 +264,31 @@ public static Version GetIdAsVersion(this Repository repo, string repoRelativePr if (!versionHeight.HasValue) { var baseVersion = workingCopyVersionOptions?.Version?.Version; - versionHeight = GetVersionHeight(headCommit, repoRelativeProjectDirectory, baseVersion); + versionHeight = GetVersionHeight(headCommit, repoRelativeProjectDirectory, pathFilters, baseVersion); } Version result = GetIdAsVersionHelper(headCommit, workingCopyVersionOptions, versionHeight.Value); return result; } - return GetIdAsVersion(headCommit, repoRelativeProjectDirectory); + return GetIdAsVersion(headCommit, pathFilters, repoRelativeProjectDirectory); } /// /// Looks up the commit that matches a specified version number. /// /// The repository to search for a matching commit. - /// The version previously obtained from . + /// The version previously obtained from . + /// /// - /// The repo-relative project directory from which was originally calculated. + /// The repo-relative project directory from which was originally calculated. /// /// The matching commit, or null if no match is found. /// /// Thrown in the very rare situation that more than one matching commit is found. /// - public static Commit GetCommitFromVersion(this Repository repo, Version version, string repoRelativeProjectDirectory = null) + public static Commit GetCommitFromVersion(this Repository repo, Version version, IEnumerable pathFilters, + string repoRelativeProjectDirectory = null) { // Note we'll accept no match, or one match. But we throw if there is more than one match. return GetCommitsFromVersion(repo, version, repoRelativeProjectDirectory).SingleOrDefault(); @@ -281,10 +298,11 @@ public static Commit GetCommitFromVersion(this Repository repo, Version version, /// Looks up the commits that match a specified version number. /// /// The repository to search for a matching commit. - /// The version previously obtained from . + /// The version previously obtained from . /// The repo-relative project directory from which was originally calculated. /// The matching commits, or an empty enumeration if no match is found. - public static IEnumerable GetCommitsFromVersion(this Repository repo, Version version, string repoRelativeProjectDirectory = null) + public static IEnumerable GetCommitsFromVersion(this Repository repo, Version version, + string repoRelativeProjectDirectory = null) { Requires.NotNull(repo, nameof(repo)); Requires.NotNull(version, nameof(version)); @@ -293,7 +311,7 @@ public static IEnumerable GetCommitsFromVersion(this Repository repo, Ve let commitVersionOptions = VersionFile.GetVersion(commit, repoRelativeProjectDirectory) where commitVersionOptions != null where !IsCommitIdMismatch(version, commitVersionOptions, commit) - where !IsVersionHeightMismatch(version, commitVersionOptions, commit, repoRelativeProjectDirectory) + where !IsVersionHeightMismatch(version, commitVersionOptions, commit, repoRelativeProjectDirectory, commitVersionOptions.PathFilters) select commit; return possibleCommits; @@ -387,7 +405,7 @@ public static string FindLibGit2NativeBinaries(string basePath) /// /// Specifies whether to use default settings for looking up global and system settings. /// - /// By default ( == false), the repository will be configured to only + /// By default ( == false), the repository will be configured to only /// use the repository-level configuration ignoring system or user-level configuration (set using git config --global. /// Thus only settings explicitly set for the repo will be available. /// @@ -401,7 +419,7 @@ public static string FindLibGit2NativeBinaries(string basePath) /// /// /// In this mode, using Repository.Configuration.Get{string}("user.name") will return the - /// value set in the user's global git configuration unless set on the repository level, + /// value set in the user's global git configuration unless set on the repository level, /// matching the behavior of the git command. /// /// @@ -525,7 +543,8 @@ private static int ReadVersionPosition(Version version, SemanticVersion.Position } } - private static bool IsVersionHeightMismatch(Version version, VersionOptions versionOptions, Commit commit, string repoRelativeProjectDirectory) + private static bool IsVersionHeightMismatch(Version version, VersionOptions versionOptions, Commit commit, + string repoRelativeProjectDirectory, IEnumerable pathFilters) { Requires.NotNull(version, nameof(version)); Requires.NotNull(versionOptions, nameof(versionOptions)); @@ -538,7 +557,7 @@ private static bool IsVersionHeightMismatch(Version version, VersionOptions vers int expectedVersionHeight = ReadVersionPosition(version, position.Value); var actualVersionOffset = versionOptions.VersionHeightOffsetOrDefault; - var actualVersionHeight = commit.GetHeight(c => CommitMatchesVersion(c, version, position.Value - 1, repoRelativeProjectDirectory)); + var actualVersionHeight = commit.GetHeight(pathFilters, c => CommitMatchesVersion(c, version, position.Value - 1, repoRelativeProjectDirectory)); return expectedVersionHeight != actualVersionHeight + actualVersionOffset; } @@ -652,13 +671,20 @@ private static string EncodeAsHex(byte[] buffer) /// /// The commit to measure the height of. /// A cache of commits and their heights. + /// + /// An array of paths to filter the changes in commits on. If a commit + /// does not affect any paths in the filter, the commit does not + /// contribute to the height. May be null to disable filtering. + /// /// - /// A function that returns false when we reach a commit that - /// should not be included in the height calculation. - /// May be null to count the height to the original commit. + /// A function that returns false when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. /// /// The height of the branch. - private static int GetCommitHeight(Commit commit, Dictionary heights, Func continueStepping) + private static int GetCommitHeight(Commit commit, Dictionary heights, + IEnumerable pathFilters, + Func continueStepping) { Requires.NotNull(commit, nameof(commit)); Requires.NotNull(heights, nameof(heights)); @@ -669,10 +695,27 @@ private static int GetCommitHeight(Commit commit, Dictionary heig height = 0; if (continueStepping == null || continueStepping(commit)) { - height = 1; + if (pathFilters != null) + { + var repository = ((IBelongToARepository)commit).Repository; + + // If the diff between this commit and any of its parents + // touches a path that we care about, bump the height. + // Otherwise, this commit will not increment the height. + if (commit.Parents.Any(parent => + repository.Diff.Compare(parent.Tree, commit.Tree, pathFilters).Any())) + { + height = 1; + } + } + else + { + height = 1; + } + if (commit.Parents.Any()) { - height += commit.Parents.Max(p => GetCommitHeight(p, heights, continueStepping)); + height += commit.Parents.Max(p => GetCommitHeight(p, heights, pathFilters, continueStepping)); } } @@ -732,7 +775,7 @@ private static void AddReachableCommitsFrom(Commit startingCommit, HashSet /// The commit whose ID and position in history is to be encoded. /// The version options applicable at this point (either from commit or working copy). - /// The version height, previously calculated by a call to . + /// The version height, previously calculated by a call to . /// /// A version whose and /// components are calculated based on the commit. diff --git a/src/NerdBank.GitVersioning/VersionOptions.cs b/src/NerdBank.GitVersioning/VersionOptions.cs index 3eaabef5..aafe0685 100644 --- a/src/NerdBank.GitVersioning/VersionOptions.cs +++ b/src/NerdBank.GitVersioning/VersionOptions.cs @@ -205,6 +205,14 @@ public int VersionHeightOffsetOrDefault [JsonIgnore] public ReleaseOptions ReleaseOrDefault => this.Release ?? ReleaseOptions.DefaultInstance; + /// + /// A list of paths to use to filter commits when calculating version height. + /// If a given commit does not affect any paths in this filter, it is ignored for version height calculations. + /// Paths should be relative to the root of the repository. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public IReadOnlyList PathFilters; + /// /// Gets or sets a value indicating whether this options object should inherit from an ancestor any settings that are not explicitly set in this one. /// diff --git a/src/NerdBank.GitVersioning/VersionOracle.cs b/src/NerdBank.GitVersioning/VersionOracle.cs index a4bb2a75..9f2a180e 100644 --- a/src/NerdBank.GitVersioning/VersionOracle.cs +++ b/src/NerdBank.GitVersioning/VersionOracle.cs @@ -495,7 +495,7 @@ private static int CalculateVersionHeight(string relativeRepoProjectDirectory, L } } - return headCommit?.GetVersionHeight(relativeRepoProjectDirectory) ?? 0; + return headCommit?.GetVersionHeight(relativeRepoProjectDirectory, workingVersion?.PathFilters) ?? 0; } private static Version GetIdAsVersion(LibGit2Sharp.Commit headCommit, VersionOptions committedVersion, VersionOptions workingVersion, int versionHeight) From db5df92dde72c51a23a81999df24320a585c946e Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Wed, 23 Oct 2019 17:13:12 +0100 Subject: [PATCH 02/25] Update version.schema.json --- src/NerdBank.GitVersioning/version.schema.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/NerdBank.GitVersioning/version.schema.json b/src/NerdBank.GitVersioning/version.schema.json index 258d89fb..6ca3da39 100644 --- a/src/NerdBank.GitVersioning/version.schema.json +++ b/src/NerdBank.GitVersioning/version.schema.json @@ -175,6 +175,14 @@ } }, "additionalProperties": false + }, + "pathFilters": { + "type": "array", + "description": "An array of repository relative paths (folders or files) that are used to filter commits when calculating the version height. A commit will not increment the version height if it does not touch a path listed in this array.", + "items": { + "type": "string" + }, + "uniqueItems": true } } }, From 61f2159e0202326185e9fa967d9246411fddd86b Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Thu, 24 Oct 2019 14:22:19 +0100 Subject: [PATCH 03/25] Add FilterPath --- .../FilterPathTests.cs | 106 +++++++++++++ src/NerdBank.GitVersioning/FilterPath.cs | 146 ++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 src/NerdBank.GitVersioning.Tests/FilterPathTests.cs create mode 100644 src/NerdBank.GitVersioning/FilterPath.cs diff --git a/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs b/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs new file mode 100644 index 00000000..35ba787e --- /dev/null +++ b/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs @@ -0,0 +1,106 @@ +using System; +using System.IO; +using Nerdbank.GitVersioning; +using Xunit; + +namespace NerdBank.GitVersioning.Tests +{ + public class FilterPathTests + { + [Theory] + [InlineData("relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData("./relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":^relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":!relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":!/absolutepath.txt", "foo", "absolutepath.txt")] + [InlineData("../bar/relativepath.txt", "foo", "bar/relativepath.txt")] + [InlineData(":/", "foo", "")] + [InlineData(":/absolutepath.txt", "foo", "absolutepath.txt")] + [InlineData(":/bar/absolutepath.txt", "foo", "bar/absolutepath.txt")] + public void CanBeParsedToRepoRelativePath(string pathSpec, string relativeTo, string expected) + { + Assert.Equal(expected.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), + new FilterPath(pathSpec, relativeTo).RepoRelativePath); + } + + [Theory] + [InlineData(":!.", "foo", "foo")] + [InlineData(":!.", "foo", "foo/")] + [InlineData(":!.", "foo", "foo/relativepath.txt")] + [InlineData(":!relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":^relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":^./relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":^../bar", "foo", "bar")] + [InlineData(":^../bar", "foo", "bar/")] + [InlineData(":^../bar", "foo", "bar/somefile.txt")] + [InlineData(":^/absolute.txt", "foo", "absolute.txt")] + public void PathsCanBeExcluded(string pathSpec, string relativeTo, string repoRelativePath) + { + Assert.True(new FilterPath(pathSpec, relativeTo, true).Excludes( + repoRelativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))); + Assert.True(new FilterPath(pathSpec, relativeTo, false).Excludes( + repoRelativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))); + } + + [Theory] + [InlineData(":!.", "foo", "foo.txt")] + [InlineData(":^relativepath.txt", "foo", "foo2/relativepath.txt")] + [InlineData(":^/absolute.txt", "foo", "absolute.txt.bak")] + [InlineData(":^/absolute.txt", "foo", "absolute")] + + // Not exclude paths + [InlineData(":/absolute.txt", "foo", "absolute.txt")] + [InlineData("/absolute.txt", "foo", "absolute.txt")] + [InlineData("../root.txt", "foo", "root.txt")] + [InlineData("relativepath.txt", "foo", "foo/relativepath.txt")] + public void NonMatchingPathsAreNotExcluded(string pathSpec, string relativeTo, string repoRelativePath) + { + Assert.False(new FilterPath(pathSpec, relativeTo, true).Excludes( + repoRelativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))); + Assert.False(new FilterPath(pathSpec, relativeTo, false).Excludes( + repoRelativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))); + } + + [Theory] + [InlineData(":!.", "foo", "Foo")] + [InlineData(":!.", "foo", "Foo/")] + [InlineData(":!.", "foo", "Foo/relativepath.txt")] + [InlineData(":!RelativePath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":^relativepath.txt", "foo", "Foo/RelativePath.txt")] + [InlineData(":^./relativepath.txt", "Foo", "foo/RelativePath.txt")] + [InlineData(":^../bar", "foo", "Bar")] + [InlineData(":^../bar", "foo", "Bar/")] + [InlineData(":^../bar", "foo", "Bar/SomeFile.txt")] + [InlineData(":^/absOLUte.txt", "foo", "Absolute.TXT")] + public void PathsCanBeExcludedCaseInsensitive(string pathSpec, string relativeTo, string repoRelativePath) + { + Assert.True(new FilterPath(pathSpec, relativeTo, true).Excludes( + repoRelativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))); + } + + [Theory] + [InlineData(":!.", "foo", "Foo")] + [InlineData(":!.", "foo", "Foo/")] + [InlineData(":!.", "foo", "Foo/relativepath.txt")] + [InlineData(":!RelativePath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":^relativepath.txt", "foo", "Foo/RelativePath.txt")] + [InlineData(":^./relativepath.txt", "Foo", "foo/RelativePath.txt")] + [InlineData(":^../bar", "foo", "Bar")] + [InlineData(":^../bar", "foo", "Bar/")] + [InlineData(":^../bar", "foo", "Bar/SomeFile.txt")] + [InlineData(":^/absOLUte.txt", "foo", "Absolute.TXT")] + public void NonMatchingPathsAreNotExcludedCaseSensitive(string pathSpec, string relativeTo, string repoRelativePath) + { + Assert.False(new FilterPath(pathSpec, relativeTo, false).Excludes( + repoRelativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))); + } + + [Fact] + public void InvalidPathspecsThrow() + { + Assert.Throws(() => new FilterPath(null, "")); + Assert.Throws(() => new FilterPath("", "")); + Assert.Throws(() => new FilterPath(":?", "")); + } + } +} \ No newline at end of file diff --git a/src/NerdBank.GitVersioning/FilterPath.cs b/src/NerdBank.GitVersioning/FilterPath.cs new file mode 100644 index 00000000..87793056 --- /dev/null +++ b/src/NerdBank.GitVersioning/FilterPath.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using LibGit2Sharp; +using Validation; + +namespace Nerdbank.GitVersioning +{ + /// + /// A filter (include or exclude) representing a repo relative path. + /// + public class FilterPath + { + private readonly StringComparison stringComparison; + + /// + /// True if this represents an exclude filter. + /// + public bool IsExclude { get; } + + /// + /// Path relative to the repository root that this represents. + /// Slashes are canonical for this OS. + /// + public string RepoRelativePath { get; } + + private static string ParsePath(string path, string relativeTo) + { + // Path is absolute, nothing to do here + if (path[0] == '/') + { + return path.Substring(1); + } + + var combined = relativeTo + '/' + path; + return string.Join(Path.DirectorySeparatorChar.ToString(), + combined + .Split(new[] {Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar}, + StringSplitOptions.RemoveEmptyEntries) + .Aggregate(new Stack(), (parts, segment) => + { + switch (segment) + { + case ".": + return parts; + case "..": + parts.Pop(); + return parts; + default: + parts.Push(segment); + return parts; + } + }) + .Reverse() + ); + } + + public FilterPath(string pathSpec, string relativeTo, Configuration config) : this(pathSpec, relativeTo, + config?.Get("core.ignorecase")?.Value ?? false) + { + } + + /// + /// Construct a from a pathspec-like string and a + /// relative path within the repository. + /// + /// + /// A string that supports some pathspec features. + /// This path is relative to . + /// + /// Examples: + /// - ../relative/inclusion.txt + /// - :/absolute/inclusion.txt + /// - :!relative/exclusion.txt + /// - :^relative/exclusion.txt + /// - :^/absolute/exclusion.txt + /// + /// + /// Path (relative to the root of the repository) that is relative to. + /// + /// Whether case should be ignored by + /// Invalid path spec. + public FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false) + { + Requires.NotNullOrEmpty(pathSpec, nameof(pathSpec)); + Requires.NotNull(relativeTo, nameof(relativeTo)); + + if (pathSpec[0] == ':') + { + if (pathSpec.Length > 1 && (pathSpec[1] == '^' || pathSpec[1] == '!')) + { + this.IsExclude = true; + this.stringComparison = ignoreCase + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; + + this.RepoRelativePath = ParsePath(pathSpec.Substring(2), relativeTo); + } + else if (pathSpec.Length > 1 && pathSpec[1] == '/') + { + this.RepoRelativePath = pathSpec.Substring(2); + } + else + { + throw new FormatException($"Unrecognized path spec '{pathSpec}'"); + } + } + else + { + this.RepoRelativePath = ParsePath(pathSpec, relativeTo); + } + + this.RepoRelativePath = + this.RepoRelativePath + .Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar) + .TrimEnd(Path.DirectorySeparatorChar); + } + + public static IReadOnlyList FromVersionOptions(string relativeRepoProjectDirectory, + IRepository repository, VersionOptions versionOptions) + { + return versionOptions?.PathFilters + ?.Select(pathSpec => new FilterPath(pathSpec, relativeRepoProjectDirectory, + repository?.Config)) + .ToList(); + } + + /// + /// Determines if should be excluded by this . + /// + /// Path (repo relative). Slashes should be canonical for the OS. + /// + /// True if this is an excluding filter that matches + /// , otherwise false. + /// + public bool Excludes(string repoRelativePath) + { + if (!this.IsExclude) return false; + + return this.RepoRelativePath.Equals(repoRelativePath, this.stringComparison) || + repoRelativePath.StartsWith(this.RepoRelativePath + Path.DirectorySeparatorChar, + this.stringComparison); + } + } +} \ No newline at end of file From ac5670ee93333ba6f683edc6a566058c82ca8c38 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Thu, 24 Oct 2019 14:22:28 +0100 Subject: [PATCH 04/25] Update GitExtensions.cs --- src/NerdBank.GitVersioning/GitExtensions.cs | 133 +++++++------------- 1 file changed, 42 insertions(+), 91 deletions(-) diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index 85e2bc87..a1d8453e 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -37,12 +37,13 @@ public static class GitExtensions /// that set the version to the value at . /// /// The commit to measure the height of. - /// The repo-relative project directory for which to calculate the version. /// + /// The repo-relative project directory for which to calculate the version. /// Optional base version to calculate the height. If not specified, the base version will be calculated by scanning the repository. /// The height of the commit. Always a positive integer. public static int GetVersionHeight(this Commit commit, - string repoRelativeProjectDirectory = null, IEnumerable pathFilters = null, + IReadOnlyList pathFilters = null, + string repoRelativeProjectDirectory = null, Version baseVersion = null) { Requires.NotNull(commit, nameof(commit)); @@ -68,56 +69,6 @@ public static class GitExtensions return 0; } - /// - /// Gets the number of commits in the longest single path between - /// HEAD in a repo and the most distant ancestor (inclusive) - /// that set the version to the value in the working copy - /// (or HEAD for bare repositories). - /// - /// The repo with the working copy / HEAD to measure the height of. - /// - /// The repo-relative project directory for which to calculate the version. - /// The height of the repo at HEAD. Always a positive integer. - public static int GetVersionHeight(this Repository repo, IEnumerable pathFilters, - string repoRelativeProjectDirectory = null) - { - if (repo == null) - { - return 0; - } - - VersionOptions workingCopyVersionOptions, committedVersionOptions; - if (IsVersionFileChangedInWorkingCopy(repo, repoRelativeProjectDirectory, out committedVersionOptions, out workingCopyVersionOptions)) - { - Version workingCopyVersion = workingCopyVersionOptions?.Version?.Version; - Version headCommitVersion = committedVersionOptions?.Version?.Version ?? Version0; - if (workingCopyVersion == null || !workingCopyVersion.Equals(headCommitVersion)) - { - // The working copy has changed the major.minor version. - // So by definition the version height is 0, since no commit represents it yet. - return 0; - } - } - - // No special changes in the working directory, so apply regular logic. - return GetVersionHeight(repo.Head, pathFilters, repoRelativeProjectDirectory); - } - - /// - /// Gets the number of commits in the longest single path between - /// the specified commit and the most distant ancestor (inclusive) - /// that set the version to the value at the tip of the . - /// - /// The branch to measure the height of. - /// - /// The repo-relative project directory for which to calculate the version. - /// The height of the branch till the version is changed. - public static int GetVersionHeight(this Branch branch, IEnumerable pathFilters, - string repoRelativeProjectDirectory = null) - { - return GetVersionHeight(branch.Commits.First(), repoRelativeProjectDirectory, pathFilters); - } - /// /// Gets the number of commits in the longest single path between /// the specified commit and the most distant ancestor (inclusive). @@ -130,31 +81,21 @@ public static class GitExtensions /// May be null to count the height to the original commit. /// /// The height of the commit. Always a positive integer. - public static int GetHeight(this Commit commit, IEnumerable pathFilters, + public static int GetHeight(this Commit commit, IReadOnlyList pathFilters, Func continueStepping = null) { Requires.NotNull(commit, nameof(commit)); - var heights = new Dictionary(); - return GetCommitHeight(commit, heights, pathFilters, continueStepping); - } + var includePaths = + pathFilters + .Where(filter => !filter.IsExclude) + .Select(filter => filter.RepoRelativePath) + .ToList(); - /// - /// Gets the number of commits in the longest single path between - /// the specified branch's head and the most distant ancestor (inclusive). - /// - /// The branch to measure the height of. - /// - /// - /// A function that returns false when we reach a commit that - /// should not be included in the height calculation. - /// May be null to count the height to the original commit. - /// - /// The height of the branch. - public static int GetHeight(this Branch branch, IEnumerable pathFilters, - Func continueStepping = null) - { - return GetHeight(branch.Commits.First(), pathFilters, continueStepping); + var excludePaths = pathFilters.Where(filter => filter.IsExclude).ToList(); + + var heights = new Dictionary(); + return GetCommitHeight(commit, heights, includePaths, excludePaths, continueStepping); } /// @@ -215,7 +156,7 @@ public static Commit GetCommitFromTruncatedIdInteger(this Repository repo, int t /// the height of the git commit while the /// component is the first four bytes of the git commit id (forced to be a positive integer). /// - public static Version GetIdAsVersion(this Commit commit, IEnumerable pathFilters, + public static Version GetIdAsVersion(this Commit commit, IReadOnlyList pathFilters, string repoRelativeProjectDirectory = null, int? versionHeight = null) { Requires.NotNull(commit, nameof(commit)); @@ -225,7 +166,7 @@ public static Commit GetCommitFromTruncatedIdInteger(this Repository repo, int t if (!versionHeight.HasValue) { - versionHeight = GetVersionHeight(commit, repoRelativeProjectDirectory, pathFilters); + versionHeight = GetVersionHeight(commit, pathFilters, repoRelativeProjectDirectory); } return GetIdAsVersionHelper(commit, versionOptions, versionHeight.Value); @@ -236,7 +177,6 @@ public static Commit GetCommitFromTruncatedIdInteger(this Repository repo, int t /// so that the original commit can be found later. /// /// The repo whose ID and position in history is to be encoded. - /// /// The repo-relative project directory for which to calculate the version. /// /// The version height, previously calculated by a call to @@ -251,20 +191,27 @@ public static Commit GetCommitFromTruncatedIdInteger(this Repository repo, int t /// the height of the git commit while the /// component is the first four bytes of the git commit id (forced to be a positive integer). /// - public static Version GetIdAsVersion(this Repository repo, IEnumerable pathFilters, + public static Version GetIdAsVersion(this Repository repo, string repoRelativeProjectDirectory = null, int? versionHeight = null) { Requires.NotNull(repo, nameof(repo)); var headCommit = repo.Head.Commits.FirstOrDefault(); VersionOptions workingCopyVersionOptions, committedVersionOptions; - if (IsVersionFileChangedInWorkingCopy(repo, repoRelativeProjectDirectory, out committedVersionOptions, out workingCopyVersionOptions)) + var versionFileChanged = IsVersionFileChangedInWorkingCopy(repo, repoRelativeProjectDirectory, + out committedVersionOptions, out workingCopyVersionOptions); + + // TODO(saul): when will repoRelativeProjectDirectory be null? + var pathFilters = + FilterPath.FromVersionOptions(repoRelativeProjectDirectory, repo, workingCopyVersionOptions); + + if (versionFileChanged) { // Apply ordinary logic, but to the working copy version info. if (!versionHeight.HasValue) { var baseVersion = workingCopyVersionOptions?.Version?.Version; - versionHeight = GetVersionHeight(headCommit, repoRelativeProjectDirectory, pathFilters, baseVersion); + versionHeight = GetVersionHeight(headCommit, pathFilters, repoRelativeProjectDirectory, baseVersion); } Version result = GetIdAsVersionHelper(headCommit, workingCopyVersionOptions, versionHeight.Value); @@ -311,7 +258,7 @@ public static Commit GetCommitFromTruncatedIdInteger(this Repository repo, int t let commitVersionOptions = VersionFile.GetVersion(commit, repoRelativeProjectDirectory) where commitVersionOptions != null where !IsCommitIdMismatch(version, commitVersionOptions, commit) - where !IsVersionHeightMismatch(version, commitVersionOptions, commit, repoRelativeProjectDirectory, commitVersionOptions.PathFilters) + where !IsVersionHeightMismatch(version, commitVersionOptions, commit, repoRelativeProjectDirectory) select commit; return possibleCommits; @@ -544,7 +491,7 @@ private static int ReadVersionPosition(Version version, SemanticVersion.Position } private static bool IsVersionHeightMismatch(Version version, VersionOptions versionOptions, Commit commit, - string repoRelativeProjectDirectory, IEnumerable pathFilters) + string repoRelativeProjectDirectory) { Requires.NotNull(version, nameof(version)); Requires.NotNull(versionOptions, nameof(versionOptions)); @@ -557,6 +504,8 @@ private static int ReadVersionPosition(Version version, SemanticVersion.Position int expectedVersionHeight = ReadVersionPosition(version, position.Value); var actualVersionOffset = versionOptions.VersionHeightOffsetOrDefault; + var pathFilters = FilterPath.FromVersionOptions(repoRelativeProjectDirectory, + ((IBelongToARepository) commit).Repository, versionOptions); var actualVersionHeight = commit.GetHeight(pathFilters, c => CommitMatchesVersion(c, version, position.Value - 1, repoRelativeProjectDirectory)); return expectedVersionHeight != actualVersionHeight + actualVersionOffset; } @@ -671,11 +620,8 @@ private static string EncodeAsHex(byte[] buffer) /// /// The commit to measure the height of. /// A cache of commits and their heights. - /// - /// An array of paths to filter the changes in commits on. If a commit - /// does not affect any paths in the filter, the commit does not - /// contribute to the height. May be null to disable filtering. - /// + /// + /// /// /// A function that returns false when we reach a commit that /// should not be included in the height calculation. @@ -683,19 +629,23 @@ private static string EncodeAsHex(byte[] buffer) /// /// The height of the branch. private static int GetCommitHeight(Commit commit, Dictionary heights, - IEnumerable pathFilters, + IEnumerable includePaths, IReadOnlyList excludePaths, Func continueStepping) { Requires.NotNull(commit, nameof(commit)); Requires.NotNull(heights, nameof(heights)); - int height; - if (!heights.TryGetValue(commit.Id, out height)) + bool ContainsRelevantChanges(IEnumerable changes) => + excludePaths.Count == 0 + ? changes.Any() + : changes.Any(change => excludePaths.Any(exclude => exclude.Excludes(change.Path))); + + if (!heights.TryGetValue(commit.Id, out int height)) { height = 0; if (continueStepping == null || continueStepping(commit)) { - if (pathFilters != null) + if (includePaths != null && excludePaths != null) { var repository = ((IBelongToARepository)commit).Repository; @@ -703,7 +653,8 @@ private static string EncodeAsHex(byte[] buffer) // touches a path that we care about, bump the height. // Otherwise, this commit will not increment the height. if (commit.Parents.Any(parent => - repository.Diff.Compare(parent.Tree, commit.Tree, pathFilters).Any())) + ContainsRelevantChanges( + repository.Diff.Compare(parent.Tree, commit.Tree, includePaths)))) { height = 1; } @@ -715,7 +666,7 @@ private static string EncodeAsHex(byte[] buffer) if (commit.Parents.Any()) { - height += commit.Parents.Max(p => GetCommitHeight(p, heights, pathFilters, continueStepping)); + height += commit.Parents.Max(p => GetCommitHeight(p, heights, includePaths, excludePaths, continueStepping)); } } From b76c16d85a37a4aeb9f7a6100233f74ba5a8e82d Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Thu, 24 Oct 2019 14:23:09 +0100 Subject: [PATCH 05/25] Update VersionOracle.cs --- src/NerdBank.GitVersioning/VersionOracle.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NerdBank.GitVersioning/VersionOracle.cs b/src/NerdBank.GitVersioning/VersionOracle.cs index 9f2a180e..c2fbc60f 100644 --- a/src/NerdBank.GitVersioning/VersionOracle.cs +++ b/src/NerdBank.GitVersioning/VersionOracle.cs @@ -495,7 +495,9 @@ private static int CalculateVersionHeight(string relativeRepoProjectDirectory, L } } - return headCommit?.GetVersionHeight(relativeRepoProjectDirectory, workingVersion?.PathFilters) ?? 0; + return headCommit?.GetVersionHeight(FilterPath.FromVersionOptions(relativeRepoProjectDirectory, + ((LibGit2Sharp.IBelongToARepository) headCommit).Repository, workingVersion), relativeRepoProjectDirectory) + ?? 0; } private static Version GetIdAsVersion(LibGit2Sharp.Commit headCommit, VersionOptions committedVersion, VersionOptions workingVersion, int versionHeight) From 76f27ee306b25327ec47949cd6edc890adf9569e Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Thu, 24 Oct 2019 15:03:09 +0100 Subject: [PATCH 06/25] Fix some tests --- .../BuildIntegrationTests.cs | 12 +-- .../VersionFileTests.cs | 10 +-- .../VersionOracleTests.cs | 6 +- src/NerdBank.GitVersioning/FilterPath.cs | 11 +-- src/NerdBank.GitVersioning/GitExtensions.cs | 75 +++++++++++++++---- src/NerdBank.GitVersioning/VersionOracle.cs | 4 +- 6 files changed, 80 insertions(+), 38 deletions(-) diff --git a/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs b/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs index 70e7d4a2..a3268df9 100644 --- a/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs +++ b/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs @@ -183,7 +183,7 @@ public async Task GetBuildVersion_In_Git_But_Head_Lacks_VersionFile() this.WriteVersionFile("3.4"); Assumes.True(repo.Index[VersionFile.JsonFileName] == null); var buildResult = await this.BuildAsync(); - Assert.Equal("3.4.0." + repo.Head.Commits.First().GetIdAsVersion(TODO).Revision, buildResult.BuildVersion); + Assert.Equal("3.4.0." + repo.Head.Commits.First().GetIdAsVersion().Revision, buildResult.BuildVersion); Assert.Equal("3.4.0+" + repo.Head.Commits.First().Id.Sha.Substring(0, VersionOptions.DefaultGitCommitIdShortFixedLength), buildResult.AssemblyInformationalVersion); } @@ -208,7 +208,7 @@ public async Task GetBuildVersion_In_Git_No_VersionFile_At_All() var repo = new Repository(this.RepoPath); // do not assign Repo property to avoid commits being generated later repo.Commit("empty", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); var buildResult = await this.BuildAsync(); - Assert.Equal("0.0.0." + repo.Head.Commits.First().GetIdAsVersion(TODO).Revision, buildResult.BuildVersion); + Assert.Equal("0.0.0." + repo.Head.Commits.First().GetIdAsVersion().Revision, buildResult.BuildVersion); Assert.Equal("0.0.0+" + repo.Head.Commits.First().Id.Sha.Substring(0, VersionOptions.DefaultGitCommitIdShortFixedLength), buildResult.AssemblyInformationalVersion); } @@ -288,7 +288,7 @@ public async Task GetBuildVersion_StableRelease() var buildResult = await this.BuildAsync(); this.AssertStandardProperties(VersionOptions.FromVersion(new Version(majorMinorVersion)), buildResult); - Version version = this.Repo.Head.Commits.First().GetIdAsVersion(TODO); + Version version = this.Repo.Head.Commits.First().GetIdAsVersion(); Assert.Equal($"{version.Major}.{version.Minor}.{buildResult.GitVersionHeight}", buildResult.NuGetPackageVersion); } @@ -976,10 +976,10 @@ private static RestoreEnvironmentVariables ApplyEnvironmentVariables(IReadOnlyDi private void AssertStandardProperties(VersionOptions versionOptions, BuildResults buildResult, string relativeProjectDirectory = null) { - int versionHeight = this.Repo.GetVersionHeight(TODO, relativeProjectDirectory); - Version idAsVersion = this.Repo.GetIdAsVersion(TODO, relativeProjectDirectory); + int versionHeight = this.Repo.GetVersionHeight(relativeProjectDirectory); + Version idAsVersion = this.Repo.GetIdAsVersion(relativeProjectDirectory); string commitIdShort = this.CommitIdShort; - Version version = this.Repo.GetIdAsVersion(TODO, relativeProjectDirectory); + Version version = this.Repo.GetIdAsVersion(relativeProjectDirectory); Version assemblyVersion = GetExpectedAssemblyVersion(versionOptions, version); var additionalBuildMetadata = from item in buildResult.BuildResult.ProjectStateAfterBuild.GetItems("BuildMetadata") select item.EvaluatedInclude; diff --git a/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs b/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs index 8508c044..7e959d04 100644 --- a/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs +++ b/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs @@ -440,13 +440,13 @@ public void VersionJson_Inheritance(bool commitInSourceControl, bool bareRepo) // The version height should be the same for all those that inherit the version from the base, // even though the inheriting files were introduced in successive commits. - Assert.Equal(totalCommits, operatingRepo.GetVersionHeight(TODO)); - Assert.Equal(totalCommits, operatingRepo.GetVersionHeight(TODO, "foo")); - Assert.Equal(totalCommits, operatingRepo.GetVersionHeight(TODO, @"foo\bar")); + Assert.Equal(totalCommits, operatingRepo.GetVersionHeight()); + Assert.Equal(totalCommits, operatingRepo.GetVersionHeight("foo")); + Assert.Equal(totalCommits, operatingRepo.GetVersionHeight(@"foo\bar")); // These either don't inherit, or inherit but reset versions, so the commits were reset. - Assert.Equal(2, operatingRepo.GetVersionHeight(TODO, "noInherit")); - Assert.Equal(1, operatingRepo.GetVersionHeight(TODO, "inheritWithVersion")); + Assert.Equal(2, operatingRepo.GetVersionHeight("noInherit")); + Assert.Equal(1, operatingRepo.GetVersionHeight("inheritWithVersion")); } } } diff --git a/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs b/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs index d159d95d..2e567108 100644 --- a/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs +++ b/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs @@ -101,18 +101,18 @@ public void VersionHeightResetsWithVersionSpecChanges(string initial, string nex var oracle = VersionOracle.Create(this.RepoPath); Assert.Equal(11, oracle.VersionHeight); - Assert.Equal(11, this.Repo.Head.GetVersionHeight(TODO)); + Assert.Equal(11, this.Repo.Head.GetVersionHeight()); options.Version = SemanticVersion.Parse(next); this.WriteVersionFile(options); oracle = VersionOracle.Create(this.RepoPath); Assert.Equal(1, oracle.VersionHeight); - Assert.Equal(1, this.Repo.Head.GetVersionHeight(TODO)); + Assert.Equal(1, this.Repo.Head.GetVersionHeight()); foreach (var commit in this.Repo.Head.Commits) { - var versionFromId = commit.GetIdAsVersion(TODO); + var versionFromId = commit.GetIdAsVersion(); Assert.Contains(commit, this.Repo.GetCommitsFromVersion(versionFromId)); } } diff --git a/src/NerdBank.GitVersioning/FilterPath.cs b/src/NerdBank.GitVersioning/FilterPath.cs index 87793056..32ef76ba 100644 --- a/src/NerdBank.GitVersioning/FilterPath.cs +++ b/src/NerdBank.GitVersioning/FilterPath.cs @@ -33,7 +33,7 @@ private static string ParsePath(string path, string relativeTo) return path.Substring(1); } - var combined = relativeTo + '/' + path; + var combined = relativeTo == null ? path : relativeTo + '/' + path; return string.Join(Path.DirectorySeparatorChar.ToString(), combined .Split(new[] {Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar}, @@ -84,7 +84,6 @@ private static string ParsePath(string path, string relativeTo) public FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false) { Requires.NotNullOrEmpty(pathSpec, nameof(pathSpec)); - Requires.NotNull(relativeTo, nameof(relativeTo)); if (pathSpec[0] == ':') { @@ -117,10 +116,12 @@ public FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false) .TrimEnd(Path.DirectorySeparatorChar); } - public static IReadOnlyList FromVersionOptions(string relativeRepoProjectDirectory, - IRepository repository, VersionOptions versionOptions) + public static IReadOnlyList FromVersionOptions(VersionOptions versionOptions, + string relativeRepoProjectDirectory, + IRepository repository) { - return versionOptions?.PathFilters + Requires.NotNull(versionOptions, nameof(versionOptions)); + return versionOptions.PathFilters ?.Select(pathSpec => new FilterPath(pathSpec, relativeRepoProjectDirectory, repository?.Config)) .ToList(); diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index a1d8453e..19e6c1b4 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -42,7 +42,6 @@ public static class GitExtensions /// Optional base version to calculate the height. If not specified, the base version will be calculated by scanning the repository. /// The height of the commit. Always a positive integer. public static int GetVersionHeight(this Commit commit, - IReadOnlyList pathFilters = null, string repoRelativeProjectDirectory = null, Version baseVersion = null) { @@ -62,6 +61,7 @@ public static class GitExtensions var versionHeightPosition = versionOptions.VersionHeightPosition; if (versionHeightPosition.HasValue) { + var pathFilters = FilterPath.FromVersionOptions(versionOptions, repoRelativeProjectDirectory, commit.GetRepository()); int height = commit.GetHeight(pathFilters, c => CommitMatchesVersion(c, baseSemVer, versionHeightPosition.Value, repoRelativeProjectDirectory)); return height; } @@ -69,6 +69,52 @@ public static class GitExtensions return 0; } + /// + /// Gets the number of commits in the longest single path between + /// HEAD in a repo and the most distant ancestor (inclusive) + /// that set the version to the value in the working copy + /// (or HEAD for bare repositories). + /// + /// The repo with the working copy / HEAD to measure the height of. + /// The repo-relative project directory for which to calculate the version. + /// The height of the repo at HEAD. Always a positive integer. + public static int GetVersionHeight(this Repository repo, string repoRelativeProjectDirectory = null) + { + if (repo == null) + { + return 0; + } + + VersionOptions workingCopyVersionOptions, committedVersionOptions; + if (IsVersionFileChangedInWorkingCopy(repo, repoRelativeProjectDirectory, out committedVersionOptions, out workingCopyVersionOptions)) + { + Version workingCopyVersion = workingCopyVersionOptions?.Version?.Version; + Version headCommitVersion = committedVersionOptions?.Version?.Version ?? Version0; + if (workingCopyVersion == null || !workingCopyVersion.Equals(headCommitVersion)) + { + // The working copy has changed the major.minor version. + // So by definition the version height is 0, since no commit represents it yet. + return 0; + } + } + + // No special changes in the working directory, so apply regular logic. + return GetVersionHeight(repo.Head, repoRelativeProjectDirectory); + } + + /// + /// Gets the number of commits in the longest single path between + /// the specified commit and the most distant ancestor (inclusive) + /// that set the version to the value at the tip of the . + /// + /// The branch to measure the height of. + /// The repo-relative project directory for which to calculate the version. + /// The height of the branch till the version is changed. + public static int GetVersionHeight(this Branch branch, string repoRelativeProjectDirectory = null) + { + return branch.Commits.First().GetVersionHeight(repoRelativeProjectDirectory); + } + /// /// Gets the number of commits in the longest single path between /// the specified commit and the most distant ancestor (inclusive). @@ -136,6 +182,11 @@ public static Commit GetCommitFromTruncatedIdInteger(this Repository repo, int t return repo.Lookup(EncodeAsHex(rawId)); } + public static IRepository GetRepository(this Commit commit) + { + return ((IBelongToARepository) commit).Repository; + } + /// /// Encodes a commit from history in a /// so that the original commit can be found later. @@ -156,8 +207,7 @@ public static Commit GetCommitFromTruncatedIdInteger(this Repository repo, int t /// the height of the git commit while the /// component is the first four bytes of the git commit id (forced to be a positive integer). /// - public static Version GetIdAsVersion(this Commit commit, IReadOnlyList pathFilters, - string repoRelativeProjectDirectory = null, int? versionHeight = null) + public static Version GetIdAsVersion(this Commit commit, string repoRelativeProjectDirectory = null, int? versionHeight = null) { Requires.NotNull(commit, nameof(commit)); Requires.Argument(repoRelativeProjectDirectory == null || !Path.IsPathRooted(repoRelativeProjectDirectory), nameof(repoRelativeProjectDirectory), "Path should be relative to repo root."); @@ -166,7 +216,7 @@ public static Commit GetCommitFromTruncatedIdInteger(this Repository repo, int t if (!versionHeight.HasValue) { - versionHeight = GetVersionHeight(commit, pathFilters, repoRelativeProjectDirectory); + versionHeight = GetVersionHeight(commit, repoRelativeProjectDirectory); } return GetIdAsVersionHelper(commit, versionOptions, versionHeight.Value); @@ -198,27 +248,21 @@ public static Commit GetCommitFromTruncatedIdInteger(this Repository repo, int t var headCommit = repo.Head.Commits.FirstOrDefault(); VersionOptions workingCopyVersionOptions, committedVersionOptions; - var versionFileChanged = IsVersionFileChangedInWorkingCopy(repo, repoRelativeProjectDirectory, - out committedVersionOptions, out workingCopyVersionOptions); - - // TODO(saul): when will repoRelativeProjectDirectory be null? - var pathFilters = - FilterPath.FromVersionOptions(repoRelativeProjectDirectory, repo, workingCopyVersionOptions); - - if (versionFileChanged) + if (IsVersionFileChangedInWorkingCopy(repo, repoRelativeProjectDirectory, + out committedVersionOptions, out workingCopyVersionOptions)) { // Apply ordinary logic, but to the working copy version info. if (!versionHeight.HasValue) { var baseVersion = workingCopyVersionOptions?.Version?.Version; - versionHeight = GetVersionHeight(headCommit, pathFilters, repoRelativeProjectDirectory, baseVersion); + versionHeight = GetVersionHeight(headCommit, repoRelativeProjectDirectory, baseVersion); } Version result = GetIdAsVersionHelper(headCommit, workingCopyVersionOptions, versionHeight.Value); return result; } - return GetIdAsVersion(headCommit, pathFilters, repoRelativeProjectDirectory); + return GetIdAsVersion(headCommit, repoRelativeProjectDirectory); } /// @@ -504,8 +548,7 @@ private static int ReadVersionPosition(Version version, SemanticVersion.Position int expectedVersionHeight = ReadVersionPosition(version, position.Value); var actualVersionOffset = versionOptions.VersionHeightOffsetOrDefault; - var pathFilters = FilterPath.FromVersionOptions(repoRelativeProjectDirectory, - ((IBelongToARepository) commit).Repository, versionOptions); + var pathFilters = FilterPath.FromVersionOptions(versionOptions, repoRelativeProjectDirectory, commit.GetRepository()); var actualVersionHeight = commit.GetHeight(pathFilters, c => CommitMatchesVersion(c, version, position.Value - 1, repoRelativeProjectDirectory)); return expectedVersionHeight != actualVersionHeight + actualVersionOffset; } diff --git a/src/NerdBank.GitVersioning/VersionOracle.cs b/src/NerdBank.GitVersioning/VersionOracle.cs index c2fbc60f..a4bb2a75 100644 --- a/src/NerdBank.GitVersioning/VersionOracle.cs +++ b/src/NerdBank.GitVersioning/VersionOracle.cs @@ -495,9 +495,7 @@ private static int CalculateVersionHeight(string relativeRepoProjectDirectory, L } } - return headCommit?.GetVersionHeight(FilterPath.FromVersionOptions(relativeRepoProjectDirectory, - ((LibGit2Sharp.IBelongToARepository) headCommit).Repository, workingVersion), relativeRepoProjectDirectory) - ?? 0; + return headCommit?.GetVersionHeight(relativeRepoProjectDirectory) ?? 0; } private static Version GetIdAsVersion(LibGit2Sharp.Commit headCommit, VersionOptions committedVersion, VersionOptions workingVersion, int versionHeight) From 9b6db88a3db2106d12da353238597ec65a41a6e2 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Thu, 24 Oct 2019 15:27:09 +0100 Subject: [PATCH 07/25] Fix tests --- .../GitExtensionsTests.cs | 98 ++++++++++--------- src/NerdBank.GitVersioning/GitExtensions.cs | 25 ++++- 2 files changed, 71 insertions(+), 52 deletions(-) diff --git a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs index 0dea26cd..31ddbcd0 100644 --- a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs +++ b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs @@ -13,6 +13,8 @@ public class GitExtensionsTests : RepoTestBase { + private readonly IReadOnlyList emptyFilterPath; + public GitExtensionsTests(ITestOutputHelper Logger) : base(Logger) { @@ -23,8 +25,8 @@ public GitExtensionsTests(ITestOutputHelper Logger) public void GetHeight_EmptyRepo() { Branch head = this.Repo.Head; - Assert.Throws(() => head.GetHeight(TODO)); - Assert.Throws(() => head.GetHeight(TODO, c => true)); + Assert.Throws(() => head.GetHeight(this.emptyFilterPath)); + Assert.Throws(() => head.GetHeight(this.emptyFilterPath, c => true)); } [Fact] @@ -33,11 +35,11 @@ public void GetHeight_SinglePath() var first = this.Repo.Commit("First", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); var second = this.Repo.Commit("Second", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); var third = this.Repo.Commit("Third", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - Assert.Equal(3, this.Repo.Head.GetHeight(TODO)); - Assert.Equal(3, this.Repo.Head.GetHeight(TODO, c => true)); + Assert.Equal(3, this.Repo.Head.GetHeight(this.emptyFilterPath)); + Assert.Equal(3, this.Repo.Head.GetHeight(this.emptyFilterPath, c => true)); - Assert.Equal(2, this.Repo.Head.GetHeight(TODO, c => c != first)); - Assert.Equal(1, this.Repo.Head.GetHeight(TODO, c => c != second)); + Assert.Equal(2, this.Repo.Head.GetHeight(this.emptyFilterPath, c => c != first)); + Assert.Equal(1, this.Repo.Head.GetHeight(this.emptyFilterPath, c => c != second)); } [Fact] @@ -56,13 +58,13 @@ public void GetHeight_Merge() this.Repo.Merge(secondCommit, new Signature("t", "t@t.com", DateTimeOffset.Now), new MergeOptions { FastForwardStrategy = FastForwardStrategy.NoFastForward }); // While we've created 8 commits, the tallest height is only 7. - Assert.Equal(7, this.Repo.Head.GetHeight(TODO)); + Assert.Equal(7, this.Repo.Head.GetHeight(this.emptyFilterPath)); // Now stop enumerating early on just one branch of the ancestry -- the number should remain high. - Assert.Equal(7, this.Repo.Head.GetHeight(TODO, c => c != secondCommit)); + Assert.Equal(7, this.Repo.Head.GetHeight(this.emptyFilterPath, c => c != secondCommit)); // This time stop in both branches of history, and verify that we count the taller one. - Assert.Equal(3, this.Repo.Head.GetHeight(TODO, c => c != secondCommit && c != branchCommits[2])); + Assert.Equal(3, this.Repo.Head.GetHeight(this.emptyFilterPath, c => c != secondCommit && c != branchCommits[2])); } [Fact] @@ -72,7 +74,7 @@ public void GetVersionHeight() var second = this.Repo.Commit("Second", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); this.WriteVersionFile(); var third = this.Repo.Commit("Third", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - Assert.Equal(2, this.Repo.Head.GetVersionHeight(TODO)); + Assert.Equal(2, this.Repo.Head.GetVersionHeight()); } [Fact] @@ -81,44 +83,44 @@ public void GetVersionHeight_VersionJsonHasUnrelatedHistory() // Emulate a repo that used version.json for something else. string versionJsonPath = Path.Combine(this.RepoPath, "version.json"); File.WriteAllText(versionJsonPath, @"{ ""unrelated"": false }"); - Assert.Equal(0, this.Repo.GetVersionHeight(TODO)); // exercise code that handles the file not yet checked in. + Assert.Equal(0, this.Repo.GetVersionHeight()); // exercise code that handles the file not yet checked in. Commands.Stage(this.Repo, versionJsonPath); this.Repo.Commit("Add unrelated version.json file.", this.Signer, this.Signer); - Assert.Equal(0, this.Repo.GetVersionHeight(TODO)); // exercise code that handles a checked in file. + Assert.Equal(0, this.Repo.GetVersionHeight()); // exercise code that handles a checked in file. // And now the repo has decided to use this package. this.WriteVersionFile(); - Assert.Equal(1, this.Repo.Head.GetVersionHeight(TODO)); - Assert.Equal(1, this.Repo.GetVersionHeight(TODO)); + Assert.Equal(1, this.Repo.Head.GetVersionHeight()); + Assert.Equal(1, this.Repo.GetVersionHeight()); // Also emulate case of where the related version.json was just changed to conform, // but not yet checked in. this.Repo.Reset(ResetMode.Mixed, this.Repo.Head.Tip.Parents.Single()); - Assert.Equal(0, this.Repo.GetVersionHeight(TODO)); + Assert.Equal(0, this.Repo.GetVersionHeight()); } [Fact] public void GetVersionHeight_VersionJsonHasParsingErrorsInHistory() { this.WriteVersionFile(); - Assert.Equal(1, this.Repo.GetVersionHeight(TODO)); + Assert.Equal(1, this.Repo.GetVersionHeight()); // Now introduce a parsing error. string versionJsonPath = Path.Combine(this.RepoPath, "version.json"); File.WriteAllText(versionJsonPath, @"{ ""version"": ""1.0"""); // no closing curly brace for parsing error - Assert.Equal(0, this.Repo.GetVersionHeight(TODO)); + Assert.Equal(0, this.Repo.GetVersionHeight()); Commands.Stage(this.Repo, versionJsonPath); this.Repo.Commit("Add broken version.json file.", this.Signer, this.Signer); - Assert.Equal(0, this.Repo.GetVersionHeight(TODO)); + Assert.Equal(0, this.Repo.GetVersionHeight()); // Now fix it. this.WriteVersionFile(); - Assert.Equal(1, this.Repo.GetVersionHeight(TODO)); + Assert.Equal(1, this.Repo.GetVersionHeight()); // And emulate fixing it without having checked in yet. this.Repo.Reset(ResetMode.Mixed, this.Repo.Head.Tip.Parents.Single()); - Assert.Equal(0, this.Repo.GetVersionHeight(TODO)); + Assert.Equal(0, this.Repo.GetVersionHeight()); } [Theory] @@ -145,8 +147,8 @@ public void GetVersionHeight_ProgressAndReset(string version1, string version2, new VersionOptions { Version = semanticVersion2 }, repoRelativeSubDirectory); - int height2 = this.Repo.Head.GetVersionHeight(TODO, repoRelativeSubDirectory); - int height1 = this.Repo.Head.Commits.Skip(1).First().GetVersionHeight(repoRelativeSubDirectory, TODO); + int height2 = this.Repo.Head.GetVersionHeight(repoRelativeSubDirectory); + int height1 = this.Repo.Head.Commits.Skip(1).First().GetVersionHeight(repoRelativeSubDirectory); this.Logger.WriteLine("Height 1: {0}", height1); this.Logger.WriteLine("Height 2: {0}", height2); @@ -176,7 +178,7 @@ public void GetIdAsVersion_ReadsMajorMinorFromVersionTxt() this.WriteVersionFile("4.8"); var firstCommit = this.Repo.Commits.First(); - Version v1 = firstCommit.GetIdAsVersion(TODO); + Version v1 = firstCommit.GetIdAsVersion(); Assert.Equal(4, v1.Major); Assert.Equal(8, v1.Minor); } @@ -187,7 +189,7 @@ public void GetIdAsVersion_ReadsMajorMinorFromVersionTxtInSubdirectory() this.WriteVersionFile("4.8", relativeDirectory: @"foo\bar"); var firstCommit = this.Repo.Commits.First(); - Version v1 = firstCommit.GetIdAsVersion(TODO, @"foo\bar"); + Version v1 = firstCommit.GetIdAsVersion(@"foo\bar"); Assert.Equal(4, v1.Major); Assert.Equal(8, v1.Minor); } @@ -198,7 +200,7 @@ public void GetIdAsVersion_MissingVersionTxt() this.AddCommits(); var firstCommit = this.Repo.Commits.First(); - Version v1 = firstCommit.GetIdAsVersion(TODO); + Version v1 = firstCommit.GetIdAsVersion(); Assert.Equal(0, v1.Major); Assert.Equal(0, v1.Minor); } @@ -210,7 +212,7 @@ public void GetIdAsVersion_VersionFileNeverCheckedIn_3Ints() var expectedVersion = new Version(1, 1, 0); var unstagedVersionData = VersionOptions.FromVersion(expectedVersion); string versionFilePath = VersionFile.SetVersion(this.RepoPath, unstagedVersionData); - Version actualVersion = this.Repo.GetIdAsVersion(TODO); + Version actualVersion = this.Repo.GetIdAsVersion(); Assert.Equal(expectedVersion.Major, actualVersion.Major); Assert.Equal(expectedVersion.Minor, actualVersion.Minor); Assert.Equal(expectedVersion.Build, actualVersion.Build); @@ -227,7 +229,7 @@ public void GetIdAsVersion_VersionFileNeverCheckedIn_2Ints() var expectedVersion = new Version(1, 1); var unstagedVersionData = VersionOptions.FromVersion(expectedVersion); string versionFilePath = VersionFile.SetVersion(this.RepoPath, unstagedVersionData); - Version actualVersion = this.Repo.GetIdAsVersion(TODO); + Version actualVersion = this.Repo.GetIdAsVersion(); Assert.Equal(expectedVersion.Major, actualVersion.Major); Assert.Equal(expectedVersion.Minor, actualVersion.Minor); Assert.Equal(0, actualVersion.Build); // height is 0 since the change hasn't been committed. @@ -242,7 +244,7 @@ public void GetIdAsVersion_VersionFileChangedOnDisk() this.AddCommits(); // Verify that we're seeing the original version. - Version actualVersion = this.Repo.GetIdAsVersion(TODO); + Version actualVersion = this.Repo.GetIdAsVersion(); Assert.Equal(1, actualVersion.Major); Assert.Equal(2, actualVersion.Minor); Assert.Equal(2, actualVersion.Build); @@ -252,7 +254,7 @@ public void GetIdAsVersion_VersionFileChangedOnDisk() string versionFile = VersionFile.SetVersion(this.RepoPath, new Version("1.3")); // Verify that HEAD reports whatever is on disk at the time. - actualVersion = this.Repo.GetIdAsVersion(TODO); + actualVersion = this.Repo.GetIdAsVersion(); Assert.Equal(1, actualVersion.Major); Assert.Equal(3, actualVersion.Minor); Assert.Equal(0, actualVersion.Build); @@ -260,7 +262,7 @@ public void GetIdAsVersion_VersionFileChangedOnDisk() // Now commit it and verify the height advances 0->1 this.CommitVersionFile(versionFile, "1.3"); - actualVersion = this.Repo.GetIdAsVersion(TODO); + actualVersion = this.Repo.GetIdAsVersion(); Assert.Equal(1, actualVersion.Major); Assert.Equal(3, actualVersion.Minor); Assert.Equal(1, actualVersion.Build); @@ -305,13 +307,13 @@ public void GetIdAsVersion_Roundtrip(string version, string assemblyVersion, int for (int i = 0; i < commits.Length; i++) { commits[i] = this.Repo.Commit($"Commit {i + 1}", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - versions[i] = commits[i].GetIdAsVersion(TODO, repoRelativeSubDirectory); + versions[i] = commits[i].GetIdAsVersion(repoRelativeSubDirectory); this.Logger.WriteLine($"Commit {commits[i].Id.Sha.Substring(0, 8)} as version: {versions[i]}"); } for (int i = 0; i < commits.Length; i++) { - Assert.Equal(commits[i], this.Repo.GetCommitFromVersion(versions[i], TODO, repoRelativeSubDirectory)); + Assert.Equal(commits[i], this.Repo.GetCommitFromVersion(versions[i], repoRelativeSubDirectory)); // Also verify that we can find it without the revision number. // This is important because stable, publicly released NuGet packages @@ -319,7 +321,7 @@ public void GetIdAsVersion_Roundtrip(string version, string assemblyVersion, int // But folks who specify a.b.c version numbers don't have any unique version component for the commit at all without the 4th integer. if (semanticVersion.Version.Build == -1) { - Assert.Equal(commits[i], this.Repo.GetCommitFromVersion(new Version(versions[i].Major, versions[i].Minor, versions[i].Build), TODO, repoRelativeSubDirectory)); + Assert.Equal(commits[i], this.Repo.GetCommitFromVersion(new Version(versions[i].Major, versions[i].Minor, versions[i].Build), repoRelativeSubDirectory)); } } } @@ -344,10 +346,10 @@ public void GetIdAsVersion_Roundtrip_UnstableOffset(int startingOffset, int offs { versionOptions.VersionHeightOffset += offsetStepChange; commits[i] = this.WriteVersionFile(versionOptions); - versions[i] = commits[i].GetIdAsVersion(TODO); + versions[i] = commits[i].GetIdAsVersion(); commits[i + 1] = this.Repo.Commit($"Commit {i + 1}", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - versions[i + 1] = commits[i + 1].GetIdAsVersion(TODO); + versions[i + 1] = commits[i + 1].GetIdAsVersion(); this.Logger.WriteLine($"Commit {commits[i].Id.Sha.Substring(0, 8)} as version: {versions[i]}"); this.Logger.WriteLine($"Commit {commits[i + 1].Id.Sha.Substring(0, 8)} as version: {versions[i + 1]}"); @@ -386,20 +388,20 @@ public void GetIdAsVersion_Roundtrip_WithSubdirectoryVersionFiles() this.InitializeSourceControl(); Commit head = this.Repo.Head.Commits.First(); - Version rootVersionActual = head.GetIdAsVersion(TODO); - Version subPathVersionActual = head.GetIdAsVersion(TODO, subPathRelative); + Version rootVersionActual = head.GetIdAsVersion(); + Version subPathVersionActual = head.GetIdAsVersion(subPathRelative); // Verify that the versions calculated took the path into account. Assert.Equal(rootVersionExpected.Version.Version.Minor, rootVersionActual?.Minor); Assert.Equal(subPathVersionExpected.Version.Version.Minor, subPathVersionActual?.Minor); // Verify that we can find the commit given the version and path. - Assert.Equal(head, this.Repo.GetCommitFromVersion(rootVersionActual, TODO)); - Assert.Equal(head, this.Repo.GetCommitFromVersion(subPathVersionActual, TODO, subPathRelative)); + Assert.Equal(head, this.Repo.GetCommitFromVersion(rootVersionActual)); + Assert.Equal(head, this.Repo.GetCommitFromVersion(subPathVersionActual, subPathRelative)); // Verify that mismatching path and version results in a null value. - Assert.Null(this.Repo.GetCommitFromVersion(rootVersionActual, TODO, subPathRelative)); - Assert.Null(this.Repo.GetCommitFromVersion(subPathVersionActual, TODO)); + Assert.Null(this.Repo.GetCommitFromVersion(rootVersionActual, subPathRelative)); + Assert.Null(this.Repo.GetCommitFromVersion(subPathVersionActual)); } [Fact] @@ -408,7 +410,7 @@ public void GetIdAsVersion_FitsInsideCompilerConstraints() this.WriteVersionFile("2.5"); var firstCommit = this.Repo.Commits.First(); - Version version = firstCommit.GetIdAsVersion(TODO); + Version version = firstCommit.GetIdAsVersion(); this.Logger.WriteLine(version.ToString()); // The C# compiler produces a build warning and truncates the version number if it exceeds 0xfffe, @@ -427,12 +429,12 @@ public void GetIdAsVersion_MigrationFromVersionTxtToJson() var jsonCommit = this.WriteVersionFile("4.8"); Assert.True(File.Exists(Path.Combine(this.RepoPath, "version.json"))); - Version v1 = txtCommit.GetIdAsVersion(TODO); + Version v1 = txtCommit.GetIdAsVersion(); Assert.Equal(4, v1.Major); Assert.Equal(8, v1.Minor); Assert.Equal(1, v1.Build); - Version v2 = jsonCommit.GetIdAsVersion(TODO); + Version v2 = jsonCommit.GetIdAsVersion(); Assert.Equal(4, v2.Major); Assert.Equal(8, v2.Minor); Assert.Equal(2, v2.Build); @@ -448,9 +450,9 @@ public void TestBiggerRepo() { foreach (var commit in this.Repo.Head.Commits) { - var version = commit.GetIdAsVersion(TODO); + var version = commit.GetIdAsVersion(); this.Logger.WriteLine($"commit {commit.Id} got version {version}"); - var backAgain = this.Repo.GetCommitFromVersion(version, TODO); + var backAgain = this.Repo.GetCommitFromVersion(version); Assert.Equal(commit, backAgain); } } @@ -475,9 +477,9 @@ private void VerifyCommitsWithVersion(Commit[] commits) for (int i = 0; i < commits.Length; i++) { - Version encodedVersion = commits[i].GetIdAsVersion(TODO); + Version encodedVersion = commits[i].GetIdAsVersion(); Assert.Equal(i + 1, encodedVersion.Build); - Assert.Equal(commits[i], this.Repo.GetCommitFromVersion(encodedVersion, TODO)); + Assert.Equal(commits[i], this.Repo.GetCommitFromVersion(encodedVersion)); } } } diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index 19e6c1b4..296eb13a 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -134,16 +134,34 @@ public static int GetVersionHeight(this Branch branch, string repoRelativeProjec var includePaths = pathFilters - .Where(filter => !filter.IsExclude) + ?.Where(filter => !filter.IsExclude) .Select(filter => filter.RepoRelativePath) .ToList(); - var excludePaths = pathFilters.Where(filter => filter.IsExclude).ToList(); + var excludePaths = pathFilters?.Where(filter => filter.IsExclude).ToList(); var heights = new Dictionary(); return GetCommitHeight(commit, heights, includePaths, excludePaths, continueStepping); } + /// + /// Gets the number of commits in the longest single path between + /// the specified branch's head and the most distant ancestor (inclusive). + /// + /// The branch to measure the height of. + /// + /// + /// A function that returns false when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. + /// + /// The height of the branch. + public static int GetHeight(this Branch branch, IReadOnlyList pathFilters, + Func continueStepping = null) + { + return GetHeight(branch.Commits.First(), pathFilters, continueStepping); + } + /// /// Takes the first 4 bytes of a commit ID (i.e. first 8 characters of its hex-encoded SHA) /// and returns them as an integer. @@ -270,7 +288,6 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj /// /// The repository to search for a matching commit. /// The version previously obtained from . - /// /// /// The repo-relative project directory from which was originally calculated. /// @@ -278,7 +295,7 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj /// /// Thrown in the very rare situation that more than one matching commit is found. /// - public static Commit GetCommitFromVersion(this Repository repo, Version version, IEnumerable pathFilters, + public static Commit GetCommitFromVersion(this Repository repo, Version version, string repoRelativeProjectDirectory = null) { // Note we'll accept no match, or one match. But we throw if there is more than one match. From 5bc44e513f8185b5709ece45f170e59696797a8c Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Thu, 24 Oct 2019 15:29:53 +0100 Subject: [PATCH 08/25] Fix some doc warnings --- src/NerdBank.GitVersioning/FilterPath.cs | 2 +- src/NerdBank.GitVersioning/GitExtensions.cs | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/NerdBank.GitVersioning/FilterPath.cs b/src/NerdBank.GitVersioning/FilterPath.cs index 32ef76ba..819ab603 100644 --- a/src/NerdBank.GitVersioning/FilterPath.cs +++ b/src/NerdBank.GitVersioning/FilterPath.cs @@ -77,7 +77,7 @@ private static string ParsePath(string path, string relativeTo) /// - :^/absolute/exclusion.txt /// /// - /// Path (relative to the root of the repository) that is relative to. + /// Path (relative to the root of the repository) that is relative to. /// /// Whether case should be ignored by /// Invalid path spec. diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index 296eb13a..63fce2df 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -37,7 +37,6 @@ public static class GitExtensions /// that set the version to the value at . /// /// The commit to measure the height of. - /// /// The repo-relative project directory for which to calculate the version. /// Optional base version to calculate the height. If not specified, the base version will be calculated by scanning the repository. /// The height of the commit. Always a positive integer. @@ -200,9 +199,14 @@ public static Commit GetCommitFromTruncatedIdInteger(this Repository repo, int t return repo.Lookup(EncodeAsHex(rawId)); } - public static IRepository GetRepository(this Commit commit) + /// + /// Returns the repository that belongs to. + /// + /// Member of the repository. + /// Repository that belongs to. + private static IRepository GetRepository(this IBelongToARepository repositoryMember) { - return ((IBelongToARepository) commit).Repository; + return repositoryMember.Repository; } /// @@ -210,10 +214,9 @@ public static IRepository GetRepository(this Commit commit) /// so that the original commit can be found later. /// /// The commit whose ID and position in history is to be encoded. - /// /// The repo-relative project directory for which to calculate the version. /// - /// The version height, previously calculated by a call to + /// The version height, previously calculated by a call to /// with the same value for . /// /// @@ -247,7 +250,7 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj /// The repo whose ID and position in history is to be encoded. /// The repo-relative project directory for which to calculate the version. /// - /// The version height, previously calculated by a call to + /// The version height, previously calculated by a call to /// with the same value for . /// /// @@ -287,7 +290,7 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj /// Looks up the commit that matches a specified version number. /// /// The repository to search for a matching commit. - /// The version previously obtained from . + /// The version previously obtained from . /// /// The repo-relative project directory from which was originally calculated. /// @@ -306,7 +309,7 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj /// Looks up the commits that match a specified version number. /// /// The repository to search for a matching commit. - /// The version previously obtained from . + /// The version previously obtained from . /// The repo-relative project directory from which was originally calculated. /// The matching commits, or an empty enumeration if no match is found. public static IEnumerable GetCommitsFromVersion(this Repository repo, Version version, @@ -786,7 +789,7 @@ private static void AddReachableCommitsFrom(Commit startingCommit, HashSet /// The commit whose ID and position in history is to be encoded. /// The version options applicable at this point (either from commit or working copy). - /// The version height, previously calculated by a call to . + /// The version height, previously calculated by a call to . /// /// A version whose and /// components are calculated based on the commit. From c2657ae79c66af7ddd0b86a3676e1dd1e0c46325 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Thu, 24 Oct 2019 15:33:17 +0100 Subject: [PATCH 09/25] Minimise diff --- src/NerdBank.GitVersioning/GitExtensions.cs | 53 +++++++++------------ 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index 63fce2df..b77c22a1 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -40,9 +40,7 @@ public static class GitExtensions /// The repo-relative project directory for which to calculate the version. /// Optional base version to calculate the height. If not specified, the base version will be calculated by scanning the repository. /// The height of the commit. Always a positive integer. - public static int GetVersionHeight(this Commit commit, - string repoRelativeProjectDirectory = null, - Version baseVersion = null) + public static int GetVersionHeight(this Commit commit, string repoRelativeProjectDirectory = null, Version baseVersion = null) { Requires.NotNull(commit, nameof(commit)); Requires.Argument(repoRelativeProjectDirectory == null || !Path.IsPathRooted(repoRelativeProjectDirectory), nameof(repoRelativeProjectDirectory), "Path should be relative to repo root."); @@ -111,7 +109,7 @@ public static int GetVersionHeight(this Repository repo, string repoRelativeProj /// The height of the branch till the version is changed. public static int GetVersionHeight(this Branch branch, string repoRelativeProjectDirectory = null) { - return branch.Commits.First().GetVersionHeight(repoRelativeProjectDirectory); + return GetVersionHeight(branch.Commits.First(), repoRelativeProjectDirectory); } /// @@ -121,9 +119,9 @@ public static int GetVersionHeight(this Branch branch, string repoRelativeProjec /// The commit to measure the height of. /// /// - /// A function that returns false when we reach a commit that - /// should not be included in the height calculation. - /// May be null to count the height to the original commit. + /// A function that returns false when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. /// /// The height of the commit. Always a positive integer. public static int GetHeight(this Commit commit, IReadOnlyList pathFilters, @@ -150,13 +148,12 @@ public static int GetVersionHeight(this Branch branch, string repoRelativeProjec /// The branch to measure the height of. /// /// - /// A function that returns false when we reach a commit that - /// should not be included in the height calculation. - /// May be null to count the height to the original commit. + /// A function that returns false when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. /// /// The height of the branch. - public static int GetHeight(this Branch branch, IReadOnlyList pathFilters, - Func continueStepping = null) + public static int GetHeight(this Branch branch, IReadOnlyList pathFilters, Func continueStepping = null) { return GetHeight(branch.Commits.First(), pathFilters, continueStepping); } @@ -216,8 +213,8 @@ private static IRepository GetRepository(this IBelongToARepository repositoryMem /// The commit whose ID and position in history is to be encoded. /// The repo-relative project directory for which to calculate the version. /// - /// The version height, previously calculated by a call to - /// with the same value for . + /// The version height, previously calculated by a call to + /// with the same value for . /// /// /// A version whose and @@ -250,8 +247,8 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj /// The repo whose ID and position in history is to be encoded. /// The repo-relative project directory for which to calculate the version. /// - /// The version height, previously calculated by a call to - /// with the same value for . + /// The version height, previously calculated by a call to + /// with the same value for . /// /// /// A version whose and @@ -262,15 +259,13 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj /// the height of the git commit while the /// component is the first four bytes of the git commit id (forced to be a positive integer). /// - public static Version GetIdAsVersion(this Repository repo, - string repoRelativeProjectDirectory = null, int? versionHeight = null) + public static Version GetIdAsVersion(this Repository repo, string repoRelativeProjectDirectory = null, int? versionHeight = null) { Requires.NotNull(repo, nameof(repo)); var headCommit = repo.Head.Commits.FirstOrDefault(); VersionOptions workingCopyVersionOptions, committedVersionOptions; - if (IsVersionFileChangedInWorkingCopy(repo, repoRelativeProjectDirectory, - out committedVersionOptions, out workingCopyVersionOptions)) + if (IsVersionFileChangedInWorkingCopy(repo, repoRelativeProjectDirectory, out committedVersionOptions, out workingCopyVersionOptions)) { // Apply ordinary logic, but to the working copy version info. if (!versionHeight.HasValue) @@ -292,14 +287,13 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj /// The repository to search for a matching commit. /// The version previously obtained from . /// - /// The repo-relative project directory from which was originally calculated. + /// The repo-relative project directory from which was originally calculated. /// /// The matching commit, or null if no match is found. /// /// Thrown in the very rare situation that more than one matching commit is found. /// - public static Commit GetCommitFromVersion(this Repository repo, Version version, - string repoRelativeProjectDirectory = null) + public static Commit GetCommitFromVersion(this Repository repo, Version version, string repoRelativeProjectDirectory = null) { // Note we'll accept no match, or one match. But we throw if there is more than one match. return GetCommitsFromVersion(repo, version, repoRelativeProjectDirectory).SingleOrDefault(); @@ -554,8 +548,7 @@ private static int ReadVersionPosition(Version version, SemanticVersion.Position } } - private static bool IsVersionHeightMismatch(Version version, VersionOptions versionOptions, Commit commit, - string repoRelativeProjectDirectory) + private static bool IsVersionHeightMismatch(Version version, VersionOptions versionOptions, Commit commit, string repoRelativeProjectDirectory) { Requires.NotNull(version, nameof(version)); Requires.NotNull(versionOptions, nameof(versionOptions)); @@ -686,9 +679,9 @@ private static string EncodeAsHex(byte[] buffer) /// /// /// - /// A function that returns false when we reach a commit that - /// should not be included in the height calculation. - /// May be null to count the height to the original commit. + /// A function that returns false when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. /// /// The height of the branch. private static int GetCommitHeight(Commit commit, Dictionary heights, @@ -710,14 +703,12 @@ private static string EncodeAsHex(byte[] buffer) { if (includePaths != null && excludePaths != null) { - var repository = ((IBelongToARepository)commit).Repository; - // If the diff between this commit and any of its parents // touches a path that we care about, bump the height. // Otherwise, this commit will not increment the height. if (commit.Parents.Any(parent => ContainsRelevantChanges( - repository.Diff.Compare(parent.Tree, commit.Tree, includePaths)))) + commit.GetRepository().Diff.Compare(parent.Tree, commit.Tree, includePaths)))) { height = 1; } From 6300fe850d7f218cb889e5cef6a03ef24e970263 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Thu, 24 Oct 2019 15:48:36 +0100 Subject: [PATCH 10/25] Add regex to version.schema.json --- src/NerdBank.GitVersioning/version.schema.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NerdBank.GitVersioning/version.schema.json b/src/NerdBank.GitVersioning/version.schema.json index 6ca3da39..ddb80b76 100644 --- a/src/NerdBank.GitVersioning/version.schema.json +++ b/src/NerdBank.GitVersioning/version.schema.json @@ -178,9 +178,10 @@ }, "pathFilters": { "type": "array", - "description": "An array of repository relative paths (folders or files) that are used to filter commits when calculating the version height. A commit will not increment the version height if it does not touch a path listed in this array.", + "description": "An array of pathspec-like strings that are used to filter commits when calculating the version height. A commit will not increment the version height if its changed files are not included by these filters.\nPaths are relative to this file. Paths relative to the root of the repository can be specified with the `:/` prefix.\nExclusions can be specified with a `:^` prefix for relative paths, or a `:^/` prefix for absolute paths.", "items": { - "type": "string" + "type": "string", + "pattern": "^(:\\^|:!|:/|[^:])" }, "uniqueItems": true } From 36239aeb41162681df69db4f7c55089bf8a39876 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Thu, 24 Oct 2019 16:21:17 +0100 Subject: [PATCH 11/25] Fix all warnings --- .../GitExtensionsTests.cs | 2 +- src/NerdBank.GitVersioning/FilterPath.cs | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs index 31ddbcd0..ff7ab9e9 100644 --- a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs +++ b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs @@ -13,7 +13,7 @@ public class GitExtensionsTests : RepoTestBase { - private readonly IReadOnlyList emptyFilterPath; + private readonly IReadOnlyList emptyFilterPath = null; public GitExtensionsTests(ITestOutputHelper Logger) : base(Logger) diff --git a/src/NerdBank.GitVersioning/FilterPath.cs b/src/NerdBank.GitVersioning/FilterPath.cs index 819ab603..45d09246 100644 --- a/src/NerdBank.GitVersioning/FilterPath.cs +++ b/src/NerdBank.GitVersioning/FilterPath.cs @@ -56,11 +56,6 @@ private static string ParsePath(string path, string relativeTo) ); } - public FilterPath(string pathSpec, string relativeTo, Configuration config) : this(pathSpec, relativeTo, - config?.Get("core.ignorecase")?.Value ?? false) - { - } - /// /// Construct a from a pathspec-like string and a /// relative path within the repository. @@ -116,14 +111,27 @@ public FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false) .TrimEnd(Path.DirectorySeparatorChar); } + /// + /// Calculate the s for a given project within a repository. + /// + /// Version options for the project. + /// + /// Path to the project directory, relative to the root of the repository. + /// If null, assumes root of repository. + /// + /// Git repository containing the project. + /// A list of instances. public static IReadOnlyList FromVersionOptions(VersionOptions versionOptions, string relativeRepoProjectDirectory, IRepository repository) { Requires.NotNull(versionOptions, nameof(versionOptions)); + + var ignoreCase = repository?.Config.Get("core.ignorecase")?.Value ?? false; + return versionOptions.PathFilters ?.Select(pathSpec => new FilterPath(pathSpec, relativeRepoProjectDirectory, - repository?.Config)) + ignoreCase)) .ToList(); } From 75fdbbf9dd126c107351a07a73581bec4a31d8ad Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Mon, 11 Nov 2019 10:56:55 +0000 Subject: [PATCH 12/25] Update doc comments --- src/NerdBank.GitVersioning/GitExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index b77c22a1..a217f3f8 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -117,7 +117,7 @@ public static int GetVersionHeight(this Branch branch, string repoRelativeProjec /// the specified commit and the most distant ancestor (inclusive). /// /// The commit to measure the height of. - /// + /// Paths to include/exclude from height calculation. /// /// A function that returns false when we reach a commit that /// should not be included in the height calculation. @@ -146,7 +146,7 @@ public static int GetVersionHeight(this Branch branch, string repoRelativeProjec /// the specified branch's head and the most distant ancestor (inclusive). /// /// The branch to measure the height of. - /// + /// Paths to include/exclude from height calculation. /// /// A function that returns false when we reach a commit that /// should not be included in the height calculation. @@ -676,8 +676,8 @@ private static string EncodeAsHex(byte[] buffer) /// /// The commit to measure the height of. /// A cache of commits and their heights. - /// - /// + /// Repo root relative paths to include in height calculation. + /// Repo root relative paths to exclude from height calculation. /// /// A function that returns false when we reach a commit that /// should not be included in the height calculation. From 4eb917a3720e6ec3d4a789e9ab9d76a6f2d365f5 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Mon, 11 Nov 2019 12:17:35 +0000 Subject: [PATCH 13/25] Add tests --- .../FilterPathTests.cs | 176 +++++++++--------- .../GitExtensionsTests.cs | 68 +++++++ .../VersionFileTests.cs | 13 ++ src/NerdBank.GitVersioning/FilterPath.cs | 14 +- src/NerdBank.GitVersioning/GitExtensions.cs | 32 ++-- 5 files changed, 193 insertions(+), 110 deletions(-) diff --git a/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs b/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs index 35ba787e..0417c1e3 100644 --- a/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs +++ b/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs @@ -3,104 +3,98 @@ using Nerdbank.GitVersioning; using Xunit; -namespace NerdBank.GitVersioning.Tests +public class FilterPathTests { - public class FilterPathTests + [Theory] + [InlineData("relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData("./relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData("./dir\\hi/relativepath.txt", "foo", "foo/dir/hi/relativepath.txt")] + [InlineData(".\\relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":^relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":!relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":!/absolutepath.txt", "foo", "absolutepath.txt")] + [InlineData(":!\\absolutepath.txt", "foo", "absolutepath.txt")] + [InlineData("../bar/relativepath.txt", "foo", "bar/relativepath.txt")] + [InlineData(":/", "foo", "")] + [InlineData(":/absolutepath.txt", "foo", "absolutepath.txt")] + [InlineData(":/bar/absolutepath.txt", "foo", "bar/absolutepath.txt")] + [InlineData(":\\bar\\absolutepath.txt", "foo", "bar/absolutepath.txt")] + public void CanBeParsedToRepoRelativePath(string pathSpec, string relativeTo, string expected) { - [Theory] - [InlineData("relativepath.txt", "foo", "foo/relativepath.txt")] - [InlineData("./relativepath.txt", "foo", "foo/relativepath.txt")] - [InlineData(":^relativepath.txt", "foo", "foo/relativepath.txt")] - [InlineData(":!relativepath.txt", "foo", "foo/relativepath.txt")] - [InlineData(":!/absolutepath.txt", "foo", "absolutepath.txt")] - [InlineData("../bar/relativepath.txt", "foo", "bar/relativepath.txt")] - [InlineData(":/", "foo", "")] - [InlineData(":/absolutepath.txt", "foo", "absolutepath.txt")] - [InlineData(":/bar/absolutepath.txt", "foo", "bar/absolutepath.txt")] - public void CanBeParsedToRepoRelativePath(string pathSpec, string relativeTo, string expected) - { - Assert.Equal(expected.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), - new FilterPath(pathSpec, relativeTo).RepoRelativePath); - } + Assert.Equal(expected, new FilterPath(pathSpec, relativeTo).RepoRelativePath); + } - [Theory] - [InlineData(":!.", "foo", "foo")] - [InlineData(":!.", "foo", "foo/")] - [InlineData(":!.", "foo", "foo/relativepath.txt")] - [InlineData(":!relativepath.txt", "foo", "foo/relativepath.txt")] - [InlineData(":^relativepath.txt", "foo", "foo/relativepath.txt")] - [InlineData(":^./relativepath.txt", "foo", "foo/relativepath.txt")] - [InlineData(":^../bar", "foo", "bar")] - [InlineData(":^../bar", "foo", "bar/")] - [InlineData(":^../bar", "foo", "bar/somefile.txt")] - [InlineData(":^/absolute.txt", "foo", "absolute.txt")] - public void PathsCanBeExcluded(string pathSpec, string relativeTo, string repoRelativePath) - { - Assert.True(new FilterPath(pathSpec, relativeTo, true).Excludes( - repoRelativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))); - Assert.True(new FilterPath(pathSpec, relativeTo, false).Excludes( - repoRelativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))); - } + [Theory] + [InlineData(":!.", "foo", "foo")] + [InlineData(":!.", "foo", "foo/")] + [InlineData(":!.", "foo", "foo/relativepath.txt")] + [InlineData(":!relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":^relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":^./relativepath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":^../bar", "foo", "bar")] + [InlineData(":^../bar", "foo", "bar/")] + [InlineData(":^../bar", "foo", "bar/somefile.txt")] + [InlineData(":^/absolute.txt", "foo", "absolute.txt")] + public void PathsCanBeExcluded(string pathSpec, string relativeTo, string repoRelativePath) + { + Assert.True(new FilterPath(pathSpec, relativeTo, true).Excludes(repoRelativePath)); + Assert.True(new FilterPath(pathSpec, relativeTo, false).Excludes(repoRelativePath)); + } - [Theory] - [InlineData(":!.", "foo", "foo.txt")] - [InlineData(":^relativepath.txt", "foo", "foo2/relativepath.txt")] - [InlineData(":^/absolute.txt", "foo", "absolute.txt.bak")] - [InlineData(":^/absolute.txt", "foo", "absolute")] + [Theory] + [InlineData(":!.", "foo", "foo.txt")] + [InlineData(":^relativepath.txt", "foo", "foo2/relativepath.txt")] + [InlineData(":^/absolute.txt", "foo", "absolute.txt.bak")] + [InlineData(":^/absolute.txt", "foo", "absolute")] - // Not exclude paths - [InlineData(":/absolute.txt", "foo", "absolute.txt")] - [InlineData("/absolute.txt", "foo", "absolute.txt")] - [InlineData("../root.txt", "foo", "root.txt")] - [InlineData("relativepath.txt", "foo", "foo/relativepath.txt")] - public void NonMatchingPathsAreNotExcluded(string pathSpec, string relativeTo, string repoRelativePath) - { - Assert.False(new FilterPath(pathSpec, relativeTo, true).Excludes( - repoRelativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))); - Assert.False(new FilterPath(pathSpec, relativeTo, false).Excludes( - repoRelativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))); - } + // Not exclude paths + [InlineData(":/absolute.txt", "foo", "absolute.txt")] + [InlineData("/absolute.txt", "foo", "absolute.txt")] + [InlineData("../root.txt", "foo", "root.txt")] + [InlineData("relativepath.txt", "foo", "foo/relativepath.txt")] + public void NonMatchingPathsAreNotExcluded(string pathSpec, string relativeTo, string repoRelativePath) + { + Assert.False(new FilterPath(pathSpec, relativeTo, true).Excludes(repoRelativePath)); + Assert.False(new FilterPath(pathSpec, relativeTo, false).Excludes(repoRelativePath)); + } - [Theory] - [InlineData(":!.", "foo", "Foo")] - [InlineData(":!.", "foo", "Foo/")] - [InlineData(":!.", "foo", "Foo/relativepath.txt")] - [InlineData(":!RelativePath.txt", "foo", "foo/relativepath.txt")] - [InlineData(":^relativepath.txt", "foo", "Foo/RelativePath.txt")] - [InlineData(":^./relativepath.txt", "Foo", "foo/RelativePath.txt")] - [InlineData(":^../bar", "foo", "Bar")] - [InlineData(":^../bar", "foo", "Bar/")] - [InlineData(":^../bar", "foo", "Bar/SomeFile.txt")] - [InlineData(":^/absOLUte.txt", "foo", "Absolute.TXT")] - public void PathsCanBeExcludedCaseInsensitive(string pathSpec, string relativeTo, string repoRelativePath) - { - Assert.True(new FilterPath(pathSpec, relativeTo, true).Excludes( - repoRelativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))); - } + [Theory] + [InlineData(":!.", "foo", "Foo")] + [InlineData(":!.", "foo", "Foo/")] + [InlineData(":!.", "foo", "Foo/relativepath.txt")] + [InlineData(":!RelativePath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":^relativepath.txt", "foo", "Foo/RelativePath.txt")] + [InlineData(":^./relativepath.txt", "Foo", "foo/RelativePath.txt")] + [InlineData(":^../bar", "foo", "Bar")] + [InlineData(":^../bar", "foo", "Bar/")] + [InlineData(":^../bar", "foo", "Bar/SomeFile.txt")] + [InlineData(":^/absOLUte.txt", "foo", "Absolute.TXT")] + public void PathsCanBeExcludedCaseInsensitive(string pathSpec, string relativeTo, string repoRelativePath) + { + Assert.True(new FilterPath(pathSpec, relativeTo, true).Excludes(repoRelativePath)); + } - [Theory] - [InlineData(":!.", "foo", "Foo")] - [InlineData(":!.", "foo", "Foo/")] - [InlineData(":!.", "foo", "Foo/relativepath.txt")] - [InlineData(":!RelativePath.txt", "foo", "foo/relativepath.txt")] - [InlineData(":^relativepath.txt", "foo", "Foo/RelativePath.txt")] - [InlineData(":^./relativepath.txt", "Foo", "foo/RelativePath.txt")] - [InlineData(":^../bar", "foo", "Bar")] - [InlineData(":^../bar", "foo", "Bar/")] - [InlineData(":^../bar", "foo", "Bar/SomeFile.txt")] - [InlineData(":^/absOLUte.txt", "foo", "Absolute.TXT")] - public void NonMatchingPathsAreNotExcludedCaseSensitive(string pathSpec, string relativeTo, string repoRelativePath) - { - Assert.False(new FilterPath(pathSpec, relativeTo, false).Excludes( - repoRelativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))); - } + [Theory] + [InlineData(":!.", "foo", "Foo")] + [InlineData(":!.", "foo", "Foo/")] + [InlineData(":!.", "foo", "Foo/relativepath.txt")] + [InlineData(":!RelativePath.txt", "foo", "foo/relativepath.txt")] + [InlineData(":^relativepath.txt", "foo", "Foo/RelativePath.txt")] + [InlineData(":^./relativepath.txt", "Foo", "foo/RelativePath.txt")] + [InlineData(":^../bar", "foo", "Bar")] + [InlineData(":^../bar", "foo", "Bar/")] + [InlineData(":^../bar", "foo", "Bar/SomeFile.txt")] + [InlineData(":^/absOLUte.txt", "foo", "Absolute.TXT")] + public void NonMatchingPathsAreNotExcludedCaseSensitive(string pathSpec, string relativeTo, string repoRelativePath) + { + Assert.False(new FilterPath(pathSpec, relativeTo, false).Excludes(repoRelativePath)); + } - [Fact] - public void InvalidPathspecsThrow() - { - Assert.Throws(() => new FilterPath(null, "")); - Assert.Throws(() => new FilterPath("", "")); - Assert.Throws(() => new FilterPath(":?", "")); - } + [Fact] + public void InvalidPathspecsThrow() + { + Assert.Throws(() => new FilterPath(null, "")); + Assert.Throws(() => new FilterPath("", "")); + Assert.Throws(() => new FilterPath(":?", "")); } } \ No newline at end of file diff --git a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs index ff7ab9e9..f1882e39 100644 --- a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs +++ b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs @@ -123,6 +123,74 @@ public void GetVersionHeight_VersionJsonHasParsingErrorsInHistory() Assert.Equal(0, this.Repo.GetVersionHeight()); } + [Fact] + public void GetVersionHeight_IntroducingFiltersIncrementsHeight() + { + this.WriteVersionFile(relativeDirectory: "some-sub-dir"); + Assert.Equal(1, this.Repo.GetVersionHeight("some-sub-dir")); + + var versionData = VersionOptions.FromVersion(new Version("1.2")); + versionData.PathFilters = new[] { "./" }; + this.WriteVersionFile(versionData, "some-sub-dir"); + Assert.Equal(2, this.Repo.GetVersionHeight("some-sub-dir")); + } + + [Fact] + public void GetVersionHeight_IncludeFilter() + { + var versionData = VersionOptions.FromVersion(new Version("1.2")); + versionData.PathFilters = new[] { "./" }; + this.WriteVersionFile(versionData, "some-sub-dir"); + Assert.Equal(1, this.Repo.GetVersionHeight("some-sub-dir")); + + // Expect commit outside of project tree to not affect version height + var otherFilePath = Path.Combine(this.RepoPath, "my-file.txt"); + File.WriteAllText(otherFilePath, "hello"); + Commands.Stage(this.Repo, otherFilePath); + this.Repo.Commit("Add other file outside of project root", this.Signer, this.Signer); + Assert.Equal(1, this.Repo.GetVersionHeight("some-sub-dir")); + + // Expect commit inside project tree to affect version height + var containedFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "another-file.txt"); + File.WriteAllText(containedFilePath, "hello"); + Commands.Stage(this.Repo, containedFilePath); + this.Repo.Commit("Add file within project root", this.Signer, this.Signer); + Assert.Equal(2, this.Repo.GetVersionHeight("some-sub-dir")); + } + + [Fact] + public void GetVersionHeight_IncludeExcludeFilter() + { + var versionData = VersionOptions.FromVersion(new Version("1.2")); + versionData.PathFilters = new[] { "./", ":^/some-sub-dir/ignore.txt", ":^excluded-dir" }; + this.WriteVersionFile(versionData, "some-sub-dir"); + Assert.Equal(1, this.Repo.GetVersionHeight("some-sub-dir")); + + // Commit touching excluded path does not affect version height + var ignoredFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "ignore.txt"); + File.WriteAllText(ignoredFilePath, "hello"); + Commands.Stage(this.Repo, ignoredFilePath); + this.Repo.Commit("Add excluded file", this.Signer, this.Signer); + Assert.Equal(1, this.Repo.GetVersionHeight("some-sub-dir")); + + // Commit touching both excluded and included path does affect height + var includedFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "another-file.txt"); + File.WriteAllText(includedFilePath, "hello"); + File.WriteAllText(ignoredFilePath, "changed"); + Commands.Stage(this.Repo, includedFilePath); + Commands.Stage(this.Repo, ignoredFilePath); + this.Repo.Commit("Change both excluded and included file", this.Signer, this.Signer); + Assert.Equal(2, this.Repo.GetVersionHeight("some-sub-dir")); + + // Commit touching excluded directory does not affect version height + var fileInExcludedDirPath = Path.Combine(this.RepoPath, "some-sub-dir", "excluded-dir", "ignore.txt"); + Directory.CreateDirectory(Path.GetDirectoryName(fileInExcludedDirPath)); + File.WriteAllText(fileInExcludedDirPath, "hello"); + Commands.Stage(this.Repo, fileInExcludedDirPath); + this.Repo.Commit("Add file to excluded dir", this.Signer, this.Signer); + Assert.Equal(2, this.Repo.GetVersionHeight("some-sub-dir")); + } + [Theory] [InlineData("2.2-alpha", "2.2-rc", false)] [InlineData("2.2-rc", "2.2", false)] diff --git a/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs b/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs index 7e959d04..398428f9 100644 --- a/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs +++ b/src/NerdBank.GitVersioning.Tests/VersionFileTests.cs @@ -321,6 +321,19 @@ public void GetVersion_ReadReleaseSettings_BranchName() Assert.Equal("someValue{version}", versionOptions.Release.BranchName); } + [Fact] + public void GetVersion_ReadPathFilters() + { + var json = @"{ ""version"" : ""1.2"", ""pathFilters"" : [ "":/root.txt"", ""./hello"" ] }"; + var path = Path.Combine(this.RepoPath, "version.json"); + File.WriteAllText(path, json); + + var versionOptions = VersionFile.GetVersion(this.RepoPath); + + Assert.NotNull(versionOptions.PathFilters); + Assert.Equal(new[] {":/root.txt", "./hello"}, versionOptions.PathFilters); + } + [Fact] public void GetVersion_String_MissingFile() { diff --git a/src/NerdBank.GitVersioning/FilterPath.cs b/src/NerdBank.GitVersioning/FilterPath.cs index 45d09246..c4c8dee6 100644 --- a/src/NerdBank.GitVersioning/FilterPath.cs +++ b/src/NerdBank.GitVersioning/FilterPath.cs @@ -28,13 +28,13 @@ public class FilterPath private static string ParsePath(string path, string relativeTo) { // Path is absolute, nothing to do here - if (path[0] == '/') + if (path[0] == '/' || path[0] == '\\') { return path.Substring(1); } var combined = relativeTo == null ? path : relativeTo + '/' + path; - return string.Join(Path.DirectorySeparatorChar.ToString(), + return string.Join("/", combined .Split(new[] {Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar}, StringSplitOptions.RemoveEmptyEntries) @@ -91,7 +91,7 @@ public FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false) this.RepoRelativePath = ParsePath(pathSpec.Substring(2), relativeTo); } - else if (pathSpec.Length > 1 && pathSpec[1] == '/') + else if (pathSpec.Length > 1 && pathSpec[1] == '/' || pathSpec[1] == '\\') { this.RepoRelativePath = pathSpec.Substring(2); } @@ -107,8 +107,8 @@ public FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false) this.RepoRelativePath = this.RepoRelativePath - .Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar) - .TrimEnd(Path.DirectorySeparatorChar); + .Replace('\\', '/') + .TrimEnd('/'); } /// @@ -138,7 +138,7 @@ public FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false) /// /// Determines if should be excluded by this . /// - /// Path (repo relative). Slashes should be canonical for the OS. + /// Forward-slash delimited path (repo relative). /// /// True if this is an excluding filter that matches /// , otherwise false. @@ -148,7 +148,7 @@ public bool Excludes(string repoRelativePath) if (!this.IsExclude) return false; return this.RepoRelativePath.Equals(repoRelativePath, this.stringComparison) || - repoRelativePath.StartsWith(this.RepoRelativePath + Path.DirectorySeparatorChar, + repoRelativePath.StartsWith(this.RepoRelativePath + "/", this.stringComparison); } } diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index a217f3f8..0d24b59d 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -694,35 +694,43 @@ private static string EncodeAsHex(byte[] buffer) bool ContainsRelevantChanges(IEnumerable changes) => excludePaths.Count == 0 ? changes.Any() - : changes.Any(change => excludePaths.Any(exclude => exclude.Excludes(change.Path))); + // If there is a single change that isn't excluded, + // then this commit is relevant. + : changes.Any(change => !excludePaths.Any(exclude => exclude.Excludes(change.Path))); if (!heights.TryGetValue(commit.Id, out int height)) { - height = 0; if (continueStepping == null || continueStepping(commit)) { + height = 1; + if (includePaths != null && excludePaths != null) { // If the diff between this commit and any of its parents - // touches a path that we care about, bump the height. - // Otherwise, this commit will not increment the height. - if (commit.Parents.Any(parent => - ContainsRelevantChanges( - commit.GetRepository().Diff.Compare(parent.Tree, commit.Tree, includePaths)))) + // does not touch a path that we care about, don't bump the + // height. + var relevantCommit = + commit.Parents.Any() + ? commit.Parents.Any(parent => ContainsRelevantChanges(commit.GetRepository().Diff + .Compare(parent.Tree, commit.Tree, includePaths))) + : ContainsRelevantChanges(commit.GetRepository().Diff + .Compare(null, commit.Tree, includePaths)); + + if (!relevantCommit) { - height = 1; + height = 0; } } - else - { - height = 1; - } if (commit.Parents.Any()) { height += commit.Parents.Max(p => GetCommitHeight(p, heights, includePaths, excludePaths, continueStepping)); } } + else + { + height = 0; + } heights[commit.Id] = height; } From 49f620a7e7b060da667fb0dd66e8bc309e9253d6 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Tue, 10 Dec 2019 10:57:42 +0000 Subject: [PATCH 14/25] Markups --- src/NerdBank.GitVersioning/FilterPath.cs | 8 +++++++- src/NerdBank.GitVersioning/GitExtensions.cs | 20 ++++++++----------- .../version.schema.json | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/NerdBank.GitVersioning/FilterPath.cs b/src/NerdBank.GitVersioning/FilterPath.cs index c4c8dee6..61014885 100644 --- a/src/NerdBank.GitVersioning/FilterPath.cs +++ b/src/NerdBank.GitVersioning/FilterPath.cs @@ -120,7 +120,10 @@ public FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false) /// If null, assumes root of repository. /// /// Git repository containing the project. - /// A list of instances. + /// + /// null if no path filters are set. Otherwise, returns a list of + /// instances. + /// public static IReadOnlyList FromVersionOptions(VersionOptions versionOptions, string relativeRepoProjectDirectory, IRepository repository) @@ -145,6 +148,9 @@ public FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false) /// public bool Excludes(string repoRelativePath) { + if (repoRelativePath is null) + throw new ArgumentNullException(nameof(repoRelativePath)); + if (!this.IsExclude) return false; return this.RepoRelativePath.Equals(repoRelativePath, this.stringComparison) || diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index 0d24b59d..5e83b398 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -124,8 +124,7 @@ public static int GetVersionHeight(this Branch branch, string repoRelativeProjec /// May be null to count the height to the original commit. /// /// The height of the commit. Always a positive integer. - public static int GetHeight(this Commit commit, IReadOnlyList pathFilters, - Func continueStepping = null) + public static int GetHeight(this Commit commit, IReadOnlyList pathFilters, Func continueStepping = null) { Requires.NotNull(commit, nameof(commit)); @@ -213,7 +212,7 @@ private static IRepository GetRepository(this IBelongToARepository repositoryMem /// The commit whose ID and position in history is to be encoded. /// The repo-relative project directory for which to calculate the version. /// - /// The version height, previously calculated by a call to + /// The version height, previously calculated by a call to /// with the same value for . /// /// @@ -247,7 +246,7 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj /// The repo whose ID and position in history is to be encoded. /// The repo-relative project directory for which to calculate the version. /// - /// The version height, previously calculated by a call to + /// The version height, previously calculated by a call to /// with the same value for . /// /// @@ -285,7 +284,7 @@ public static Version GetIdAsVersion(this Repository repo, string repoRelativePr /// Looks up the commit that matches a specified version number. /// /// The repository to search for a matching commit. - /// The version previously obtained from . + /// The version previously obtained from . /// /// The repo-relative project directory from which was originally calculated. /// @@ -303,11 +302,10 @@ public static Commit GetCommitFromVersion(this Repository repo, Version version, /// Looks up the commits that match a specified version number. /// /// The repository to search for a matching commit. - /// The version previously obtained from . + /// The version previously obtained from . /// The repo-relative project directory from which was originally calculated. /// The matching commits, or an empty enumeration if no match is found. - public static IEnumerable GetCommitsFromVersion(this Repository repo, Version version, - string repoRelativeProjectDirectory = null) + public static IEnumerable GetCommitsFromVersion(this Repository repo, Version version, string repoRelativeProjectDirectory = null) { Requires.NotNull(repo, nameof(repo)); Requires.NotNull(version, nameof(version)); @@ -684,9 +682,7 @@ private static string EncodeAsHex(byte[] buffer) /// May be null to count the height to the original commit. /// /// The height of the branch. - private static int GetCommitHeight(Commit commit, Dictionary heights, - IEnumerable includePaths, IReadOnlyList excludePaths, - Func continueStepping) + private static int GetCommitHeight(Commit commit, Dictionary heights, IEnumerable includePaths, IReadOnlyList excludePaths, Func continueStepping) { Requires.NotNull(commit, nameof(commit)); Requires.NotNull(heights, nameof(heights)); @@ -788,7 +784,7 @@ private static void AddReachableCommitsFrom(Commit startingCommit, HashSet /// The commit whose ID and position in history is to be encoded. /// The version options applicable at this point (either from commit or working copy). - /// The version height, previously calculated by a call to . + /// The version height, previously calculated by a call to . /// /// A version whose and /// components are calculated based on the commit. diff --git a/src/NerdBank.GitVersioning/version.schema.json b/src/NerdBank.GitVersioning/version.schema.json index ddb80b76..7f772945 100644 --- a/src/NerdBank.GitVersioning/version.schema.json +++ b/src/NerdBank.GitVersioning/version.schema.json @@ -178,7 +178,7 @@ }, "pathFilters": { "type": "array", - "description": "An array of pathspec-like strings that are used to filter commits when calculating the version height. A commit will not increment the version height if its changed files are not included by these filters.\nPaths are relative to this file. Paths relative to the root of the repository can be specified with the `:/` prefix.\nExclusions can be specified with a `:^` prefix for relative paths, or a `:^/` prefix for absolute paths.", + "description": "An array of pathspec-like strings that are used to filter commits when calculating the version height. A commit will not increment the version height if its changed files are not included by these filters.\nPaths are relative to this file. Paths relative to the root of the repository can be specified with the `:/` prefix.\nExclusions can be specified with a `:^` prefix for relative paths, or a `:^/` prefix for paths relative to the root of the repository.\nAfter a path matches any non-exclude filter, it will be run through all exclude filters. If it matches, the path is ignored.", "items": { "type": "string", "pattern": "^(:\\^|:!|:/|[^:])" From 565bfe0b8f460b487b60f4a6b5a5334eaccd5a67 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Tue, 10 Dec 2019 11:36:45 +0000 Subject: [PATCH 15/25] Markups --- .../FilterPathTests.cs | 5 ++++ src/NerdBank.GitVersioning/FilterPath.cs | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs b/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs index 0417c1e3..489a8f13 100644 --- a/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs +++ b/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs @@ -6,6 +6,9 @@ public class FilterPathTests { [Theory] + [InlineData("./", "foo", "foo")] + [InlineData("../relative-dir", "foo", "relative-dir")] + [InlineData("../../some/dir/here", "foo/multi/wow", "foo/some/dir/here")] [InlineData("relativepath.txt", "foo", "foo/relativepath.txt")] [InlineData("./relativepath.txt", "foo", "foo/relativepath.txt")] [InlineData("./dir\\hi/relativepath.txt", "foo", "foo/dir/hi/relativepath.txt")] @@ -96,5 +99,7 @@ public void InvalidPathspecsThrow() Assert.Throws(() => new FilterPath(null, "")); Assert.Throws(() => new FilterPath("", "")); Assert.Throws(() => new FilterPath(":?", "")); + Assert.Throws(() => new FilterPath("../foo.txt", "")); + Assert.Throws(() => new FilterPath(".././a/../../foo.txt", "foo")); } } \ No newline at end of file diff --git a/src/NerdBank.GitVersioning/FilterPath.cs b/src/NerdBank.GitVersioning/FilterPath.cs index 61014885..4af1a61b 100644 --- a/src/NerdBank.GitVersioning/FilterPath.cs +++ b/src/NerdBank.GitVersioning/FilterPath.cs @@ -25,6 +25,21 @@ public class FilterPath /// public string RepoRelativePath { get; } + /// + /// Parses a pathspec-like string into a root-relative path. + /// + /// + /// See for supported + /// formats of pathspecs. + /// + /// + /// Path that is relative to. + /// Can be null - which indicates is + /// relative to the root of the repository. + /// + /// + /// Forward slash delimited string representing the root-relative path. + /// private static string ParsePath(string path, string relativeTo) { // Path is absolute, nothing to do here @@ -34,24 +49,35 @@ private static string ParsePath(string path, string relativeTo) } var combined = relativeTo == null ? path : relativeTo + '/' + path; + return string.Join("/", combined .Split(new[] {Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar}, StringSplitOptions.RemoveEmptyEntries) + // Loop through each path segment... .Aggregate(new Stack(), (parts, segment) => { switch (segment) { + // If it refers to the current directory, skip it case ".": return parts; + + // If it refers to the parent directory, pop the most recent directory case "..": + if (parts.Count == 0) + throw new FormatException($"Too many '..' in path '{combined}' - would escape the root of the repository."); + parts.Pop(); return parts; + + // Otherwise it's a directory/file name - add it to the stack default: parts.Push(segment); return parts; } }) + // Reverse the stack, so it iterates root -> leaf .Reverse() ); } From 4310a2d432dfabae3b1ab04410376dabbdaaea46 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Tue, 10 Dec 2019 13:08:20 +0000 Subject: [PATCH 16/25] Get filters at each commit --- .../GitExtensionsTests.cs | 20 ++--- src/NerdBank.GitVersioning/GitExtensions.cs | 90 +++++++++++++------ 2 files changed, 70 insertions(+), 40 deletions(-) diff --git a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs index f1882e39..47748d75 100644 --- a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs +++ b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs @@ -13,8 +13,6 @@ public class GitExtensionsTests : RepoTestBase { - private readonly IReadOnlyList emptyFilterPath = null; - public GitExtensionsTests(ITestOutputHelper Logger) : base(Logger) { @@ -25,8 +23,8 @@ public GitExtensionsTests(ITestOutputHelper Logger) public void GetHeight_EmptyRepo() { Branch head = this.Repo.Head; - Assert.Throws(() => head.GetHeight(this.emptyFilterPath)); - Assert.Throws(() => head.GetHeight(this.emptyFilterPath, c => true)); + Assert.Throws(() => head.GetHeight()); + Assert.Throws(() => head.GetHeight(c => true)); } [Fact] @@ -35,11 +33,11 @@ public void GetHeight_SinglePath() var first = this.Repo.Commit("First", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); var second = this.Repo.Commit("Second", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); var third = this.Repo.Commit("Third", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); - Assert.Equal(3, this.Repo.Head.GetHeight(this.emptyFilterPath)); - Assert.Equal(3, this.Repo.Head.GetHeight(this.emptyFilterPath, c => true)); + Assert.Equal(3, this.Repo.Head.GetHeight()); + Assert.Equal(3, this.Repo.Head.GetHeight(c => true)); - Assert.Equal(2, this.Repo.Head.GetHeight(this.emptyFilterPath, c => c != first)); - Assert.Equal(1, this.Repo.Head.GetHeight(this.emptyFilterPath, c => c != second)); + Assert.Equal(2, this.Repo.Head.GetHeight(c => c != first)); + Assert.Equal(1, this.Repo.Head.GetHeight(c => c != second)); } [Fact] @@ -58,13 +56,13 @@ public void GetHeight_Merge() this.Repo.Merge(secondCommit, new Signature("t", "t@t.com", DateTimeOffset.Now), new MergeOptions { FastForwardStrategy = FastForwardStrategy.NoFastForward }); // While we've created 8 commits, the tallest height is only 7. - Assert.Equal(7, this.Repo.Head.GetHeight(this.emptyFilterPath)); + Assert.Equal(7, this.Repo.Head.GetHeight()); // Now stop enumerating early on just one branch of the ancestry -- the number should remain high. - Assert.Equal(7, this.Repo.Head.GetHeight(this.emptyFilterPath, c => c != secondCommit)); + Assert.Equal(7, this.Repo.Head.GetHeight(c => c != secondCommit)); // This time stop in both branches of history, and verify that we count the taller one. - Assert.Equal(3, this.Repo.Head.GetHeight(this.emptyFilterPath, c => c != secondCommit && c != branchCommits[2])); + Assert.Equal(3, this.Repo.Head.GetHeight(c => c != secondCommit && c != branchCommits[2])); } [Fact] diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index 5e83b398..0c8e2b2e 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -58,8 +58,7 @@ public static int GetVersionHeight(this Commit commit, string repoRelativeProjec var versionHeightPosition = versionOptions.VersionHeightPosition; if (versionHeightPosition.HasValue) { - var pathFilters = FilterPath.FromVersionOptions(versionOptions, repoRelativeProjectDirectory, commit.GetRepository()); - int height = commit.GetHeight(pathFilters, c => CommitMatchesVersion(c, baseSemVer, versionHeightPosition.Value, repoRelativeProjectDirectory)); + int height = commit.GetHeight(repoRelativeProjectDirectory, c => CommitMatchesVersion(c, baseSemVer, versionHeightPosition.Value, repoRelativeProjectDirectory)); return height; } @@ -117,27 +116,35 @@ public static int GetVersionHeight(this Branch branch, string repoRelativeProjec /// the specified commit and the most distant ancestor (inclusive). /// /// The commit to measure the height of. - /// Paths to include/exclude from height calculation. /// /// A function that returns false when we reach a commit that /// should not be included in the height calculation. /// May be null to count the height to the original commit. /// /// The height of the commit. Always a positive integer. - public static int GetHeight(this Commit commit, IReadOnlyList pathFilters, Func continueStepping = null) + public static int GetHeight(this Commit commit, Func continueStepping = null) { - Requires.NotNull(commit, nameof(commit)); - - var includePaths = - pathFilters - ?.Where(filter => !filter.IsExclude) - .Select(filter => filter.RepoRelativePath) - .ToList(); + return commit.GetHeight(null, continueStepping); + } - var excludePaths = pathFilters?.Where(filter => filter.IsExclude).ToList(); + /// + /// Gets the number of commits in the longest single path between + /// the specified commit and the most distant ancestor (inclusive). + /// + /// The commit to measure the height of. + /// The path to the directory of the project whose version is being queried, relative to the repo root. + /// + /// A function that returns false when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. + /// + /// The height of the commit. Always a positive integer. + public static int GetHeight(this Commit commit, string repoRelativeProjectDirectory, Func continueStepping = null) + { + Requires.NotNull(commit, nameof(commit)); var heights = new Dictionary(); - return GetCommitHeight(commit, heights, includePaths, excludePaths, continueStepping); + return GetCommitHeight(commit, repoRelativeProjectDirectory, heights, continueStepping); } /// @@ -145,16 +152,32 @@ public static int GetHeight(this Commit commit, IReadOnlyList pathFi /// the specified branch's head and the most distant ancestor (inclusive). /// /// The branch to measure the height of. - /// Paths to include/exclude from height calculation. /// /// A function that returns false when we reach a commit that /// should not be included in the height calculation. /// May be null to count the height to the original commit. /// /// The height of the branch. - public static int GetHeight(this Branch branch, IReadOnlyList pathFilters, Func continueStepping = null) + public static int GetHeight(this Branch branch, Func continueStepping = null) { - return GetHeight(branch.Commits.First(), pathFilters, continueStepping); + return branch.GetHeight(null, continueStepping); + } + + /// + /// Gets the number of commits in the longest single path between + /// the specified branch's head and the most distant ancestor (inclusive). + /// + /// The branch to measure the height of. + /// The path to the directory of the project whose version is being queried, relative to the repo root. + /// + /// A function that returns false when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. + /// + /// The height of the branch. + public static int GetHeight(this Branch branch, string repoRelativeProjectDirectory, Func continueStepping = null) + { + return GetHeight(branch.Commits.First(), repoRelativeProjectDirectory, continueStepping); } /// @@ -559,8 +582,7 @@ private static bool IsVersionHeightMismatch(Version version, VersionOptions vers int expectedVersionHeight = ReadVersionPosition(version, position.Value); var actualVersionOffset = versionOptions.VersionHeightOffsetOrDefault; - var pathFilters = FilterPath.FromVersionOptions(versionOptions, repoRelativeProjectDirectory, commit.GetRepository()); - var actualVersionHeight = commit.GetHeight(pathFilters, c => CommitMatchesVersion(c, version, position.Value - 1, repoRelativeProjectDirectory)); + var actualVersionHeight = commit.GetHeight(repoRelativeProjectDirectory, c => CommitMatchesVersion(c, version, position.Value - 1, repoRelativeProjectDirectory)); return expectedVersionHeight != actualVersionHeight + actualVersionOffset; } @@ -673,31 +695,41 @@ private static string EncodeAsHex(byte[] buffer) /// the specified branch's head and the most distant ancestor (inclusive). /// /// The commit to measure the height of. + /// The path to the directory of the project whose height is being queried, relative to the repo root. /// A cache of commits and their heights. - /// Repo root relative paths to include in height calculation. - /// Repo root relative paths to exclude from height calculation. /// /// A function that returns false when we reach a commit that /// should not be included in the height calculation. /// May be null to count the height to the original commit. /// /// The height of the branch. - private static int GetCommitHeight(Commit commit, Dictionary heights, IEnumerable includePaths, IReadOnlyList excludePaths, Func continueStepping) + private static int GetCommitHeight(Commit commit, string repoRelativeProjectDirectory, Dictionary heights, Func continueStepping) { Requires.NotNull(commit, nameof(commit)); Requires.NotNull(heights, nameof(heights)); - bool ContainsRelevantChanges(IEnumerable changes) => - excludePaths.Count == 0 - ? changes.Any() - // If there is a single change that isn't excluded, - // then this commit is relevant. - : changes.Any(change => !excludePaths.Any(exclude => exclude.Excludes(change.Path))); - if (!heights.TryGetValue(commit.Id, out int height)) { if (continueStepping == null || continueStepping(commit)) { + var versionOptions = repoRelativeProjectDirectory != null ? VersionFile.GetVersion(commit, repoRelativeProjectDirectory) : null; + var pathFilters = versionOptions != null ? FilterPath.FromVersionOptions(versionOptions, repoRelativeProjectDirectory, commit.GetRepository()) : null; + + var includePaths = + pathFilters + ?.Where(filter => !filter.IsExclude) + .Select(filter => filter.RepoRelativePath) + .ToList(); + + var excludePaths = pathFilters?.Where(filter => filter.IsExclude).ToList(); + + bool ContainsRelevantChanges(IEnumerable changes) => + excludePaths.Count == 0 + ? changes.Any() + // If there is a single change that isn't excluded, + // then this commit is relevant. + : changes.Any(change => !excludePaths.Any(exclude => exclude.Excludes(change.Path))); + height = 1; if (includePaths != null && excludePaths != null) @@ -720,7 +752,7 @@ private static int GetCommitHeight(Commit commit, Dictionary heig if (commit.Parents.Any()) { - height += commit.Parents.Max(p => GetCommitHeight(p, heights, includePaths, excludePaths, continueStepping)); + height += commit.Parents.Max(p => GetCommitHeight(p, repoRelativeProjectDirectory, heights, continueStepping)); } } else From fea390581fef171fdef0c53c14e02925ab843281 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Tue, 10 Dec 2019 14:14:57 +0000 Subject: [PATCH 17/25] Add more tests --- .../FilterPathTests.cs | 2 + .../GitExtensionsTests.cs | 88 ++++++++++++++++++- src/NerdBank.GitVersioning/FilterPath.cs | 5 ++ src/NerdBank.GitVersioning/GitExtensions.cs | 14 ++- 4 files changed, 103 insertions(+), 6 deletions(-) diff --git a/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs b/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs index 489a8f13..4899ad6a 100644 --- a/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs +++ b/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs @@ -18,6 +18,8 @@ public class FilterPathTests [InlineData(":!/absolutepath.txt", "foo", "absolutepath.txt")] [InlineData(":!\\absolutepath.txt", "foo", "absolutepath.txt")] [InlineData("../bar/relativepath.txt", "foo", "bar/relativepath.txt")] + [InlineData("/", "foo", "")] + [InlineData("/absolute/file.txt", "foo", "absolute/file.txt")] [InlineData(":/", "foo", "")] [InlineData(":/absolutepath.txt", "foo", "absolutepath.txt")] [InlineData(":/bar/absolutepath.txt", "foo", "bar/absolutepath.txt")] diff --git a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs index 47748d75..ae758aee 100644 --- a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs +++ b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs @@ -133,11 +133,15 @@ public void GetVersionHeight_IntroducingFiltersIncrementsHeight() Assert.Equal(2, this.Repo.GetVersionHeight("some-sub-dir")); } - [Fact] - public void GetVersionHeight_IncludeFilter() + [Theory] + [InlineData("./")] + [InlineData("../some-sub-dir")] + [InlineData("/some-sub-dir")] + [InlineData(":/some-sub-dir")] + public void GetVersionHeight_IncludeFilter(string includeFilter) { var versionData = VersionOptions.FromVersion(new Version("1.2")); - versionData.PathFilters = new[] { "./" }; + versionData.PathFilters = new[] { includeFilter }; this.WriteVersionFile(versionData, "some-sub-dir"); Assert.Equal(1, this.Repo.GetVersionHeight("some-sub-dir")); @@ -189,6 +193,84 @@ public void GetVersionHeight_IncludeExcludeFilter() Assert.Equal(2, this.Repo.GetVersionHeight("some-sub-dir")); } + [Theory] + [InlineData(":^/excluded-dir")] + [InlineData(":^../excluded-dir")] + public void GetVersionHeight_AddingExcludeDoesNotLowerHeight(string excludePathFilter) + { + var versionData = VersionOptions.FromVersion(new Version("1.2")); + this.WriteVersionFile(versionData, "some-sub-dir"); + Assert.Equal(1, this.Repo.GetVersionHeight("some-sub-dir")); + + // Commit a file which will later be ignored + var ignoredFilePath = Path.Combine(this.RepoPath, "excluded-dir", "ignore.txt"); + Directory.CreateDirectory(Path.GetDirectoryName(ignoredFilePath)); + File.WriteAllText(ignoredFilePath, "hello"); + Commands.Stage(this.Repo, ignoredFilePath); + this.Repo.Commit("Add file which will later be excluded", this.Signer, this.Signer); + Assert.Equal(2, this.Repo.GetVersionHeight("some-sub-dir")); + + versionData.PathFilters = new[] { excludePathFilter }; + this.WriteVersionFile(versionData, "some-sub-dir"); + Assert.Equal(3, this.Repo.GetVersionHeight("some-sub-dir")); + + // Committing a change to an ignored file does not increment the version height + File.WriteAllText(ignoredFilePath, "changed"); + Commands.Stage(this.Repo, ignoredFilePath); + this.Repo.Commit("Change now excluded file", this.Signer, this.Signer); + Assert.Equal(3, this.Repo.GetVersionHeight("some-sub-dir")); + } + + [Fact] + public void GetVersionHeight_IncludeRoot() + { + var versionData = VersionOptions.FromVersion(new Version("1.2")); + versionData.PathFilters = new[] { ":/" }; + this.WriteVersionFile(versionData, "some-sub-dir"); + Assert.Equal(1, this.Repo.GetVersionHeight("some-sub-dir")); + + // Expect commit outside of project tree to affect version height + var otherFilePath = Path.Combine(this.RepoPath, "my-file.txt"); + File.WriteAllText(otherFilePath, "hello"); + Commands.Stage(this.Repo, otherFilePath); + this.Repo.Commit("Add other file outside of project root", this.Signer, this.Signer); + Assert.Equal(2, this.Repo.GetVersionHeight("some-sub-dir")); + + // Expect commit inside project tree to affect version height + var containedFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "another-file.txt"); + File.WriteAllText(containedFilePath, "hello"); + Commands.Stage(this.Repo, containedFilePath); + this.Repo.Commit("Add file within project root", this.Signer, this.Signer); + Assert.Equal(3, this.Repo.GetVersionHeight("some-sub-dir")); + } + + [Fact] + public void GetVersionHeight_IncludeRootExcludeSome() + { + var versionData = VersionOptions.FromVersion(new Version("1.2")); + versionData.PathFilters = new[] { ":/", ":^/excluded-dir" }; + this.WriteVersionFile(versionData, "some-sub-dir"); + Assert.Equal(1, this.Repo.GetVersionHeight("some-sub-dir")); + + // Expect commit in an excluded directory to not affect version height + var ignoredFilePath = Path.Combine(this.RepoPath, "excluded-dir", "my-file.txt"); + Directory.CreateDirectory(Path.GetDirectoryName(ignoredFilePath)); + File.WriteAllText(ignoredFilePath, "hello"); + Commands.Stage(this.Repo, ignoredFilePath); + this.Repo.Commit("Add other file to excluded directory", this.Signer, this.Signer); + Assert.Equal(1, this.Repo.GetVersionHeight("some-sub-dir")); + + // Expect commit within another directory to affect version height + var otherFilePath = Path.Combine(this.RepoPath, "another-dir", "another-file.txt"); + Directory.CreateDirectory(Path.GetDirectoryName(otherFilePath)); + File.WriteAllText(otherFilePath, "hello"); + Commands.Stage(this.Repo, otherFilePath); + this.Repo.Commit("Add file within project root", this.Signer, this.Signer); + Assert.Equal(2, this.Repo.GetVersionHeight("some-sub-dir")); + } + + // TODO: test excluding root + [Theory] [InlineData("2.2-alpha", "2.2-rc", false)] [InlineData("2.2-rc", "2.2", false)] diff --git a/src/NerdBank.GitVersioning/FilterPath.cs b/src/NerdBank.GitVersioning/FilterPath.cs index 4af1a61b..171a048a 100644 --- a/src/NerdBank.GitVersioning/FilterPath.cs +++ b/src/NerdBank.GitVersioning/FilterPath.cs @@ -25,6 +25,11 @@ public class FilterPath /// public string RepoRelativePath { get; } + /// + /// True if this represents the root of the repository. + /// + public bool IsRoot => this.RepoRelativePath == ""; + /// /// Parses a pathspec-like string into a root-relative path. /// diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index 0c8e2b2e..fa57f76a 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -732,17 +732,25 @@ private static int GetCommitHeight(Commit commit, string repoRelativeProjectDire height = 1; - if (includePaths != null && excludePaths != null) + if (includePaths != null) { + // If there are no include paths, or any of the include + // paths refer to the root of the repository, then do not + // filter the diff at all. + var diffInclude = + includePaths.Count == 0 || pathFilters.Any(filter => filter.IsRoot) + ? null + : includePaths; + // If the diff between this commit and any of its parents // does not touch a path that we care about, don't bump the // height. var relevantCommit = commit.Parents.Any() ? commit.Parents.Any(parent => ContainsRelevantChanges(commit.GetRepository().Diff - .Compare(parent.Tree, commit.Tree, includePaths))) + .Compare(parent.Tree, commit.Tree, diffInclude))) : ContainsRelevantChanges(commit.GetRepository().Diff - .Compare(null, commit.Tree, includePaths)); + .Compare(null, commit.Tree, diffInclude)); if (!relevantCommit) { From 9469c88fdfa0d37ca5601ea5145becf27a09bec5 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Tue, 10 Dec 2019 15:00:24 +0000 Subject: [PATCH 18/25] Add more tests --- .../FilterPathTests.cs | 1 - .../GitExtensionsTests.cs | 124 +++++++++++++++++- src/NerdBank.GitVersioning/GitExtensions.cs | 2 +- 3 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs b/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs index 4899ad6a..c2e3525e 100644 --- a/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs +++ b/src/NerdBank.GitVersioning.Tests/FilterPathTests.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using Nerdbank.GitVersioning; using Xunit; diff --git a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs index ae758aee..e88f0f8c 100644 --- a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs +++ b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs @@ -193,6 +193,40 @@ public void GetVersionHeight_IncludeExcludeFilter() Assert.Equal(2, this.Repo.GetVersionHeight("some-sub-dir")); } + [Fact] + public void GetVersionHeight_IncludeExcludeFilter_NoProjectDirectory() + { + var versionData = VersionOptions.FromVersion(new Version("1.2")); + versionData.PathFilters = new[] { "./", ":^/some-sub-dir/ignore.txt", ":^/excluded-dir" }; + this.WriteVersionFile(versionData); + Assert.Equal(1, this.Repo.GetVersionHeight()); + + // Commit touching excluded path does not affect version height + var ignoredFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "ignore.txt"); + Directory.CreateDirectory(Path.GetDirectoryName(ignoredFilePath)); + File.WriteAllText(ignoredFilePath, "hello"); + Commands.Stage(this.Repo, ignoredFilePath); + this.Repo.Commit("Add excluded file", this.Signer, this.Signer); + Assert.Equal(1, this.Repo.GetVersionHeight()); + + // Commit touching both excluded and included path does affect height + var includedFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "another-file.txt"); + File.WriteAllText(includedFilePath, "hello"); + File.WriteAllText(ignoredFilePath, "changed"); + Commands.Stage(this.Repo, includedFilePath); + Commands.Stage(this.Repo, ignoredFilePath); + this.Repo.Commit("Change both excluded and included file", this.Signer, this.Signer); + Assert.Equal(2, this.Repo.GetVersionHeight()); + + // Commit touching excluded directory does not affect version height + var fileInExcludedDirPath = Path.Combine(this.RepoPath, "excluded-dir", "ignore.txt"); + Directory.CreateDirectory(Path.GetDirectoryName(fileInExcludedDirPath)); + File.WriteAllText(fileInExcludedDirPath, "hello"); + Commands.Stage(this.Repo, fileInExcludedDirPath); + this.Repo.Commit("Add file to excluded dir", this.Signer, this.Signer); + Assert.Equal(2, this.Repo.GetVersionHeight()); + } + [Theory] [InlineData(":^/excluded-dir")] [InlineData(":^../excluded-dir")] @@ -269,7 +303,95 @@ public void GetVersionHeight_IncludeRootExcludeSome() Assert.Equal(2, this.Repo.GetVersionHeight("some-sub-dir")); } - // TODO: test excluding root + [Fact] + public void GetVersionHeight_ProjectDirectoryIsMoved() + { + var versionData = VersionOptions.FromVersion(new Version("1.2")); + versionData.PathFilters = new[] { "./", ":^/some-sub-dir/ignore.txt", ":^excluded-dir" }; + this.WriteVersionFile(versionData, "some-sub-dir"); + Assert.Equal(1, this.Repo.GetVersionHeight("some-sub-dir")); + + // Commit touching excluded path does not affect version height + var ignoredFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "ignore.txt"); + File.WriteAllText(ignoredFilePath, "hello"); + Commands.Stage(this.Repo, ignoredFilePath); + this.Repo.Commit("Add excluded file", this.Signer, this.Signer); + Assert.Equal(1, this.Repo.GetVersionHeight("some-sub-dir")); + + // Commit touching both excluded and included path does affect height + var includedFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "another-file.txt"); + File.WriteAllText(includedFilePath, "hello"); + File.WriteAllText(ignoredFilePath, "changed"); + Commands.Stage(this.Repo, includedFilePath); + Commands.Stage(this.Repo, ignoredFilePath); + this.Repo.Commit("Change both excluded and included file", this.Signer, this.Signer); + Assert.Equal(2, this.Repo.GetVersionHeight("some-sub-dir")); + + // Commit touching excluded directory does not affect version height + var fileInExcludedDirPath = Path.Combine(this.RepoPath, "some-sub-dir", "excluded-dir", "ignore.txt"); + Directory.CreateDirectory(Path.GetDirectoryName(fileInExcludedDirPath)); + File.WriteAllText(fileInExcludedDirPath, "hello"); + Commands.Stage(this.Repo, fileInExcludedDirPath); + this.Repo.Commit("Add file to excluded dir", this.Signer, this.Signer); + Assert.Equal(2, this.Repo.GetVersionHeight("some-sub-dir")); + + // Rename the project directory + Directory.Move(Path.Combine(this.RepoPath, "some-sub-dir"), Path.Combine(this.RepoPath, "new-project-dir")); + Commands.Stage(this.Repo, "some-sub-dir"); + Commands.Stage(this.Repo, "new-project-dir"); + this.Repo.Commit("Move project directory", this.Signer, this.Signer); + + // Version is reset as project directory cannot be find in the ancestor commit + Assert.Equal(1, this.Repo.GetVersionHeight("new-project-dir")); + } + + [Fact] + public void GetCommitsFromVersion_WithPathFilters() + { + var commitsAt121 = new List(); + var commitsAt122 = new List(); + var commitsAt123 = new List(); + + var versionData = VersionOptions.FromVersion(new Version("1.2")); + versionData.PathFilters = new[] { "./", ":^/some-sub-dir/ignore.txt", ":^excluded-dir" }; + commitsAt121.Add(this.WriteVersionFile(versionData, "some-sub-dir")); + + // Commit touching excluded path does not affect version height + var ignoredFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "ignore.txt"); + File.WriteAllText(ignoredFilePath, "hello"); + Commands.Stage(this.Repo, ignoredFilePath); + commitsAt121.Add(this.Repo.Commit("Add excluded file", this.Signer, this.Signer)); + + // Commit touching both excluded and included path does affect height + var includedFilePath = Path.Combine(this.RepoPath, "some-sub-dir", "another-file.txt"); + File.WriteAllText(includedFilePath, "hello"); + File.WriteAllText(ignoredFilePath, "changed"); + Commands.Stage(this.Repo, includedFilePath); + Commands.Stage(this.Repo, ignoredFilePath); + commitsAt122.Add(this.Repo.Commit("Change both excluded and included file", this.Signer, this.Signer)); + + // Commit touching excluded directory does not affect version height + var fileInExcludedDirPath = Path.Combine(this.RepoPath, "some-sub-dir", "excluded-dir", "ignore.txt"); + Directory.CreateDirectory(Path.GetDirectoryName(fileInExcludedDirPath)); + File.WriteAllText(fileInExcludedDirPath, "hello"); + Commands.Stage(this.Repo, fileInExcludedDirPath); + commitsAt122.Add(this.Repo.Commit("Add file to excluded dir", this.Signer, this.Signer)); + + // Commit touching project directory affects version height + File.WriteAllText(includedFilePath, "more changes"); + Commands.Stage(this.Repo, includedFilePath); + commitsAt123.Add(this.Repo.Commit("Changed included file", this.Signer, this.Signer)); + + Assert.Equal( + commitsAt121.OrderBy(c => c.Sha), + this.Repo.GetCommitsFromVersion(new Version(1, 2, 1), "some-sub-dir").OrderBy(c => c.Sha)); + Assert.Equal( + commitsAt122.OrderBy(c => c.Sha), + this.Repo.GetCommitsFromVersion(new Version(1, 2, 2), "some-sub-dir").OrderBy(c => c.Sha)); + Assert.Equal( + commitsAt123.OrderBy(c => c.Sha), + this.Repo.GetCommitsFromVersion(new Version(1, 2, 3), "some-sub-dir").OrderBy(c => c.Sha)); + } [Theory] [InlineData("2.2-alpha", "2.2-rc", false)] diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index fa57f76a..59271e17 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -712,7 +712,7 @@ private static int GetCommitHeight(Commit commit, string repoRelativeProjectDire { if (continueStepping == null || continueStepping(commit)) { - var versionOptions = repoRelativeProjectDirectory != null ? VersionFile.GetVersion(commit, repoRelativeProjectDirectory) : null; + var versionOptions = VersionFile.GetVersion(commit, repoRelativeProjectDirectory); var pathFilters = versionOptions != null ? FilterPath.FromVersionOptions(versionOptions, repoRelativeProjectDirectory, commit.GetRepository()) : null; var includePaths = From ca081cf27e477138e7bb8376d1b2aeffd3c71110 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 21 Dec 2019 10:23:52 -0700 Subject: [PATCH 19/25] Make FilterPath internal --- .../NerdBank.GitVersioning.Tests.csproj | 1 + src/NerdBank.GitVersioning/FilterPath.cs | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj b/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj index 33823156..24c761b3 100644 --- a/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj +++ b/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/src/NerdBank.GitVersioning/FilterPath.cs b/src/NerdBank.GitVersioning/FilterPath.cs index 171a048a..9e775040 100644 --- a/src/NerdBank.GitVersioning/FilterPath.cs +++ b/src/NerdBank.GitVersioning/FilterPath.cs @@ -10,25 +10,25 @@ namespace Nerdbank.GitVersioning /// /// A filter (include or exclude) representing a repo relative path. /// - public class FilterPath + internal class FilterPath { private readonly StringComparison stringComparison; /// /// True if this represents an exclude filter. /// - public bool IsExclude { get; } + internal bool IsExclude { get; } /// /// Path relative to the repository root that this represents. /// Slashes are canonical for this OS. /// - public string RepoRelativePath { get; } + internal string RepoRelativePath { get; } /// /// True if this represents the root of the repository. /// - public bool IsRoot => this.RepoRelativePath == ""; + internal bool IsRoot => this.RepoRelativePath == ""; /// /// Parses a pathspec-like string into a root-relative path. @@ -107,7 +107,7 @@ private static string ParsePath(string path, string relativeTo) /// /// Whether case should be ignored by /// Invalid path spec. - public FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false) + internal FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false) { Requires.NotNullOrEmpty(pathSpec, nameof(pathSpec)); @@ -155,7 +155,7 @@ public FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false) /// null if no path filters are set. Otherwise, returns a list of /// instances. /// - public static IReadOnlyList FromVersionOptions(VersionOptions versionOptions, + internal static IReadOnlyList FromVersionOptions(VersionOptions versionOptions, string relativeRepoProjectDirectory, IRepository repository) { @@ -177,7 +177,7 @@ public FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false) /// True if this is an excluding filter that matches /// , otherwise false. /// - public bool Excludes(string repoRelativePath) + internal bool Excludes(string repoRelativePath) { if (repoRelativePath is null) throw new ArgumentNullException(nameof(repoRelativePath)); From 59335a766a0469980f0af836878a6021cb45ee5c Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Fri, 3 Jan 2020 10:57:57 +0000 Subject: [PATCH 20/25] Add cache to VersionFile.GetVersion --- src/NerdBank.GitVersioning/VersionFile.cs | 29 +++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/NerdBank.GitVersioning/VersionFile.cs b/src/NerdBank.GitVersioning/VersionFile.cs index 961322a6..4b89a821 100644 --- a/src/NerdBank.GitVersioning/VersionFile.cs +++ b/src/NerdBank.GitVersioning/VersionFile.cs @@ -31,12 +31,24 @@ public static class VersionFile public static readonly IReadOnlyList PreferredFileNames = new[] { JsonFileName, TxtFileName }; /// - /// Reads the version.txt file and returns the and prerelease tag from it. + /// Reads the version.{txt,json} file and returns the for it. /// /// The commit to read the version file from. /// The directory to consider when searching for the version.txt file. /// The version information read from the file. public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoRelativeProjectDirectory = null) + { + return GetVersion(commit, repoRelativeProjectDirectory, null); + } + + /// + /// Reads the version.{txt,json} file and returns the for it. + /// + /// The commit to read the version file from. + /// The directory to consider when searching for the version.txt file. + /// + /// The version information read from the file. + public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoRelativeProjectDirectory, Dictionary cache = null) { if (commit == null) { @@ -52,9 +64,15 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR var versionTxtBlob = commit.Tree[candidatePath]?.Target as LibGit2Sharp.Blob; if (versionTxtBlob != null) { + if (cache != null && cache.TryGetValue(versionTxtBlob, out var cachedOptions)) + { + return cachedOptions; + } + var result = TryReadVersionFile(new StreamReader(versionTxtBlob.GetContentStream()), isJsonFile: false); if (result != null) { + cache?.Add(versionTxtBlob, result); return result; } } @@ -63,6 +81,11 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR var versionJsonBlob = commit.Tree[candidatePath]?.Target as LibGit2Sharp.Blob; if (versionJsonBlob != null) { + if (cache != null && cache.TryGetValue(versionJsonBlob, out var cachedOptions)) + { + return cachedOptions; + } + string versionJsonContent; using (var sr = new StreamReader(versionJsonBlob.GetContentStream())) { @@ -86,10 +109,11 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR { if (parentDirectory != null) { - result = GetVersion(commit, parentDirectory); + result = GetVersion(commit, parentDirectory, cache); if (result != null) { JsonConvert.PopulateObject(versionJsonContent, result, VersionOptions.GetJsonSettings()); + cache?.Add(versionJsonBlob, result); return result; } } @@ -98,6 +122,7 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR } else if (result != null) { + cache?.Add(versionJsonBlob, result); return result; } } From 187343667fc2df46cbef27d45ace59f0eaab1026 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Fri, 3 Jan 2020 11:16:38 +0000 Subject: [PATCH 21/25] Add caching to GetHeight --- src/NerdBank.GitVersioning/GitExtensions.cs | 50 ++++++++++++++------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index 568fc937..bfc9bb8a 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -58,7 +58,7 @@ public static int GetVersionHeight(this Commit commit, string repoRelativeProjec var versionHeightPosition = versionOptions.VersionHeightPosition; if (versionHeightPosition.HasValue) { - int height = commit.GetHeight(repoRelativeProjectDirectory, c => CommitMatchesVersion(c, baseSemVer, versionHeightPosition.Value, repoRelativeProjectDirectory)); + int height = commit.GetHeight(repoRelativeProjectDirectory, (c, v) => VersionOptionsMatchesVersion(v, baseSemVer, versionHeightPosition.Value)); return height; } @@ -140,11 +140,30 @@ public static int GetHeight(this Commit commit, Func continueStepp /// /// The height of the commit. Always a positive integer. public static int GetHeight(this Commit commit, string repoRelativeProjectDirectory, Func continueStepping = null) + { + return commit.GetHeight(repoRelativeProjectDirectory, + continueStepping == null ? default(Func) : (c, v) => continueStepping(c)); + } + + /// + /// Gets the number of commits in the longest single path between + /// the specified commit and the most distant ancestor (inclusive). + /// + /// The commit to measure the height of. + /// The path to the directory of the project whose version is being queried, relative to the repo root. + /// + /// A function that returns false when we reach a commit that + /// should not be included in the height calculation. + /// May be null to count the height to the original commit. + /// + /// The height of the commit. Always a positive integer. + private static int GetHeight(this Commit commit, string repoRelativeProjectDirectory, Func continueStepping = null) { Requires.NotNull(commit, nameof(commit)); var heights = new Dictionary(); - return GetCommitHeight(commit, repoRelativeProjectDirectory, heights, continueStepping); + var versionFileCache = new Dictionary(); + return GetCommitHeight(commit, repoRelativeProjectDirectory, heights, versionFileCache, continueStepping); } /// @@ -472,20 +491,16 @@ public static Repository OpenGitRepo(string pathUnderGitRepo, bool useDefaultCon } /// - /// Tests whether a commit is of a specified version, comparing major and minor components - /// with the version.txt file defined by that commit. + /// Tests whether a is of a specified version, comparing components up to a precision specified by . /// - /// The commit to test. - /// The version to test for in the commit + /// Version data for a given commit + /// The version to test for against /// The last component of the version to include in the comparison. - /// The repo-relative directory from which was originally calculated. - /// true if the matches the major and minor components of . - internal static bool CommitMatchesVersion(this Commit commit, SemanticVersion expectedVersion, SemanticVersion.Position comparisonPrecision, string repoRelativeProjectDirectory) + /// true if the matches the components (up to ) of . + private static bool VersionOptionsMatchesVersion(VersionOptions commitVersionData, SemanticVersion expectedVersion, SemanticVersion.Position comparisonPrecision) { - Requires.NotNull(commit, nameof(commit)); Requires.NotNull(expectedVersion, nameof(expectedVersion)); - var commitVersionData = VersionFile.GetVersion(commit, repoRelativeProjectDirectory); var semVerFromFile = commitVersionData?.Version; if (semVerFromFile == null) { @@ -525,7 +540,7 @@ internal static bool CommitMatchesVersion(this Commit commit, SemanticVersion ex /// The last component of the version to include in the comparison. /// The repo-relative directory from which was originally calculated. /// true if the matches the major and minor components of . - internal static bool CommitMatchesVersion(this Commit commit, Version expectedVersion, SemanticVersion.Position comparisonPrecision, string repoRelativeProjectDirectory) + private static bool CommitMatchesVersion(this Commit commit, Version expectedVersion, SemanticVersion.Position comparisonPrecision, string repoRelativeProjectDirectory) { Requires.NotNull(commit, nameof(commit)); Requires.NotNull(expectedVersion, nameof(expectedVersion)); @@ -697,22 +712,25 @@ private static string EncodeAsHex(byte[] buffer) /// The commit to measure the height of. /// The path to the directory of the project whose height is being queried, relative to the repo root. /// A cache of commits and their heights. + /// A cache of version file blobs to /// /// A function that returns false when we reach a commit that /// should not be included in the height calculation. /// May be null to count the height to the original commit. /// /// The height of the branch. - private static int GetCommitHeight(Commit commit, string repoRelativeProjectDirectory, Dictionary heights, Func continueStepping) + private static int GetCommitHeight(Commit commit, string repoRelativeProjectDirectory, Dictionary heights, Dictionary versionFileCache, Func continueStepping) { Requires.NotNull(commit, nameof(commit)); Requires.NotNull(heights, nameof(heights)); + Requires.NotNull(versionFileCache, nameof(versionFileCache)); if (!heights.TryGetValue(commit.Id, out int height)) { - if (continueStepping == null || continueStepping(commit)) + var versionOptions = VersionFile.GetVersion(commit, repoRelativeProjectDirectory, versionFileCache); + + if (continueStepping == null || continueStepping(commit, versionOptions)) { - var versionOptions = VersionFile.GetVersion(commit, repoRelativeProjectDirectory); var pathFilters = versionOptions != null ? FilterPath.FromVersionOptions(versionOptions, repoRelativeProjectDirectory, commit.GetRepository()) : null; var includePaths = @@ -760,7 +778,7 @@ private static int GetCommitHeight(Commit commit, string repoRelativeProjectDire if (commit.Parents.Any()) { - height += commit.Parents.Max(p => GetCommitHeight(p, repoRelativeProjectDirectory, heights, continueStepping)); + height += commit.Parents.Max(p => GetCommitHeight(p, repoRelativeProjectDirectory, heights, versionFileCache, continueStepping)); } } else From 1beaf12f5c3fa2cd47e0e5ee0b49ebc1a0b0e314 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Fri, 3 Jan 2020 16:11:24 +0000 Subject: [PATCH 22/25] Fix caching, add ICloneable to VersionFile classes --- src/NerdBank.GitVersioning/SemanticVersion.cs | 8 +- src/NerdBank.GitVersioning/VersionFile.cs | 24 ++++-- src/NerdBank.GitVersioning/VersionOptions.cs | 80 +++++++++++++++++-- 3 files changed, 99 insertions(+), 13 deletions(-) diff --git a/src/NerdBank.GitVersioning/SemanticVersion.cs b/src/NerdBank.GitVersioning/SemanticVersion.cs index 54a525d4..66b560c8 100644 --- a/src/NerdBank.GitVersioning/SemanticVersion.cs +++ b/src/NerdBank.GitVersioning/SemanticVersion.cs @@ -9,7 +9,7 @@ /// Describes a version with an optional unstable tag. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] - public class SemanticVersion : IEquatable + public class SemanticVersion : IEquatable, ICloneable { /// /// The regular expression with capture groups for semantic versioning. @@ -206,6 +206,12 @@ public override string ToString() return this.Version + this.Prerelease + this.BuildMetadata; } + /// + /// Clones this object. + /// + /// A deep clone of this object. + public object Clone() => this.MemberwiseClone(); + /// /// Checks equality against another instance of this class. /// diff --git a/src/NerdBank.GitVersioning/VersionFile.cs b/src/NerdBank.GitVersioning/VersionFile.cs index 4b89a821..69f05a15 100644 --- a/src/NerdBank.GitVersioning/VersionFile.cs +++ b/src/NerdBank.GitVersioning/VersionFile.cs @@ -66,13 +66,18 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR { if (cache != null && cache.TryGetValue(versionTxtBlob, out var cachedOptions)) { - return cachedOptions; + return (VersionOptions)cachedOptions.Clone(); } var result = TryReadVersionFile(new StreamReader(versionTxtBlob.GetContentStream()), isJsonFile: false); if (result != null) { - cache?.Add(versionTxtBlob, result); + if (cache != null) + { + cache.Add(versionTxtBlob, result); + return (VersionOptions) result.Clone(); + } + return result; } } @@ -83,7 +88,7 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR { if (cache != null && cache.TryGetValue(versionJsonBlob, out var cachedOptions)) { - return cachedOptions; + return (VersionOptions)cachedOptions.Clone(); } string versionJsonContent; @@ -113,7 +118,11 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR if (result != null) { JsonConvert.PopulateObject(versionJsonContent, result, VersionOptions.GetJsonSettings()); - cache?.Add(versionJsonBlob, result); + + // We can't cache VersionOptions that use inheritance. + // The file blob will still be the same even if an ancestor + // version.json has changed. + return result; } } @@ -122,7 +131,12 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR } else if (result != null) { - cache?.Add(versionJsonBlob, result); + if (cache != null) + { + cache.Add(versionJsonBlob, result); + return (VersionOptions) result.Clone(); + } + return result; } } diff --git a/src/NerdBank.GitVersioning/VersionOptions.cs b/src/NerdBank.GitVersioning/VersionOptions.cs index aafe0685..5bf90ded 100644 --- a/src/NerdBank.GitVersioning/VersionOptions.cs +++ b/src/NerdBank.GitVersioning/VersionOptions.cs @@ -13,7 +13,7 @@ /// Describes the various versions and options required for the build. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] - public class VersionOptions : IEquatable + public class VersionOptions : IEquatable, ICloneable { /// /// Default value for . @@ -341,6 +341,22 @@ public override bool Equals(object obj) /// The hash code. public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); + /// + /// Clones this object. + /// + /// A deep clone of this object. + public object Clone() + { + var newVersionOptions = (VersionOptions) this.MemberwiseClone(); + newVersionOptions.Version = (SemanticVersion) newVersionOptions.Version?.Clone(); + newVersionOptions.AssemblyVersion = (AssemblyVersionOptions) newVersionOptions.AssemblyVersion?.Clone(); + newVersionOptions.NuGetPackageVersion = (NuGetPackageVersionOptions) newVersionOptions.NuGetPackageVersion?.Clone(); + newVersionOptions.PublicReleaseRefSpec = (string[]) newVersionOptions.PublicReleaseRefSpec?.Clone(); + newVersionOptions.CloudBuild = (CloudBuildOptions) newVersionOptions.CloudBuild?.Clone(); + newVersionOptions.Release = (ReleaseOptions) newVersionOptions.Release?.Clone(); + return newVersionOptions; + } + /// /// Checks equality against another instance of this class. /// @@ -368,7 +384,7 @@ internal bool IsDefaultVersionTheOnlyPropertySet /// /// The class that contains settings for the property. /// - public class NuGetPackageVersionOptions : IEquatable + public class NuGetPackageVersionOptions : IEquatable, ICloneable { /// /// The default (uninitialized) instance. @@ -425,6 +441,12 @@ protected NuGetPackageVersionOptions(bool isReadOnly) /// public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); + /// + /// Clones this object. + /// + /// A deep clone of this object. + public object Clone() => this.isReadOnly ? this : this.MemberwiseClone(); + /// /// Gets a value indicating whether this instance is equivalent to the default instance. /// @@ -475,7 +497,7 @@ public int GetHashCode(NuGetPackageVersionOptions obj) /// /// Describes the details of how the AssemblyVersion value will be calculated. /// - public class AssemblyVersionOptions : IEquatable + public class AssemblyVersionOptions : IEquatable, ICloneable { /// /// The default (uninitialized) instance. @@ -557,6 +579,12 @@ public Version Version /// public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); + /// + /// Clones this object. + /// + /// A deep clone of this object. + public object Clone() => this.isReadOnly ? this : this.MemberwiseClone(); + /// /// Gets a value indicating whether this instance is equivalent to the default instance. /// @@ -608,7 +636,7 @@ public int GetHashCode(AssemblyVersionOptions obj) /// /// Options that are applicable specifically to cloud builds (e.g. VSTS, AppVeyor, TeamCity) /// - public class CloudBuildOptions : IEquatable + public class CloudBuildOptions : IEquatable, ICloneable { /// /// The default (uninitialized) instance. @@ -701,6 +729,21 @@ public CloudBuildNumberOptions BuildNumber /// public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); + /// + /// Clones this object. + /// + /// A deep clone of this object. + public object Clone() + { + if (this.isReadOnly) + { + return this; + } + var newCbo = (CloudBuildOptions)this.MemberwiseClone(); + newCbo.BuildNumber = (CloudBuildNumberOptions) newCbo.BuildNumber?.Clone(); + return newCbo; + } + /// /// Gets a value indicating whether this instance is equivalent to the default instance. /// @@ -755,7 +798,7 @@ public int GetHashCode(CloudBuildOptions obj) /// /// Override the build number preset by the cloud build with one enriched with version information. /// - public class CloudBuildNumberOptions : IEquatable + public class CloudBuildNumberOptions : IEquatable, ICloneable { /// /// The default (uninitialized) instance. @@ -822,6 +865,17 @@ protected CloudBuildNumberOptions(bool isReadOnly) /// public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); + /// + /// Clones this object. + /// + /// A deep clone of this object. + public object Clone() + { + var newCbno = (CloudBuildNumberOptions)this.MemberwiseClone(); + newCbno.IncludeCommitId = (CloudBuildNumberCommitIdOptions) newCbno.IncludeCommitId?.Clone(); + return newCbno; + } + /// /// Gets a value indicating whether this instance is equivalent to the default instance. /// @@ -874,7 +928,7 @@ public int GetHashCode(CloudBuildNumberOptions obj) /// /// Describes when and where to include information about the git commit being built. /// - public class CloudBuildNumberCommitIdOptions : IEquatable + public class CloudBuildNumberCommitIdOptions : IEquatable, ICloneable { /// /// The default (uninitialized) instance. @@ -948,6 +1002,12 @@ protected CloudBuildNumberCommitIdOptions(bool isReadOnly) /// public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); + /// + /// Clones this object. + /// + /// A deep clone of this object. + public object Clone() => this.isReadOnly ? this : this.MemberwiseClone(); + /// /// Gets a value indicating whether this instance is equivalent to the default instance. /// @@ -1096,7 +1156,7 @@ public enum CloudBuildNumberCommitWhere /// /// Encapsulates settings for the "prepare-release" command /// - public class ReleaseOptions : IEquatable + public class ReleaseOptions : IEquatable, ICloneable { /// /// The default (uninitialized) instance. @@ -1193,6 +1253,12 @@ public string FirstUnstableTag /// public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); + /// + /// Clones this object. + /// + /// A deep clone of this object. + public object Clone() => this.isReadOnly ? this : this.MemberwiseClone(); + /// /// Gets a value indicating whether this instance is equivalent to the default instance. /// From c3d28c0fc353b0e506b01f9f6eadd248906fb3c6 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sun, 19 Jan 2020 05:46:13 -0700 Subject: [PATCH 23/25] Revise how VersionOptions is cached --- src/NerdBank.GitVersioning/GitExtensions.cs | 108 ++++++++++-------- src/NerdBank.GitVersioning/SemanticVersion.cs | 8 +- src/NerdBank.GitVersioning/VersionFile.cs | 43 +------ src/NerdBank.GitVersioning/VersionOptions.cs | 84 ++------------ 4 files changed, 72 insertions(+), 171 deletions(-) diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index bfc9bb8a..b31a4053 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -45,7 +45,9 @@ public static int GetVersionHeight(this Commit commit, string repoRelativeProjec Requires.NotNull(commit, nameof(commit)); Requires.Argument(repoRelativeProjectDirectory == null || !Path.IsPathRooted(repoRelativeProjectDirectory), nameof(repoRelativeProjectDirectory), "Path should be relative to repo root."); - var versionOptions = VersionFile.GetVersion(commit, repoRelativeProjectDirectory); + var tracker = new GitWalkTracker(repoRelativeProjectDirectory); + + var versionOptions = tracker.GetVersion(commit); if (versionOptions == null) { return 0; @@ -58,7 +60,7 @@ public static int GetVersionHeight(this Commit commit, string repoRelativeProjec var versionHeightPosition = versionOptions.VersionHeightPosition; if (versionHeightPosition.HasValue) { - int height = commit.GetHeight(repoRelativeProjectDirectory, (c, v) => VersionOptionsMatchesVersion(v, baseSemVer, versionHeightPosition.Value)); + int height = commit.GetHeight(repoRelativeProjectDirectory, c => CommitMatchesVersion(c, baseSemVer, versionHeightPosition.Value, tracker)); return height; } @@ -140,30 +142,11 @@ public static int GetHeight(this Commit commit, Func continueStepp /// /// The height of the commit. Always a positive integer. public static int GetHeight(this Commit commit, string repoRelativeProjectDirectory, Func continueStepping = null) - { - return commit.GetHeight(repoRelativeProjectDirectory, - continueStepping == null ? default(Func) : (c, v) => continueStepping(c)); - } - - /// - /// Gets the number of commits in the longest single path between - /// the specified commit and the most distant ancestor (inclusive). - /// - /// The commit to measure the height of. - /// The path to the directory of the project whose version is being queried, relative to the repo root. - /// - /// A function that returns false when we reach a commit that - /// should not be included in the height calculation. - /// May be null to count the height to the original commit. - /// - /// The height of the commit. Always a positive integer. - private static int GetHeight(this Commit commit, string repoRelativeProjectDirectory, Func continueStepping = null) { Requires.NotNull(commit, nameof(commit)); - var heights = new Dictionary(); - var versionFileCache = new Dictionary(); - return GetCommitHeight(commit, repoRelativeProjectDirectory, heights, versionFileCache, continueStepping); + var tracker = new GitWalkTracker(repoRelativeProjectDirectory); + return GetCommitHeight(commit, tracker, continueStepping); } /// @@ -352,11 +335,12 @@ public static IEnumerable GetCommitsFromVersion(this Repository repo, Ve Requires.NotNull(repo, nameof(repo)); Requires.NotNull(version, nameof(version)); + var tracker = new GitWalkTracker(repoRelativeProjectDirectory); var possibleCommits = from commit in GetCommitsReachableFromRefs(repo).Distinct() - let commitVersionOptions = VersionFile.GetVersion(commit, repoRelativeProjectDirectory) + let commitVersionOptions = tracker.GetVersion(commit) where commitVersionOptions != null where !IsCommitIdMismatch(version, commitVersionOptions, commit) - where !IsVersionHeightMismatch(version, commitVersionOptions, commit, repoRelativeProjectDirectory) + where !IsVersionHeightMismatch(version, commitVersionOptions, commit, tracker) select commit; return possibleCommits; @@ -491,16 +475,20 @@ public static Repository OpenGitRepo(string pathUnderGitRepo, bool useDefaultCon } /// - /// Tests whether a is of a specified version, comparing components up to a precision specified by . + /// Tests whether a commit is of a specified version, comparing major and minor components + /// with the version.txt file defined by that commit. /// - /// Version data for a given commit - /// The version to test for against + /// The commit to test. + /// The version to test for in the commit /// The last component of the version to include in the comparison. - /// true if the matches the components (up to ) of . - private static bool VersionOptionsMatchesVersion(VersionOptions commitVersionData, SemanticVersion expectedVersion, SemanticVersion.Position comparisonPrecision) + /// The caching tracker for storing or fetching version information per commit. + /// true if the matches the major and minor components of . + private static bool CommitMatchesVersion(this Commit commit, SemanticVersion expectedVersion, SemanticVersion.Position comparisonPrecision, GitWalkTracker tracker) { + Requires.NotNull(commit, nameof(commit)); Requires.NotNull(expectedVersion, nameof(expectedVersion)); + var commitVersionData = tracker.GetVersion(commit); var semVerFromFile = commitVersionData?.Version; if (semVerFromFile == null) { @@ -538,14 +526,14 @@ private static bool VersionOptionsMatchesVersion(VersionOptions commitVersionDat /// The commit to test. /// The version to test for in the commit /// The last component of the version to include in the comparison. - /// The repo-relative directory from which was originally calculated. + /// The caching tracker for storing or fetching version information per commit. /// true if the matches the major and minor components of . - private static bool CommitMatchesVersion(this Commit commit, Version expectedVersion, SemanticVersion.Position comparisonPrecision, string repoRelativeProjectDirectory) + private static bool CommitMatchesVersion(this Commit commit, Version expectedVersion, SemanticVersion.Position comparisonPrecision, GitWalkTracker tracker) { Requires.NotNull(commit, nameof(commit)); Requires.NotNull(expectedVersion, nameof(expectedVersion)); - var commitVersionData = VersionFile.GetVersion(commit, repoRelativeProjectDirectory); + var commitVersionData = tracker.GetVersion(commit); var semVerFromFile = commitVersionData?.Version; if (semVerFromFile == null) { @@ -584,7 +572,7 @@ private static int ReadVersionPosition(Version version, SemanticVersion.Position } } - private static bool IsVersionHeightMismatch(Version version, VersionOptions versionOptions, Commit commit, string repoRelativeProjectDirectory) + private static bool IsVersionHeightMismatch(Version version, VersionOptions versionOptions, Commit commit, GitWalkTracker tracker) { Requires.NotNull(version, nameof(version)); Requires.NotNull(versionOptions, nameof(versionOptions)); @@ -597,7 +585,7 @@ private static bool IsVersionHeightMismatch(Version version, VersionOptions vers int expectedVersionHeight = ReadVersionPosition(version, position.Value); var actualVersionOffset = versionOptions.VersionHeightOffsetOrDefault; - var actualVersionHeight = commit.GetHeight(repoRelativeProjectDirectory, c => CommitMatchesVersion(c, version, position.Value - 1, repoRelativeProjectDirectory)); + var actualVersionHeight = GetCommitHeight(commit, tracker, c => CommitMatchesVersion(c, version, position.Value - 1, tracker)); return expectedVersionHeight != actualVersionHeight + actualVersionOffset; } @@ -710,28 +698,24 @@ private static string EncodeAsHex(byte[] buffer) /// the specified branch's head and the most distant ancestor (inclusive). /// /// The commit to measure the height of. - /// The path to the directory of the project whose height is being queried, relative to the repo root. - /// A cache of commits and their heights. - /// A cache of version file blobs to + /// The caching tracker for storing or fetching version information per commit. /// /// A function that returns false when we reach a commit that /// should not be included in the height calculation. /// May be null to count the height to the original commit. /// /// The height of the branch. - private static int GetCommitHeight(Commit commit, string repoRelativeProjectDirectory, Dictionary heights, Dictionary versionFileCache, Func continueStepping) + private static int GetCommitHeight(Commit commit, GitWalkTracker tracker, Func continueStepping) { Requires.NotNull(commit, nameof(commit)); - Requires.NotNull(heights, nameof(heights)); - Requires.NotNull(versionFileCache, nameof(versionFileCache)); + Requires.NotNull(tracker, nameof(tracker)); - if (!heights.TryGetValue(commit.Id, out int height)) + if (!tracker.TryGetVersionHeight(commit, out int height)) { - var versionOptions = VersionFile.GetVersion(commit, repoRelativeProjectDirectory, versionFileCache); - - if (continueStepping == null || continueStepping(commit, versionOptions)) + if (continueStepping == null || continueStepping(commit)) { - var pathFilters = versionOptions != null ? FilterPath.FromVersionOptions(versionOptions, repoRelativeProjectDirectory, commit.GetRepository()) : null; + var versionOptions = tracker.GetVersion(commit); + var pathFilters = versionOptions != null ? FilterPath.FromVersionOptions(versionOptions, tracker.RepoRelativeDirectory, commit.GetRepository()) : null; var includePaths = pathFilters @@ -778,7 +762,7 @@ private static int GetCommitHeight(Commit commit, string repoRelativeProjectDire if (commit.Parents.Any()) { - height += commit.Parents.Max(p => GetCommitHeight(p, repoRelativeProjectDirectory, heights, versionFileCache, continueStepping)); + height += commit.Parents.Max(p => GetCommitHeight(p, tracker, continueStepping)); } } else @@ -786,7 +770,7 @@ private static int GetCommitHeight(Commit commit, string repoRelativeProjectDire height = 0; } - heights[commit.Id] = height; + tracker.RecordHeight(commit, height); } return height; @@ -920,5 +904,33 @@ private static bool IsVersionFileChangedInWorkingCopy(Repository repo, string re workingCopyVersion = null; return false; } + + private class GitWalkTracker + { + private readonly Dictionary commitVersionCache = new Dictionary(); + private readonly Dictionary heights = new Dictionary(); + + internal GitWalkTracker(string repoRelativeDirectory) + { + this.RepoRelativeDirectory = repoRelativeDirectory; + } + + internal string RepoRelativeDirectory { get; } + + internal bool TryGetVersionHeight(Commit commit, out int height) => this.heights.TryGetValue(commit.Id, out height); + + internal void RecordHeight(Commit commit, int height) => this.heights.Add(commit.Id, height); + + internal VersionOptions GetVersion(Commit commit) + { + if (!this.commitVersionCache.TryGetValue(commit.Id, out VersionOptions options)) + { + options = VersionFile.GetVersion(commit, this.RepoRelativeDirectory); + this.commitVersionCache.Add(commit.Id, options); + } + + return options; + } + } } } diff --git a/src/NerdBank.GitVersioning/SemanticVersion.cs b/src/NerdBank.GitVersioning/SemanticVersion.cs index 66b560c8..54a525d4 100644 --- a/src/NerdBank.GitVersioning/SemanticVersion.cs +++ b/src/NerdBank.GitVersioning/SemanticVersion.cs @@ -9,7 +9,7 @@ /// Describes a version with an optional unstable tag. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] - public class SemanticVersion : IEquatable, ICloneable + public class SemanticVersion : IEquatable { /// /// The regular expression with capture groups for semantic versioning. @@ -206,12 +206,6 @@ public override string ToString() return this.Version + this.Prerelease + this.BuildMetadata; } - /// - /// Clones this object. - /// - /// A deep clone of this object. - public object Clone() => this.MemberwiseClone(); - /// /// Checks equality against another instance of this class. /// diff --git a/src/NerdBank.GitVersioning/VersionFile.cs b/src/NerdBank.GitVersioning/VersionFile.cs index 69f05a15..961322a6 100644 --- a/src/NerdBank.GitVersioning/VersionFile.cs +++ b/src/NerdBank.GitVersioning/VersionFile.cs @@ -31,24 +31,12 @@ public static class VersionFile public static readonly IReadOnlyList PreferredFileNames = new[] { JsonFileName, TxtFileName }; /// - /// Reads the version.{txt,json} file and returns the for it. + /// Reads the version.txt file and returns the and prerelease tag from it. /// /// The commit to read the version file from. /// The directory to consider when searching for the version.txt file. /// The version information read from the file. public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoRelativeProjectDirectory = null) - { - return GetVersion(commit, repoRelativeProjectDirectory, null); - } - - /// - /// Reads the version.{txt,json} file and returns the for it. - /// - /// The commit to read the version file from. - /// The directory to consider when searching for the version.txt file. - /// - /// The version information read from the file. - public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoRelativeProjectDirectory, Dictionary cache = null) { if (commit == null) { @@ -64,20 +52,9 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR var versionTxtBlob = commit.Tree[candidatePath]?.Target as LibGit2Sharp.Blob; if (versionTxtBlob != null) { - if (cache != null && cache.TryGetValue(versionTxtBlob, out var cachedOptions)) - { - return (VersionOptions)cachedOptions.Clone(); - } - var result = TryReadVersionFile(new StreamReader(versionTxtBlob.GetContentStream()), isJsonFile: false); if (result != null) { - if (cache != null) - { - cache.Add(versionTxtBlob, result); - return (VersionOptions) result.Clone(); - } - return result; } } @@ -86,11 +63,6 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR var versionJsonBlob = commit.Tree[candidatePath]?.Target as LibGit2Sharp.Blob; if (versionJsonBlob != null) { - if (cache != null && cache.TryGetValue(versionJsonBlob, out var cachedOptions)) - { - return (VersionOptions)cachedOptions.Clone(); - } - string versionJsonContent; using (var sr = new StreamReader(versionJsonBlob.GetContentStream())) { @@ -114,15 +86,10 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR { if (parentDirectory != null) { - result = GetVersion(commit, parentDirectory, cache); + result = GetVersion(commit, parentDirectory); if (result != null) { JsonConvert.PopulateObject(versionJsonContent, result, VersionOptions.GetJsonSettings()); - - // We can't cache VersionOptions that use inheritance. - // The file blob will still be the same even if an ancestor - // version.json has changed. - return result; } } @@ -131,12 +98,6 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR } else if (result != null) { - if (cache != null) - { - cache.Add(versionJsonBlob, result); - return (VersionOptions) result.Clone(); - } - return result; } } diff --git a/src/NerdBank.GitVersioning/VersionOptions.cs b/src/NerdBank.GitVersioning/VersionOptions.cs index 5bf90ded..a36b320b 100644 --- a/src/NerdBank.GitVersioning/VersionOptions.cs +++ b/src/NerdBank.GitVersioning/VersionOptions.cs @@ -13,7 +13,7 @@ /// Describes the various versions and options required for the build. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] - public class VersionOptions : IEquatable, ICloneable + public class VersionOptions : IEquatable { /// /// Default value for . @@ -206,12 +206,12 @@ public int VersionHeightOffsetOrDefault public ReleaseOptions ReleaseOrDefault => this.Release ?? ReleaseOptions.DefaultInstance; /// - /// A list of paths to use to filter commits when calculating version height. + /// Gets or sets a list of paths to use to filter commits when calculating version height. /// If a given commit does not affect any paths in this filter, it is ignored for version height calculations. /// Paths should be relative to the root of the repository. /// [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public IReadOnlyList PathFilters; + public string[] PathFilters { get; set; } /// /// Gets or sets a value indicating whether this options object should inherit from an ancestor any settings that are not explicitly set in this one. @@ -341,22 +341,6 @@ public override bool Equals(object obj) /// The hash code. public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - /// - /// Clones this object. - /// - /// A deep clone of this object. - public object Clone() - { - var newVersionOptions = (VersionOptions) this.MemberwiseClone(); - newVersionOptions.Version = (SemanticVersion) newVersionOptions.Version?.Clone(); - newVersionOptions.AssemblyVersion = (AssemblyVersionOptions) newVersionOptions.AssemblyVersion?.Clone(); - newVersionOptions.NuGetPackageVersion = (NuGetPackageVersionOptions) newVersionOptions.NuGetPackageVersion?.Clone(); - newVersionOptions.PublicReleaseRefSpec = (string[]) newVersionOptions.PublicReleaseRefSpec?.Clone(); - newVersionOptions.CloudBuild = (CloudBuildOptions) newVersionOptions.CloudBuild?.Clone(); - newVersionOptions.Release = (ReleaseOptions) newVersionOptions.Release?.Clone(); - return newVersionOptions; - } - /// /// Checks equality against another instance of this class. /// @@ -384,7 +368,7 @@ internal bool IsDefaultVersionTheOnlyPropertySet /// /// The class that contains settings for the property. /// - public class NuGetPackageVersionOptions : IEquatable, ICloneable + public class NuGetPackageVersionOptions : IEquatable { /// /// The default (uninitialized) instance. @@ -441,12 +425,6 @@ protected NuGetPackageVersionOptions(bool isReadOnly) /// public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - /// - /// Clones this object. - /// - /// A deep clone of this object. - public object Clone() => this.isReadOnly ? this : this.MemberwiseClone(); - /// /// Gets a value indicating whether this instance is equivalent to the default instance. /// @@ -497,7 +475,7 @@ public int GetHashCode(NuGetPackageVersionOptions obj) /// /// Describes the details of how the AssemblyVersion value will be calculated. /// - public class AssemblyVersionOptions : IEquatable, ICloneable + public class AssemblyVersionOptions : IEquatable { /// /// The default (uninitialized) instance. @@ -579,12 +557,6 @@ public Version Version /// public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - /// - /// Clones this object. - /// - /// A deep clone of this object. - public object Clone() => this.isReadOnly ? this : this.MemberwiseClone(); - /// /// Gets a value indicating whether this instance is equivalent to the default instance. /// @@ -636,7 +608,7 @@ public int GetHashCode(AssemblyVersionOptions obj) /// /// Options that are applicable specifically to cloud builds (e.g. VSTS, AppVeyor, TeamCity) /// - public class CloudBuildOptions : IEquatable, ICloneable + public class CloudBuildOptions : IEquatable { /// /// The default (uninitialized) instance. @@ -729,21 +701,6 @@ public CloudBuildNumberOptions BuildNumber /// public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - /// - /// Clones this object. - /// - /// A deep clone of this object. - public object Clone() - { - if (this.isReadOnly) - { - return this; - } - var newCbo = (CloudBuildOptions)this.MemberwiseClone(); - newCbo.BuildNumber = (CloudBuildNumberOptions) newCbo.BuildNumber?.Clone(); - return newCbo; - } - /// /// Gets a value indicating whether this instance is equivalent to the default instance. /// @@ -798,7 +755,7 @@ public int GetHashCode(CloudBuildOptions obj) /// /// Override the build number preset by the cloud build with one enriched with version information. /// - public class CloudBuildNumberOptions : IEquatable, ICloneable + public class CloudBuildNumberOptions : IEquatable { /// /// The default (uninitialized) instance. @@ -865,17 +822,6 @@ protected CloudBuildNumberOptions(bool isReadOnly) /// public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - /// - /// Clones this object. - /// - /// A deep clone of this object. - public object Clone() - { - var newCbno = (CloudBuildNumberOptions)this.MemberwiseClone(); - newCbno.IncludeCommitId = (CloudBuildNumberCommitIdOptions) newCbno.IncludeCommitId?.Clone(); - return newCbno; - } - /// /// Gets a value indicating whether this instance is equivalent to the default instance. /// @@ -928,7 +874,7 @@ public int GetHashCode(CloudBuildNumberOptions obj) /// /// Describes when and where to include information about the git commit being built. /// - public class CloudBuildNumberCommitIdOptions : IEquatable, ICloneable + public class CloudBuildNumberCommitIdOptions : IEquatable { /// /// The default (uninitialized) instance. @@ -1002,12 +948,6 @@ protected CloudBuildNumberCommitIdOptions(bool isReadOnly) /// public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - /// - /// Clones this object. - /// - /// A deep clone of this object. - public object Clone() => this.isReadOnly ? this : this.MemberwiseClone(); - /// /// Gets a value indicating whether this instance is equivalent to the default instance. /// @@ -1156,7 +1096,7 @@ public enum CloudBuildNumberCommitWhere /// /// Encapsulates settings for the "prepare-release" command /// - public class ReleaseOptions : IEquatable, ICloneable + public class ReleaseOptions : IEquatable { /// /// The default (uninitialized) instance. @@ -1253,12 +1193,6 @@ public string FirstUnstableTag /// public override int GetHashCode() => EqualWithDefaultsComparer.Singleton.GetHashCode(this); - /// - /// Clones this object. - /// - /// A deep clone of this object. - public object Clone() => this.isReadOnly ? this : this.MemberwiseClone(); - /// /// Gets a value indicating whether this instance is equivalent to the default instance. /// From 104e0dd588a558abb071dd8726aa6f2bef2ce598 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sun, 19 Jan 2020 05:56:54 -0700 Subject: [PATCH 24/25] Create empty doc file and links to it --- doc/pathFilters.md | 2 ++ doc/versionJson.md | 5 +++++ readme.md | 1 + 3 files changed, 8 insertions(+) create mode 100644 doc/pathFilters.md diff --git a/doc/pathFilters.md b/doc/pathFilters.md new file mode 100644 index 00000000..530d228b --- /dev/null +++ b/doc/pathFilters.md @@ -0,0 +1,2 @@ +# Path filters + diff --git a/doc/versionJson.md b/doc/versionJson.md index 4721d5bc..c4725921 100644 --- a/doc/versionJson.md +++ b/doc/versionJson.md @@ -33,6 +33,9 @@ The content of the version.json file is a JSON serialized object with these prop "nugetPackageVersion": { "semVer": 1 // optional. Set to either 1 or 2 to control how the NuGet package version string is generated. Default is 1. }, + "pathFilters": [ + // optional list of paths to consider when calculating version height. + ] "publicReleaseRefSpec": [ "^refs/heads/master$", // we release out of master "^refs/tags/v\\d+\\.\\d+" // we also release tags starting with vN.N @@ -70,3 +73,5 @@ The `publicReleaseRefSpec` field causes builds out of certain branches or tags to automatically drop the `-gabc123` git commit ID suffix from the version, making it convenient to build releases out of these refs with a friendly version number that assumes linear versioning. + +[Learn more about pathFilters](pathFilters.md). diff --git a/readme.md b/readme.md index 778425d8..d1299871 100644 --- a/readme.md +++ b/readme.md @@ -58,6 +58,7 @@ Also some special [cloud build considerations](doc/cloudbuild.md). This package calculates the version based on a combination of the version.json file, the git 'height' of the version, and the git commit ID. +The height can optionally be incremented only for those [commits that change certain paths](doc/pathFilters.md). ### Version generation From f455b014ad8a5cc95328a136f851274a841d80ba Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Sun, 19 Jan 2020 22:11:01 +0000 Subject: [PATCH 25/25] Update pathFilters.md --- doc/pathFilters.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/doc/pathFilters.md b/doc/pathFilters.md index 530d228b..27075c48 100644 --- a/doc/pathFilters.md +++ b/doc/pathFilters.md @@ -1,2 +1,59 @@ # Path filters +## Problem + +Some repositories may contain more than project. This is sometimes referred to as a _mono repo_ (as opposed to having a repo for each project - _many repo_). Imagine a repository structured as: + +- / + - Foo/ + - version.json => `{"version": "1.0"}` + - Bar/ + - version.json => `{"version": "2.1"}` + - Quux/ + - version.json => `{"version": "4.3"}` + - README.md + +With GitVersioning's default configuration, a commit to a given project's subtree will result in the version height bumping for all projects in the repository. This is typically not desirable. Intuitively, a commit to `Bar` should only cause a version bump for `Bar`, and not `Foo` or `Quux`. + +## Solution + +Path filters provide a way to filter which subtrees in the repository affect version height. Imagine the `version.json` files had a `pathFilter` property: + +```json +{ + "version": "1.0", + "pathFilters": ["."] +} +``` + +With this single path filter of `"."`, the version height for this project would only bump when a commit was made within that subtree. Now imagine all projects in the original example have this value for `pathFilters`. Consider the following commits to the repository, and note their effect on the version height for each project: + +| Paths changed | Result | +| ---------------------------------------- | -------------------------------------------------------------------------- | +| `/README.md` | Commit does not affect any project. No versions change. | +| `/Bar/Program.cs`
`/Quux/Quux.csproj` | Commit affects both `Bar` and `Quux`. Their patch versions will bump by 1. | +| `/Bar/MyClass.cs` | Commit affects only `Bar`. `Bar`'s patch version will bump by 1. | + +When absent, the implied value for `pathFilters` is: + +```json +{ + "pathFilters": [":/"] +} +``` + +This results in the entire repository tree being considered for version height calculations. This is the default behavior for GitVersioning. + +## Path filter format + +Path filters take on a variety of formats, and can specify paths relative to the `version.json` or relative to the root of the repository. See the [Path filter format](#path-filter-format) section for more information. + +Multiple path filters may also be specified. The order is irrelevant. After a path matches any non-exclude path filter, it will be run through all exclude path filter. If it matches, the path is ignored. + +| Path filter | Description | +| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `file-here.txt`
`./quux.txt`
`./sub-dir/foo.txt`
`../subdir/inclusion.txt` | File will be included. Path is relative to the `version.json` file. | +| `sub-dir`
`../sub-dir` | Directory will be included. Path is relative to the `version.json` file. | +| `:/dir/file.txt` | File will be included. Path is absolute (i.e., relative to the root of the repository). | +| `:!bar.txt`
`:^../foo/baz.txt` | File will be excluded. Path is relative to the `version.json` file. `:!` and `:^` prefixes are synonymous. | +| `:!/root-file.txt` | File will be excluded. Path is absolute (i.e., relative to the root of the repository). |