Skip to content

Commit 7f4b4d9

Browse files
authored
Merge pull request #471 from dotnet/fixSO
Fix StackOverflowException that can break a build
2 parents 2819578 + ddb9510 commit 7f4b4d9

File tree

2 files changed

+100
-56
lines changed

2 files changed

+100
-56
lines changed

src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs

+14-3
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,17 @@ public void GetVersionHeight_ProjectDirectoryIsMoved()
400400
Assert.Equal(1, this.Repo.GetVersionHeight("new-project-dir"));
401401
}
402402

403+
[Fact(Skip = "Slow test")]
404+
public void GetVersionHeight_VeryLongHistory()
405+
{
406+
this.WriteVersionFile();
407+
408+
// Make a *lot* of commits
409+
this.AddCommits(2000);
410+
411+
this.Repo.GetVersionHeight();
412+
}
413+
403414
[Fact]
404415
public void GetCommitsFromVersion_WithPathFilters()
405416
{
@@ -775,16 +786,16 @@ public void GetIdAsVersion_MigrationFromVersionTxtToJson()
775786
[SkippableFact] // Skippable, only run test on specific machine
776787
public void TestBiggerRepo()
777788
{
778-
var testBiggerRepoPath = @"C:\Users\andrew\git\NerdBank.GitVersioning";
789+
var testBiggerRepoPath = @"D:\git\NerdBank.GitVersioning";
779790
Skip.If(!Directory.Exists(testBiggerRepoPath));
780791

781792
using (this.Repo = new Repository(testBiggerRepoPath))
782793
{
783794
foreach (var commit in this.Repo.Head.Commits)
784795
{
785-
var version = commit.GetIdAsVersion();
796+
var version = commit.GetIdAsVersion("src");
786797
this.Logger.WriteLine($"commit {commit.Id} got version {version}");
787-
var backAgain = this.Repo.GetCommitFromVersion(version);
798+
var backAgain = this.Repo.GetCommitFromVersion(version, "src");
788799
Assert.Equal(commit, backAgain);
789800
}
790801
}

src/NerdBank.GitVersioning/GitExtensions.cs

+86-53
Original file line numberDiff line numberDiff line change
@@ -708,84 +708,117 @@ private static string EncodeAsHex(byte[] buffer)
708708
/// Gets the number of commits in the longest single path between
709709
/// the specified branch's head and the most distant ancestor (inclusive).
710710
/// </summary>
711-
/// <param name="commit">The commit to measure the height of.</param>
711+
/// <param name="startingCommit">The commit to measure the height of.</param>
712712
/// <param name="tracker">The caching tracker for storing or fetching version information per commit.</param>
713713
/// <param name="continueStepping">
714714
/// A function that returns <c>false</c> when we reach a commit that
715715
/// should not be included in the height calculation.
716716
/// May be null to count the height to the original commit.
717717
/// </param>
718718
/// <returns>The height of the branch.</returns>
719-
private static int GetCommitHeight(Commit commit, GitWalkTracker tracker, Func<Commit, bool> continueStepping)
719+
private static int GetCommitHeight(Commit startingCommit, GitWalkTracker tracker, Func<Commit, bool> continueStepping)
720720
{
721-
Requires.NotNull(commit, nameof(commit));
721+
Requires.NotNull(startingCommit, nameof(startingCommit));
722722
Requires.NotNull(tracker, nameof(tracker));
723723

724-
if (!tracker.TryGetVersionHeight(commit, out int height))
724+
if (continueStepping is object && !continueStepping(startingCommit))
725+
{
726+
return 0;
727+
}
728+
729+
var commitsToEvaluate = new Stack<Commit>();
730+
bool TryCalculateHeight(Commit commit)
725731
{
726-
if (continueStepping == null || continueStepping(commit))
732+
// Get max height among all parents, or schedule all missing parents for their own evaluation and return false.
733+
int maxHeightAmongParents = 0;
734+
bool parentMissing = false;
735+
foreach (Commit parent in commit.Parents)
736+
{
737+
if (!tracker.TryGetVersionHeight(parent, out int parentHeight))
738+
{
739+
if (continueStepping is object && !continueStepping(parent))
740+
{
741+
// This parent isn't supposed to contribute to height.
742+
continue;
743+
}
744+
745+
commitsToEvaluate.Push(parent);
746+
parentMissing = true;
747+
}
748+
else
749+
{
750+
maxHeightAmongParents = Math.Max(maxHeightAmongParents, parentHeight);
751+
}
752+
}
753+
754+
if (parentMissing)
727755
{
728-
var versionOptions = tracker.GetVersion(commit);
729-
var pathFilters = versionOptions?.PathFilters;
756+
return false;
757+
}
730758

731-
var includePaths =
732-
pathFilters
733-
?.Where(filter => !filter.IsExclude)
734-
.Select(filter => filter.RepoRelativePath)
735-
.ToList();
759+
var versionOptions = tracker.GetVersion(commit);
760+
var pathFilters = versionOptions?.PathFilters;
736761

737-
var excludePaths = pathFilters?.Where(filter => filter.IsExclude).ToList();
762+
var includePaths =
763+
pathFilters
764+
?.Where(filter => !filter.IsExclude)
765+
.Select(filter => filter.RepoRelativePath)
766+
.ToList();
738767

739-
var ignoreCase = commit.GetRepository().Config.Get<bool>("core.ignorecase")?.Value ?? false;
740-
bool ContainsRelevantChanges(IEnumerable<TreeEntryChanges> changes) =>
741-
excludePaths.Count == 0
742-
? changes.Any()
743-
// If there is a single change that isn't excluded,
744-
// then this commit is relevant.
745-
: changes.Any(change => !excludePaths.Any(exclude => exclude.Excludes(change.Path, ignoreCase)));
768+
var excludePaths = pathFilters?.Where(filter => filter.IsExclude).ToList();
746769

747-
height = 1;
770+
var ignoreCase = commit.GetRepository().Config.Get<bool>("core.ignorecase")?.Value ?? false;
771+
bool ContainsRelevantChanges(IEnumerable<TreeEntryChanges> changes) =>
772+
excludePaths.Count == 0
773+
? changes.Any()
774+
// If there is a single change that isn't excluded,
775+
// then this commit is relevant.
776+
: changes.Any(change => !excludePaths.Any(exclude => exclude.Excludes(change.Path, ignoreCase)));
748777

749-
if (includePaths != null)
750-
{
751-
// If there are no include paths, or any of the include
752-
// paths refer to the root of the repository, then do not
753-
// filter the diff at all.
754-
var diffInclude =
755-
includePaths.Count == 0 || pathFilters.Any(filter => filter.IsRoot)
756-
? null
757-
: includePaths;
758-
759-
// If the diff between this commit and any of its parents
760-
// does not touch a path that we care about, don't bump the
761-
// height.
762-
var relevantCommit =
763-
commit.Parents.Any()
764-
? commit.Parents.Any(parent => ContainsRelevantChanges(commit.GetRepository().Diff
765-
.Compare<TreeChanges>(parent.Tree, commit.Tree, diffInclude)))
766-
: ContainsRelevantChanges(commit.GetRepository().Diff
767-
.Compare<TreeChanges>(null, commit.Tree, diffInclude));
768-
769-
if (!relevantCommit)
770-
{
771-
height = 0;
772-
}
773-
}
778+
int height = 1;
774779

775-
if (commit.Parents.Any())
780+
if (includePaths != null)
781+
{
782+
// If there are no include paths, or any of the include
783+
// paths refer to the root of the repository, then do not
784+
// filter the diff at all.
785+
var diffInclude =
786+
includePaths.Count == 0 || pathFilters.Any(filter => filter.IsRoot)
787+
? null
788+
: includePaths;
789+
790+
// If the diff between this commit and any of its parents
791+
// does not touch a path that we care about, don't bump the
792+
// height.
793+
var relevantCommit =
794+
commit.Parents.Any()
795+
? commit.Parents.Any(parent => ContainsRelevantChanges(commit.GetRepository().Diff
796+
.Compare<TreeChanges>(parent.Tree, commit.Tree, diffInclude)))
797+
: ContainsRelevantChanges(commit.GetRepository().Diff
798+
.Compare<TreeChanges>(null, commit.Tree, diffInclude));
799+
800+
if (!relevantCommit)
776801
{
777-
height += commit.Parents.Max(p => GetCommitHeight(p, tracker, continueStepping));
802+
height = 0;
778803
}
779804
}
780-
else
805+
806+
tracker.RecordHeight(commit, height + maxHeightAmongParents);
807+
return true;
808+
}
809+
810+
commitsToEvaluate.Push(startingCommit);
811+
while (commitsToEvaluate.Count > 0)
812+
{
813+
Commit commit = commitsToEvaluate.Peek();
814+
if (tracker.TryGetVersionHeight(commit, out _) || TryCalculateHeight(commit))
781815
{
782-
height = 0;
816+
commitsToEvaluate.Pop();
783817
}
784-
785-
tracker.RecordHeight(commit, height);
786818
}
787819

788-
return height;
820+
Assumes.True(tracker.TryGetVersionHeight(startingCommit, out int result));
821+
return result;
789822
}
790823

791824
/// <summary>

0 commit comments

Comments
 (0)