Skip to content

Commit f92d363

Browse files
committed
Minimize calls to git when calculating VersionOracle
The VersionOracle constructor was using several helper methods from GitExtensions to calculate the VersionHeight and Version for the oracle. Unfortunately, most of the helper methods recalculate the VersionOptions by querying git and reserializing the version.json file. This is particularly costly when the git history gets deep. With this commit, the VersionOracle calculates the commit and working tree VersionOptions each once and goes about its processing from there. This should greatly improve performance on deep git history's. Addresses part of #114 Note: caching VersionOracle across projects in a single MSBuild invocation still makes a lot of sense.
1 parent 7cb9894 commit f92d363

File tree

3 files changed

+92
-33
lines changed

3 files changed

+92
-33
lines changed

src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.IO;
1+
using System;
2+
using System.IO;
23
using System.Linq;
34
using LibGit2Sharp;
45
using Nerdbank.GitVersioning;
@@ -15,6 +16,14 @@ public VersionOracleTests(ITestOutputHelper logger)
1516

1617
private string CommitIdShort => this.Repo.Head.Commits.First().Id.Sha.Substring(0, 10);
1718

19+
[Fact]
20+
public void NotRepo()
21+
{
22+
// Seems safe to assume the system directory is not a repository.
23+
var oracle = VersionOracle.Create(Environment.SystemDirectory);
24+
Assert.Equal(0, oracle.VersionHeight);
25+
}
26+
1827
[Fact(Skip = "Unstable test. See issue #125")]
1928
public void Submodule_RecognizedWithCorrectVersion()
2029
{

src/NerdBank.GitVersioning/GitExtensions.cs

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,18 @@ public static class GitExtensions
3333
/// </summary>
3434
/// <param name="commit">The commit to measure the height of.</param>
3535
/// <param name="repoRelativeProjectDirectory">The repo-relative project directory for which to calculate the version.</param>
36+
/// <param name="baseVersion">Optional base version to calculate the height. If not specified, the base version will be calculated by scanning the repository.</param>
3637
/// <returns>The height of the commit. Always a positive integer.</returns>
37-
public static int GetVersionHeight(this Commit commit, string repoRelativeProjectDirectory = null)
38+
public static int GetVersionHeight(this Commit commit, string repoRelativeProjectDirectory = null, Version baseVersion = null)
3839
{
3940
Requires.NotNull(commit, nameof(commit));
4041
Requires.Argument(repoRelativeProjectDirectory == null || !Path.IsPathRooted(repoRelativeProjectDirectory), nameof(repoRelativeProjectDirectory), "Path should be relative to repo root.");
4142

42-
var baseVersion = VersionFile.GetVersion(commit, repoRelativeProjectDirectory)?.Version?.Version ?? Version0;
43+
if (baseVersion == null)
44+
{
45+
baseVersion = VersionFile.GetVersion(commit, repoRelativeProjectDirectory)?.Version?.Version ?? Version0;
46+
}
47+
4348
int height = commit.GetHeight(c => CommitMatchesMajorMinorVersion(c, baseVersion, repoRelativeProjectDirectory));
4449
return height;
4550
}
@@ -170,7 +175,7 @@ public static Commit GetCommitFromTruncatedIdInteger(this Repository repo, int t
170175
/// <param name="commit">The commit whose ID and position in history is to be encoded.</param>
171176
/// <param name="repoRelativeProjectDirectory">The repo-relative project directory for which to calculate the version.</param>
172177
/// <param name="versionHeight">
173-
/// The version height, previously calculated by a call to <see cref="GetVersionHeight(Commit, string)"/>
178+
/// The version height, previously calculated by a call to <see cref="GetVersionHeight(Commit, string, Version)"/>
174179
/// with the same value for <paramref name="repoRelativeProjectDirectory"/>.
175180
/// </param>
176181
/// <returns>
@@ -188,7 +193,13 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj
188193
Requires.Argument(repoRelativeProjectDirectory == null || !Path.IsPathRooted(repoRelativeProjectDirectory), nameof(repoRelativeProjectDirectory), "Path should be relative to repo root.");
189194

190195
var versionOptions = VersionFile.GetVersion(commit, repoRelativeProjectDirectory);
191-
return GetIdAsVersionHelper(commit, versionOptions, repoRelativeProjectDirectory, versionHeight);
196+
197+
if (!versionHeight.HasValue)
198+
{
199+
versionHeight = GetVersionHeight(commit, repoRelativeProjectDirectory);
200+
}
201+
202+
return GetIdAsVersionHelper(commit, versionOptions, versionHeight.Value);
192203
}
193204

194205
/// <summary>
@@ -198,7 +209,7 @@ public static Version GetIdAsVersion(this Commit commit, string repoRelativeProj
198209
/// <param name="repo">The repo whose ID and position in history is to be encoded.</param>
199210
/// <param name="repoRelativeProjectDirectory">The repo-relative project directory for which to calculate the version.</param>
200211
/// <param name="versionHeight">
201-
/// The version height, previously calculated by a call to <see cref="GetVersionHeight(Commit, string)"/>
212+
/// The version height, previously calculated by a call to <see cref="GetVersionHeight(Commit, string, Version)"/>
202213
/// with the same value for <paramref name="repoRelativeProjectDirectory"/>.
203214
/// </param>
204215
/// <returns>
@@ -219,7 +230,13 @@ public static Version GetIdAsVersion(this Repository repo, string repoRelativePr
219230
if (IsVersionFileChangedInWorkingCopy(repo, repoRelativeProjectDirectory, out committedVersionOptions, out workingCopyVersionOptions))
220231
{
221232
// Apply ordinary logic, but to the working copy version info.
222-
Version result = GetIdAsVersionHelper(headCommit, workingCopyVersionOptions, repoRelativeProjectDirectory, versionHeight);
233+
if (!versionHeight.HasValue)
234+
{
235+
var baseVersion = workingCopyVersionOptions?.Version?.Version;
236+
versionHeight = GetVersionHeight(headCommit, repoRelativeProjectDirectory, baseVersion);
237+
}
238+
239+
Version result = GetIdAsVersionHelper(headCommit, workingCopyVersionOptions, versionHeight.Value);
223240
return result;
224241
}
225242

@@ -367,7 +384,7 @@ public static string FindLibGit2NativeBinaries(string basePath)
367384
/// <param name="expectedVersion">The version to test for in the commit</param>
368385
/// <param name="repoRelativeProjectDirectory">The repo-relative directory from which <paramref name="expectedVersion"/> was originally calculated.</param>
369386
/// <returns><c>true</c> if the <paramref name="commit"/> matches the major and minor components of <paramref name="expectedVersion"/>.</returns>
370-
private static bool CommitMatchesMajorMinorVersion(Commit commit, Version expectedVersion, string repoRelativeProjectDirectory)
387+
internal static bool CommitMatchesMajorMinorVersion(this Commit commit, Version expectedVersion, string repoRelativeProjectDirectory)
371388
{
372389
Requires.NotNull(commit, nameof(commit));
373390
Requires.NotNull(expectedVersion, nameof(expectedVersion));
@@ -494,11 +511,7 @@ private static void AddReachableCommitsFrom(Commit startingCommit, HashSet<Commi
494511
/// </summary>
495512
/// <param name="commit">The commit whose ID and position in history is to be encoded.</param>
496513
/// <param name="versionOptions">The version options applicable at this point (either from commit or working copy).</param>
497-
/// <param name="repoRelativeProjectDirectory">The repo-relative project directory for which to calculate the version.</param>
498-
/// <param name="versionHeight">
499-
/// The version height, previously calculated by a call to <see cref="GetVersionHeight(Commit, string)"/>
500-
/// with the same value for <paramref name="repoRelativeProjectDirectory"/>.
501-
/// </param>
514+
/// <param name="versionHeight">The version height, previously calculated by a call to <see cref="GetVersionHeight(Commit, string, Version)"/>.</param>
502515
/// <returns>
503516
/// A version whose <see cref="Version.Build"/> and
504517
/// <see cref="Version.Revision"/> components are calculated based on the commit.
@@ -509,7 +522,7 @@ private static void AddReachableCommitsFrom(Commit startingCommit, HashSet<Commi
509522
/// component is the first four bytes of the git commit id (forced to be a positive integer).
510523
/// </remarks>
511524
/// <returns></returns>
512-
private static Version GetIdAsVersionHelper(Commit commit, VersionOptions versionOptions, string repoRelativeProjectDirectory, int? versionHeight)
525+
internal static Version GetIdAsVersionHelper(this Commit commit, VersionOptions versionOptions, int versionHeight)
513526
{
514527
var baseVersion = versionOptions?.Version?.Version ?? Version0;
515528
int buildNumber = baseVersion.Build;
@@ -522,14 +535,8 @@ private static Version GetIdAsVersionHelper(Commit commit, VersionOptions versio
522535
// The build number is set to the git height. This helps ensure that
523536
// within a major.minor release, each patch has an incrementing integer.
524537
// The revision is set to the first two bytes of the git commit ID.
525-
if (!versionHeight.HasValue)
526-
{
527-
versionHeight = commit != null
528-
? commit.GetHeight(c => CommitMatchesMajorMinorVersion(c, baseVersion, repoRelativeProjectDirectory))
529-
: 0;
530-
}
531538

532-
int adjustedVersionHeight = versionHeight.Value == 0 ? 0 : versionHeight.Value + (versionOptions?.BuildNumberOffset ?? 0);
539+
int adjustedVersionHeight = versionHeight == 0 ? 0 : versionHeight + (versionOptions?.BuildNumberOffset ?? 0);
533540
Verify.Operation(adjustedVersionHeight <= MaximumBuildNumberOrRevisionComponent, "Git height is {0}, which is greater than the maximum allowed {0}.", adjustedVersionHeight, MaximumBuildNumberOrRevisionComponent);
534541

535542
if (buildNumber < 0)

src/NerdBank.GitVersioning/VersionOracle.cs

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
using System.Globalization;
66
using System.IO;
77
using System.Linq;
8-
using System.Text;
98
using System.Text.RegularExpressions;
10-
using System.Threading.Tasks;
119
using Validation;
1210

1311
/// <summary>
@@ -21,9 +19,9 @@ public class VersionOracle
2119
private static readonly Regex NumericIdentifierRegex = new Regex(@"(?<![\w-])(\d+)(?![\w-])");
2220

2321
/// <summary>
24-
/// The cloud build suppport, if any.
22+
/// The 0.0 version.
2523
/// </summary>
26-
private readonly ICloudBuild cloudBuild;
24+
private static readonly Version Version0 = new Version(0, 0);
2725

2826
/// <summary>
2927
/// Initializes a new instance of the <see cref="VersionOracle"/> class.
@@ -51,24 +49,33 @@ public static VersionOracle Create(string projectDirectory, string gitRepoDirect
5149
/// </summary>
5250
public VersionOracle(string projectDirectory, LibGit2Sharp.Repository repo, ICloudBuild cloudBuild)
5351
{
54-
this.cloudBuild = cloudBuild;
55-
this.VersionOptions =
56-
VersionFile.GetVersion(repo, projectDirectory) ??
57-
VersionFile.GetVersion(projectDirectory);
58-
5952
var repoRoot = repo?.Info?.WorkingDirectory?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
6053
var relativeRepoProjectDirectory = !string.IsNullOrWhiteSpace(repoRoot)
6154
? projectDirectory.Substring(repoRoot.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
6255
: null;
6356

6457
var commit = repo?.Head.Commits.FirstOrDefault();
58+
59+
var committedVersion = VersionFile.GetVersion(commit, relativeRepoProjectDirectory);
60+
61+
var workingVersion = VersionFile.GetVersion(projectDirectory);
62+
63+
this.VersionOptions = committedVersion ?? workingVersion;
64+
6565
this.GitCommitId = commit?.Id.Sha ?? cloudBuild?.GitCommitId ?? null;
66-
this.VersionHeight = repo?.GetVersionHeight(relativeRepoProjectDirectory) ?? 0;
66+
this.VersionHeight = CalculateVersionHeight(relativeRepoProjectDirectory, commit, committedVersion, workingVersion);
6767
this.BuildingRef = cloudBuild?.BuildingTag ?? cloudBuild?.BuildingBranch ?? repo?.Head.CanonicalName;
6868

6969
// Override the typedVersion with the special build number and revision components, when available.
70-
this.Version = repo?.GetIdAsVersion(relativeRepoProjectDirectory, this.VersionHeight) ?? this.VersionOptions?.Version.Version;
71-
this.Version = this.Version ?? new Version(0, 0);
70+
if (repo != null)
71+
{
72+
this.Version = GetIdAsVersion(commit, committedVersion, workingVersion, this.VersionHeight);
73+
}
74+
else
75+
{
76+
this.Version = this.VersionOptions?.Version.Version ?? Version0;
77+
}
78+
7279
this.VersionHeightOffset = this.VersionOptions?.BuildNumberOffset ?? 0;
7380

7481
this.PrereleaseVersion = ReplaceMacros(this.VersionOptions?.Version.Prerelease ?? string.Empty);
@@ -391,5 +398,41 @@ private static string MakePrereleaseSemVer1Compliant(string prerelease, int padd
391398

392399
return semver1;
393400
}
401+
402+
private static int CalculateVersionHeight(string relativeRepoProjectDirectory, LibGit2Sharp.Commit headCommit, VersionOptions committedVersion, VersionOptions workingVersion)
403+
{
404+
var headCommitVersion = committedVersion?.Version?.Version ?? Version0;
405+
406+
if (IsVersionFileChangedInWorkingTree(committedVersion, workingVersion))
407+
{
408+
var workingCopyVersion = workingVersion?.Version?.Version;
409+
410+
if (workingCopyVersion == null || !workingCopyVersion.Equals(headCommitVersion))
411+
{
412+
// The working copy has changed the major.minor version.
413+
// So by definition the version height is 0, since no commit represents it yet.
414+
return 0;
415+
}
416+
}
417+
418+
return headCommit?.GetHeight(c => c.CommitMatchesMajorMinorVersion(headCommitVersion, relativeRepoProjectDirectory)) ?? 0;
419+
}
420+
421+
private static Version GetIdAsVersion(LibGit2Sharp.Commit headCommit, VersionOptions committedVersion, VersionOptions workingVersion, int versionHeight)
422+
{
423+
var version = IsVersionFileChangedInWorkingTree(committedVersion, workingVersion) ? workingVersion : committedVersion;
424+
425+
return headCommit.GetIdAsVersionHelper(version, versionHeight);
426+
}
427+
428+
private static bool IsVersionFileChangedInWorkingTree(VersionOptions committedVersion, VersionOptions workingVersion)
429+
{
430+
if (workingVersion != null)
431+
{
432+
return !EqualityComparer<VersionOptions>.Default.Equals(workingVersion, committedVersion);
433+
}
434+
435+
return false; // a missing working version is allowed and not a change.
436+
}
394437
}
395438
}

0 commit comments

Comments
 (0)