diff --git a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs index b27d3889..f8e07e03 100644 --- a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs +++ b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -410,6 +411,123 @@ public void GetVersionHeight_VeryLongHistory() this.Repo.GetVersionHeight(); } + + [Fact] + public void GetVersionHeight_CachingPerf() + { + const string repoRelativeSubDirectory = "subdir"; + + var semanticVersion1 = SemanticVersion.Parse("1.0"); + this.WriteVersionFile( + new VersionOptions { Version = semanticVersion1 }, + repoRelativeSubDirectory); + + // Add a large volume of commits where the versison file hasn't been bumped- key thing is that when we try to determine the git height, + // we have a lot of commits to walk + const int numCommitsToTraverse = 300; + MeasureRuntime( + () => + { + for (int i = 0; i < numCommitsToTraverse; i++) + this.Repo.Commit($"Test commit #{i}", this.Signer, this.Signer, new CommitOptions {AllowEmptyCommit = true}); + + return -1; + }, + $"Add {numCommitsToTraverse} commits to the git history" + ); + + // First calculation of height will not have the benefit of the cache, will be slow + var (initialHeight, initialTime) = MeasureRuntime(() => this.Repo.Head.GetVersionHeight(repoRelativeSubDirectory), "get the initial (uncached) version height"); + Assert.True(File.Exists(Path.Combine(RepoPath, repoRelativeSubDirectory, GitHeightCache.CacheFileName))); + + // Second calculation of height should be able to use the cache file generated by the previous calculation, even though it's for a child of the original path + var (cachedHeight, cachedTime) = MeasureRuntime(() => this.Repo.Head.GetVersionHeight(Path.Combine(repoRelativeSubDirectory, "new_sub_dir")), "get the version height for the unmodified repository (should be cached)"); + + // Want to see at least 20x perf increase + Assert.InRange(cachedTime, TimeSpan.Zero, TimeSpan.FromTicks(initialTime.Ticks / 20)); + Assert.Equal(initialHeight, numCommitsToTraverse + 1); + Assert.Equal(initialHeight, cachedHeight); + + // Adding an additional commit and then getting the height should only involve walking a single commit + this.Repo.Commit($"Final Test commit", this.Signer, this.Signer, new CommitOptions {AllowEmptyCommit = true}); + // Third calculation of height should be able to use the cache file for the first set of commits but walk the last commit to determine the height + (cachedHeight, cachedTime) = MeasureRuntime(() => this.Repo.Head.GetVersionHeight(repoRelativeSubDirectory), "get the version height for the modified repository (should partially use the cache)"); + Assert.Equal(cachedHeight, numCommitsToTraverse + 2); + // We'd expect a less dramatic perf increase this time but should still be significant + Assert.InRange(cachedTime, TimeSpan.Zero, TimeSpan.FromTicks(initialTime.Ticks / 10)); + } + + [Fact] + public void GetVersionHeight_CachingMultipleVersions() + { + // TODO probably should make this test more vigorous + const string repoRelativeSubDirectory1 = "subdir1", repoRelativeSubDirectory2 = "subdir2";; + + this.WriteVersionFile( + new VersionOptions { Version = SemanticVersion.Parse("1.1") }, + repoRelativeSubDirectory1); + + this.WriteVersionFile( + new VersionOptions { Version = SemanticVersion.Parse("1.2") }, + repoRelativeSubDirectory2); + + // Verify that two separate cache files are generated + this.Repo.Head.GetVersionHeight(repoRelativeSubDirectory1); + Assert.True(File.Exists(Path.Combine(RepoPath, repoRelativeSubDirectory1, GitHeightCache.CacheFileName))); + + this.Repo.Head.GetVersionHeight(repoRelativeSubDirectory2); + Assert.True(File.Exists(Path.Combine(RepoPath, repoRelativeSubDirectory2, GitHeightCache.CacheFileName))); + } + + [Fact] + public void GetVersionHeight_CachingMultipleParents() + { + /* + * Layout of branches + commits: + * master: version -> second --------------> | + * | | + * another: | -> branch commit #n x 5 | -> merge commit + */ + this.WriteVersionFile(); + var anotherBranch = this.Repo.CreateBranch("another"); + var secondCommit = this.Repo.Commit("Second", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); + + // get height of the second commit- will cache the height for this commit + var height = secondCommit.GetVersionHeight(useHeightCaching: true); + Assert.Equal(2, height); + + // add many commits to the another branch + merge with master + Commands.Checkout(this.Repo, anotherBranch); + Commit[] branchCommits = new Commit[5]; + for (int i = 1; i <= branchCommits.Length; i++) + { + branchCommits[i - 1] = this.Repo.Commit($"branch commit #{i}", this.Signer, this.Signer, new CommitOptions { AllowEmptyCommit = true }); + } + + this.Repo.Merge(secondCommit, new Signature("t", "t@t.com", DateTimeOffset.Now), new MergeOptions { FastForwardStrategy = FastForwardStrategy.NoFastForward }); + + // The height should be the height of the 'another' branch as it has the greatest height. + // The cached height for the master branch should be ignored. + Assert.Equal(7, this.Repo.Head.GetVersionHeight()); + } + + [Fact] + public void GetVersionHeight_NewCommitsInvalidateCache() + { + const string repoRelativeSubDirectory = "subdir"; + + var semanticVersion1 = SemanticVersion.Parse("1.0"); + this.WriteVersionFile( + new VersionOptions { Version = semanticVersion1 }, + repoRelativeSubDirectory); + + var height1 = this.Repo.Head.GetVersionHeight(repoRelativeSubDirectory); + this.Repo.Commit($"Test commit (should invalidate cache)", this.Signer, this.Signer, new CommitOptions {AllowEmptyCommit = true}); + var height2 = this.Repo.Head.GetVersionHeight(repoRelativeSubDirectory); + + Assert.Equal(1, height1); + Assert.Equal(2, height2); + } [Fact] public void GetCommitsFromVersion_WithPathFilters() @@ -825,4 +943,21 @@ private void VerifyCommitsWithVersion(Commit[] commits) Assert.Equal(commits[i], this.Repo.GetCommitFromVersion(encodedVersion)); } } + + private (T, TimeSpan) MeasureRuntime(Func toTime, string description) + { + var sp = Stopwatch.StartNew(); + try + { + var result = toTime(); + sp.Stop(); + + return (result, sp.Elapsed); + } + finally + { + sp.Stop(); + this.Logger.WriteLine($"Took {sp.Elapsed} to {description}"); + } + } } diff --git a/src/NerdBank.GitVersioning.Tests/GitHeightCacheTests.cs b/src/NerdBank.GitVersioning.Tests/GitHeightCacheTests.cs new file mode 100644 index 00000000..4c854dac --- /dev/null +++ b/src/NerdBank.GitVersioning.Tests/GitHeightCacheTests.cs @@ -0,0 +1,53 @@ +using System.IO; +using LibGit2Sharp; +using Xunit; +using Version = System.Version; + +namespace Nerdbank.GitVersioning +{ + public class GitHeightCacheTests + { + [Fact] + public void CachedHeightAvailable_NoCacheFile() + { + var cache = new GitHeightCache(Directory.GetCurrentDirectory(), "non-existent-dir", new Version(1, 0)); + Assert.False(cache.CachedHeightAvailable); + } + + [Fact] + public void CachedHeightAvailable_RootCacheFile() + { + File.WriteAllText($"./{GitHeightCache.CacheFileName}", ""); + var cache = new GitHeightCache(Directory.GetCurrentDirectory(), null, new Version(1, 0)); + Assert.True(cache.CachedHeightAvailable); + } + + [Fact] + public void CachedHeightAvailable_CacheFile() + { + Directory.CreateDirectory("./testDir"); + File.WriteAllText($"./testDir/{GitHeightCache.CacheFileName}", ""); + var cache = new GitHeightCache(Directory.GetCurrentDirectory(),"testDir/", new Version(1, 0)); + Assert.True(cache.CachedHeightAvailable); + } + + [Fact] + public void GitHeightCache_RoundtripCaching() + { + var cache = new GitHeightCache(Directory.GetCurrentDirectory(), null, new Version(1, 0)); + + // test initial set + cache.SetHeight(new ObjectId("8b1f731de6b98aaf536085a62c40dfd3e38817b6"), 2); + var cachedHeight = cache.GetHeight(); + Assert.Equal("8b1f731de6b98aaf536085a62c40dfd3e38817b6", cachedHeight.CommitId.Sha); + Assert.Equal(2, cachedHeight.Height); + Assert.Equal("1.0", cachedHeight.BaseVersion.ToString()); + + // verify overwriting works correctly + cache.SetHeight(new ObjectId("352459698e082aebef799d77807961d222e75efe"), 3); + cachedHeight = cache.GetHeight(); + Assert.Equal("352459698e082aebef799d77807961d222e75efe", cachedHeight.CommitId.Sha); + Assert.Equal("1.0", cachedHeight.BaseVersion.ToString()); + } + } +} \ No newline at end of file diff --git a/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj b/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj index a0a31aeb..d6a3479c 100644 --- a/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj +++ b/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj @@ -34,6 +34,8 @@ + + diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index 66b2d4d1..8f25647e 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -39,8 +39,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. + /// + /// If true, the version height will be cached for subsequent invocations. If a previously cached version height exists and it is valid, it will be used. + /// Defaults to true. + /// /// 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, bool useHeightCaching = true) { Requires.NotNull(commit, nameof(commit)); Requires.Argument(repoRelativeProjectDirectory == null || !Path.IsPathRooted(repoRelativeProjectDirectory), nameof(repoRelativeProjectDirectory), "Path should be relative to repo root."); @@ -56,11 +60,33 @@ public static int GetVersionHeight(this Commit commit, string repoRelativeProjec var baseSemVer = baseVersion != null ? SemanticVersion.Parse(baseVersion.ToString()) : versionOptions.Version ?? SemVer0; - + var versionHeightPosition = versionOptions.VersionHeightPosition; + if (versionHeightPosition.HasValue) { - int height = commit.GetHeight(repoRelativeProjectDirectory, c => CommitMatchesVersion(c, baseSemVer, versionHeightPosition.Value, tracker)); + var cache = new GitHeightCache(commit.GetRepository().Info.WorkingDirectory, versionOptions.RelativeFilePath, baseVersion); + + CachedHeight cachedHeight = null; + if (useHeightCaching && cache.CachedHeightAvailable && (cachedHeight = cache.GetHeight()) != null) + { + if (cachedHeight.CommitId.Equals(commit.Id)) + // Cached height exactly matches the current commit + return cachedHeight.Height; + else + { + // Cached height doesn't match the current commit. However, we can store the cached height in the walker to avoid walking the full height of the commit graph. + var cachedCommit = commit.GetRepository().Lookup(cachedHeight.CommitId) as Commit; + if (cachedCommit != null) + tracker.RecordHeight(cachedCommit, cachedHeight.Height); + } + } + + var height = GetCommitHeight(commit, tracker, c => CommitMatchesVersion(c, baseSemVer, versionHeightPosition.Value, tracker)); + + if (useHeightCaching) + cache.SetHeight(commit.Id, height); + return height; } @@ -237,7 +263,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 . /// /// @@ -271,7 +297,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 . /// /// @@ -729,6 +755,8 @@ private static int GetCommitHeight(Commit startingCommit, GitWalkTracker tracker var commitsToEvaluate = new Stack(); bool TryCalculateHeight(Commit commit) { + // if is cached, then bail? + // Get max height among all parents, or schedule all missing parents for their own evaluation and return false. int maxHeightAmongParents = 0; bool parentMissing = false; @@ -880,7 +908,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/GitHeightCache.cs b/src/NerdBank.GitVersioning/GitHeightCache.cs new file mode 100644 index 00000000..4b04f246 --- /dev/null +++ b/src/NerdBank.GitVersioning/GitHeightCache.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using LibGit2Sharp; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Version = System.Version; + +namespace Nerdbank.GitVersioning +{ + /// + /// Allows caching the height of a commit for a project, reducing repetitive height calculations. + /// + /// + /// Calculating the height of commits can be time consuming for repositories with 1000s of commits between major/ minor version bumps, + /// so caching the height of a commit can save time. This is especially when packaging projects where height calculation must be done multiple times, + /// see https://github.com/dotnet/Nerdbank.GitVersioning/issues/114#issuecomment-669713622. + /// + public class GitHeightCache + { + private readonly string versionFileRelativeDirectory; + private readonly Version baseVersion; + + private static readonly JsonSerializer JsonSerializer = new JsonSerializer() + { + Converters = + { + new VersionConverter(), + new ObjectIdConverter() + } + }; + + /// + /// The name used for the cache file. + /// + public const string CacheFileName = "version.cache.json"; + + private readonly string heightCacheFilePath; + private readonly Lazy cachedHeightAvailable; + + /// + /// Creates a new height cache. + /// + /// The root path of the repository. + /// The relative path of the version file within the repository to cache heights for. + /// + public GitHeightCache(string repositoryPath, string versionFileRelativePath, Version baseVersion) + { + this.versionFileRelativeDirectory = Path.GetDirectoryName(versionFileRelativePath); + this.baseVersion = baseVersion; + + if (repositoryPath == null) + this.heightCacheFilePath = null; + else + this.heightCacheFilePath = Path.Combine(repositoryPath, this.versionFileRelativeDirectory ?? "", CacheFileName); + + this.cachedHeightAvailable = new Lazy(() => this.heightCacheFilePath != null && File.Exists(this.heightCacheFilePath)); + } + + /// + /// Determines if a cached version is available. + /// + public bool CachedHeightAvailable => cachedHeightAvailable.Value; + + /// + /// Fetches the . May return null if the cache is not valid. + /// + /// + /// + public CachedHeight GetHeight() + { + try + { + using (var sr = File.OpenText(this.heightCacheFilePath)) + using (var jsonReader = new JsonTextReader(sr)) + { + var cachedHeight = JsonSerializer.Deserialize(jsonReader); + + // Indicates any cached height is irrelevant- every time the base version is bumped, we need to walk an entirely different set of commits + if (cachedHeight.BaseVersion != this.baseVersion) + return null; + + // Indicates that the project the cache is associated with has moved directories- any cached results may be invalid, so discard + if (cachedHeight.VersionRelativeDir != this.versionFileRelativeDirectory) + return null; + + return cachedHeight; + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Unexpected error occurred attempting to read and deserialize '{this.heightCacheFilePath}'. Try deleting this file and rebuilding.", ex); + } + } + + /// + /// Caches the height of a commit, overwriting any previously cached values. + /// + /// + /// + public void SetHeight(ObjectId commitId, int height) + { + if (this.heightCacheFilePath == null || commitId == null || commitId == ObjectId.Zero || !Directory.Exists(Path.GetDirectoryName(this.heightCacheFilePath))) + return; + + using (var sw = File.CreateText(this.heightCacheFilePath)) + using (var jsonWriter = new JsonTextWriter(sw)) + { + jsonWriter.WriteComment("Cached commit height, created by Nerdbank.GitVersioning. Do not modify."); + JsonSerializer.Serialize(jsonWriter, new CachedHeight(commitId, height, this.baseVersion, this.versionFileRelativeDirectory)); + } + } + + private class ObjectIdConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteValue(((ObjectId) value).Sha); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => new ObjectId(reader.Value as string); + + public override bool CanConvert(Type objectType) => objectType == typeof(ObjectId); + } + } + + /// + /// The cached git height of a project. + /// + public class CachedHeight + { + /// + /// + /// + /// + /// + /// + /// + public CachedHeight(ObjectId commitId, int height, Version baseVersion, string versionRelativeDir) + { + this.CommitId = commitId; + this.Height = height; + this.BaseVersion = baseVersion; + this.VersionRelativeDir = versionRelativeDir; + } + + /// + /// The base version this cached height was calculated for. + /// + public Version BaseVersion { get; } + + /// + /// The cached height. + /// + public int Height { get; } + + /// + /// The commit id for the cached height. + /// + public ObjectId CommitId { get; } + + /// + /// The relative directory path for the version file that this cached is associated with. + /// + public string VersionRelativeDir { get; } + + /// + public override string ToString() => $"({CommitId}, {Height}, {BaseVersion})"; + } +} \ No newline at end of file diff --git a/src/NerdBank.GitVersioning/VersionFile.cs b/src/NerdBank.GitVersioning/VersionFile.cs index c8745e04..a3216b8b 100644 --- a/src/NerdBank.GitVersioning/VersionFile.cs +++ b/src/NerdBank.GitVersioning/VersionFile.cs @@ -52,7 +52,7 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR var versionTxtBlob = commit.Tree[candidatePath]?.Target as LibGit2Sharp.Blob; if (versionTxtBlob != null) { - var result = TryReadVersionFile(new StreamReader(versionTxtBlob.GetContentStream())); + var result = TryReadVersionFile(new StreamReader(versionTxtBlob.GetContentStream()), candidatePath); if (result != null) { return result; @@ -72,7 +72,7 @@ public static VersionOptions GetVersion(LibGit2Sharp.Commit commit, string repoR VersionOptions result; try { - result = TryReadVersionJsonContent(versionJsonContent, searchDirectory); + result = TryReadVersionJsonContent(versionJsonContent, searchDirectory, candidatePath); } catch (FormatException ex) { @@ -158,7 +158,7 @@ public static VersionOptions GetVersion(string projectDirectory, out string actu { using (var sr = new StreamReader(File.OpenRead(versionTxtPath))) { - var result = TryReadVersionFile(sr); + var result = TryReadVersionFile(sr, repo?.GetRepoRelativePath(versionTxtPath)); if (result != null) { actualDirectory = searchDirectory; @@ -174,7 +174,7 @@ public static VersionOptions GetVersion(string projectDirectory, out string actu var repoRelativeBaseDirectory = repo?.GetRepoRelativePath(searchDirectory); VersionOptions result = - TryReadVersionJsonContent(versionJsonContent, repoRelativeBaseDirectory); + TryReadVersionJsonContent(versionJsonContent, repoRelativeBaseDirectory, repo?.GetRepoRelativePath(versionJsonPath)); if (result?.Inherit ?? false) { if (parentDirectory != null) @@ -314,7 +314,7 @@ public static string SetVersion(string projectDirectory, Version version, string /// /// The content of the version.txt file to read. /// The version information read from the file; or null if a deserialization error occurs. - private static VersionOptions TryReadVersionFile(TextReader versionTextContent) + private static VersionOptions TryReadVersionFile(TextReader versionTextContent, string candidatePath) { string versionLine = versionTextContent.ReadLine(); string prereleaseVersion = versionTextContent.ReadLine(); @@ -332,6 +332,7 @@ private static VersionOptions TryReadVersionFile(TextReader versionTextContent) return new VersionOptions { Version = semVer, + RelativeFilePath = candidatePath }; } @@ -341,11 +342,13 @@ private static VersionOptions TryReadVersionFile(TextReader versionTextContent) /// The content of the version.json file. /// Directory that this version.json file is relative to the root of the repository. /// The deserialized object, if deserialization was successful. - private static VersionOptions TryReadVersionJsonContent(string jsonContent, string repoRelativeBaseDirectory) + private static VersionOptions TryReadVersionJsonContent(string jsonContent, string repoRelativeBaseDirectory, string candidatePath) { try { - return JsonConvert.DeserializeObject(jsonContent, VersionOptions.GetJsonSettings(repoRelativeBaseDirectory: repoRelativeBaseDirectory)); + var result = JsonConvert.DeserializeObject(jsonContent, VersionOptions.GetJsonSettings(repoRelativeBaseDirectory: repoRelativeBaseDirectory)); + result.RelativeFilePath = candidatePath; + return result; } catch (JsonSerializationException) { diff --git a/src/NerdBank.GitVersioning/VersionOptions.cs b/src/NerdBank.GitVersioning/VersionOptions.cs index f2878a7f..465de11d 100644 --- a/src/NerdBank.GitVersioning/VersionOptions.cs +++ b/src/NerdBank.GitVersioning/VersionOptions.cs @@ -253,6 +253,13 @@ public int VersionHeightOffsetOrDefault [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public bool Inherit { get; set; } + /// + /// The relative path of the source file (if deserialized from a file), otherwise null. + /// If is true, returns the path of the child version file. + /// + [JsonIgnore] + public string RelativeFilePath { get; set; } + /// /// Gets the position in a computed version that the version height should appear. /// diff --git a/src/NerdBank.GitVersioning/VersionOracle.cs b/src/NerdBank.GitVersioning/VersionOracle.cs index 26a481f5..653e6eda 100644 --- a/src/NerdBank.GitVersioning/VersionOracle.cs +++ b/src/NerdBank.GitVersioning/VersionOracle.cs @@ -1,4 +1,6 @@ -namespace Nerdbank.GitVersioning +using LibGit2Sharp; + +namespace Nerdbank.GitVersioning { using System; using System.Collections.Generic; @@ -28,7 +30,7 @@ public class VersionOracle /// /// Initializes a new instance of the class. /// - public static VersionOracle Create(string projectDirectory, string gitRepoDirectory = null, ICloudBuild cloudBuild = null, int? overrideBuildNumberOffset = null, string projectPathRelativeToGitRepoRoot = null) + public static VersionOracle Create(string projectDirectory, string gitRepoDirectory = null, ICloudBuild cloudBuild = null, int? overrideBuildNumberOffset = null, string projectPathRelativeToGitRepoRoot = null, bool useHeightCaching = true) { Requires.NotNull(projectDirectory, nameof(projectDirectory)); if (string.IsNullOrEmpty(gitRepoDirectory)) @@ -38,22 +40,22 @@ public static VersionOracle Create(string projectDirectory, string gitRepoDirect using (var git = GitExtensions.OpenGitRepo(gitRepoDirectory)) { - return new VersionOracle(projectDirectory, git, null, cloudBuild, overrideBuildNumberOffset, projectPathRelativeToGitRepoRoot); + return new VersionOracle(projectDirectory, git, null, cloudBuild, overrideBuildNumberOffset, projectPathRelativeToGitRepoRoot, useHeightCaching); } } /// /// Initializes a new instance of the class. /// - public VersionOracle(string projectDirectory, LibGit2Sharp.Repository repo, ICloudBuild cloudBuild, int? overrideBuildNumberOffset = null, string projectPathRelativeToGitRepoRoot = null) - : this(projectDirectory, repo, null, cloudBuild, overrideBuildNumberOffset, projectPathRelativeToGitRepoRoot) + public VersionOracle(string projectDirectory, LibGit2Sharp.Repository repo, ICloudBuild cloudBuild, int? overrideBuildNumberOffset = null, string projectPathRelativeToGitRepoRoot = null, bool useHeightCaching = true) + : this(projectDirectory, repo, null, cloudBuild, overrideBuildNumberOffset, projectPathRelativeToGitRepoRoot, useHeightCaching) { } /// /// Initializes a new instance of the class. /// - public VersionOracle(string projectDirectory, LibGit2Sharp.Repository repo, LibGit2Sharp.Commit head, ICloudBuild cloudBuild, int? overrideVersionHeightOffset = null, string projectPathRelativeToGitRepoRoot = null) + public VersionOracle(string projectDirectory, Repository repo, Commit head, ICloudBuild cloudBuild, int? overrideVersionHeightOffset = null, string projectPathRelativeToGitRepoRoot = null, bool useHeightCaching = true) { var relativeRepoProjectDirectory = projectPathRelativeToGitRepoRoot ?? repo?.GetRepoRelativePath(projectDirectory); @@ -80,7 +82,7 @@ public VersionOracle(string projectDirectory, LibGit2Sharp.Repository repo, LibG this.GitCommitId = commit?.Id.Sha ?? cloudBuild?.GitCommitId ?? null; this.GitCommitDate = commit?.Author.When; - this.VersionHeight = CalculateVersionHeight(relativeRepoProjectDirectory, commit, committedVersion, workingVersion); + this.VersionHeight = CalculateVersionHeight(relativeRepoProjectDirectory, commit, committedVersion, workingVersion, useHeightCaching); this.BuildingRef = cloudBuild?.BuildingTag ?? cloudBuild?.BuildingBranch ?? repo?.Head.CanonicalName; // Override the typedVersion with the special build number and revision components, when available. @@ -471,7 +473,7 @@ private static Version GetAssemblyVersion(Version version, VersionOptions versio /// The specified string, with macros substituted for actual values. private string ReplaceMacros(string prereleaseOrBuildMetadata) => prereleaseOrBuildMetadata?.Replace(VersionOptions.VersionHeightPlaceholder, this.VersionHeightWithOffset.ToString(CultureInfo.InvariantCulture)); - private static int CalculateVersionHeight(string relativeRepoProjectDirectory, LibGit2Sharp.Commit headCommit, VersionOptions committedVersion, VersionOptions workingVersion) + private static int CalculateVersionHeight(string relativeRepoProjectDirectory, LibGit2Sharp.Commit headCommit, VersionOptions committedVersion, VersionOptions workingVersion, bool useHeightCaching) { var headCommitVersion = committedVersion?.Version ?? SemVer0; @@ -487,7 +489,7 @@ private static int CalculateVersionHeight(string relativeRepoProjectDirectory, L } } - return headCommit?.GetVersionHeight(relativeRepoProjectDirectory) ?? 0; + return headCommit?.GetVersionHeight(relativeRepoProjectDirectory, useHeightCaching: useHeightCaching) ?? 0; } private static Version GetIdAsVersion(LibGit2Sharp.Commit headCommit, VersionOptions committedVersion, VersionOptions workingVersion, int versionHeight) diff --git a/src/Nerdbank.GitVersioning.Tasks/GetBuildVersion.cs b/src/Nerdbank.GitVersioning.Tasks/GetBuildVersion.cs index 72fad909..a47f3d32 100644 --- a/src/Nerdbank.GitVersioning.Tasks/GetBuildVersion.cs +++ b/src/Nerdbank.GitVersioning.Tasks/GetBuildVersion.cs @@ -55,6 +55,12 @@ public GetBuildVersion() /// Gets or sets the optional override build number offset. /// public int OverrideBuildNumberOffset { get; set; } = int.MaxValue; + + /// + /// Gets or set if the heights of commits should be cached. Caching commit heights can bring + /// significant performance improvements. + /// + public bool UseHeightCaching { get; set; } = true; /// /// Gets or sets the path to the folder that contains the NB.GV .targets file. @@ -202,7 +208,7 @@ protected override bool ExecuteInner() var cloudBuild = CloudBuild.Active; var overrideBuildNumberOffset = (this.OverrideBuildNumberOffset == int.MaxValue) ? (int?)null : this.OverrideBuildNumberOffset; - var oracle = VersionOracle.Create(Directory.GetCurrentDirectory(), this.GitRepoRoot, cloudBuild, overrideBuildNumberOffset, this.ProjectPathRelativeToGitRepoRoot); + var oracle = VersionOracle.Create(Directory.GetCurrentDirectory(), this.GitRepoRoot, cloudBuild, overrideBuildNumberOffset, this.ProjectPathRelativeToGitRepoRoot, this.UseHeightCaching); if (!string.IsNullOrEmpty(this.DefaultPublicRelease)) { oracle.PublicRelease = string.Equals(this.DefaultPublicRelease, "true", StringComparison.OrdinalIgnoreCase); diff --git a/src/Nerdbank.GitVersioning.Tasks/build/Nerdbank.GitVersioning.targets b/src/Nerdbank.GitVersioning.Tasks/build/Nerdbank.GitVersioning.targets index 112d707a..83591248 100644 --- a/src/Nerdbank.GitVersioning.Tasks/build/Nerdbank.GitVersioning.targets +++ b/src/Nerdbank.GitVersioning.Tasks/build/Nerdbank.GitVersioning.targets @@ -27,6 +27,7 @@ <_NBGV_PlatformSuffix Condition=" '$(_NBGV_PlatformSuffix)' == '' and '$(MSBuildRuntimeType)' == 'Core' ">MSBuildCore/ <_NBGV_PlatformSuffix Condition=" '$(_NBGV_PlatformSuffix)' == '' ">MSBuildFull/ $(MSBuildThisFileDirectory)$(_NBGV_PlatformSuffix) + true @@ -75,7 +76,8 @@ GitRepoRoot="$(GitRepoRoot)" ProjectPathRelativeToGitRepoRoot="$(ProjectPathRelativeToGitRepoRoot)" OverrideBuildNumberOffset="$(OverrideBuildNumberOffset)" - TargetsPath="$(MSBuildThisFileDirectory)"> + TargetsPath="$(MSBuildThisFileDirectory)" + UseHeightCaching="$(NerdbankGitVersioningUseHeightCache)">