diff --git a/CommitsSinceVersionSource.txt b/CommitsSinceVersionSource.txt new file mode 100644 index 0000000000..d807d974aa --- /dev/null +++ b/CommitsSinceVersionSource.txt @@ -0,0 +1,207 @@ + + +@startuml +skinparam participantMargin 10 +skinparam noteMargin 10 +skinparam shadowing false + +participant main +participant develop +participant feature_f1 +participant release_1_1_0 +participant feature_f2 +participant release_1_2_0 +participant hotfix_hf + +note right of main #LightBlue +📝 Commit: A +Version Source: A (initial commit) +Commits beyond Version Source: none +Version: 1.0.0 +end note + +main --> develop: 🌿 branch develop from A + +note right of develop #LightSkyBlue +📝 Commit: B +Version Source: A (already merged to develop) +Commits beyond Version Source: B +Version: 1.1.0-alpha.1 +end note + +develop --> feature_f1: 🌿 branch feature/f1 from B + +note right of feature_f1 #LightGreen +📝 Commit: C +Version Source: B (already merged to develop) +Commits beyond Version Source: C +Version: 1.1.0-f1.1+2 +end note + +feature_f1 --> develop: 🔀 E (merge f1 → develop) + +note right of develop #Khaki +📝 Commit: E +Version Source: E (develop tip) +Commits beyond Version Source: none +Version: 1.1.0-alpha.3 +end note + +develop --> release_1_1_0: 🌿 branch release/1.1.0 from E + +note right of release_1_1_0 #PaleGreen +📝 Commit: F +Version Source: E (already merged to develop) +Commits beyond Version Source: F +Version: 1.1.0-beta.1+4 +end note + +release_1_1_0 --> main: 🔀 G (merge release/1.1.0 → main) + +note right of main #Gold +📝 Commit: G +🏷 Tag: 1.1.0 +Version Source: G (main tip) +Commits beyond Version Source: none +Version: 1.1.0-5 +end note + +main -> main: 🏷 tag 1.1.0 + +note right of main #Gold +Version: 1.1.0 +end note + +release_1_1_0 --> develop: 🔀 H (merge release/1.1.0 → develop) + +note right of develop #Khaki +📝 Commit: H +Version Source: H (develop tip) +Commits beyond Version Source: none +Version: 1.2.0-alpha.1 +end note + +develop --> feature_f2: 🌿 branch feature/f2 from H + +note right of feature_f2 #LightGreen +📝 Commit: I +Version Source: H (already merged to develop) +Commits beyond Version Source: I +Version: 1.2.0-f2.1+2 +end note + +feature_f2 -> feature_f2: Commit 'feature 2 additional commit (J)' + +note right of feature_f2 #LightGreen +📝 Commit: J +Version Source: H (already merged to develop) +Commits beyond Version Source: J +Version: 1.3.0-f2.1+0 +end note + +feature_f2 --> develop: 🔀 K (merge f2 → develop) + +note right of develop #Khaki +📝 Commit: K +Version Source: K (develop tip) +Commits beyond Version Source: none +Version: 1.2.0-alpha.3 +end note + +develop --> release_1_2_0: 🌿 branch release/1.2.0 from K + +note right of release_1_2_0 #PaleGreen +📝 Commit: L +Version Source: K (already merged to develop) +Commits beyond Version Source: L +Beta parent: B.. E,F,G,H,I,J,K +Version: 1.2.0-beta.1+8 +end note + +release_1_2_0 --> main: 🔀 M (merge release/1.2.0 → main) + +note right of main #Gold +📝 Commit: M +🏷 Tag: 1.2.0 +Version Source: M (main tip) +Commits beyond Version Source: none +Version: 1.2.0-5 +end note + +main -> main: 🏷 tag 1.2.0 + +note right of main #Gold +Version: 1.2.0 +end note + +release_1_2_0 --> develop: 🔀 N (merge release/1.2.0 → develop) + +note right of develop #Khaki +📝 Commit: N +Version Source: N (develop tip) +Commits beyond Version Source: none +Version: 1.3.0-alpha.1 +end note + +main --> hotfix_hf: 🌿 branch hotfix/hf from M + +note right of hotfix_hf #LightCoral +📝 Commit: O +Version Source: M (already merged to main) +Commits beyond Version Source: O +Version: 1.2.1-beta.1+1 +end note + +hotfix_hf --> main: 🔀 P (merge hotfix → main) + +note right of main #Gold +📝 Commit: P +🏷 Tag: 1.2.1 +Version Source: P (main tip) +Commits beyond Version Source: none +Version: 1.2.1-2 +end note + +main -> main: 🏷 tag 1.2.1 + +note right of main #Gold +Version: 1.2.1 +end note + +hotfix_hf --> develop: 🔀 Q (merge hotfix → develop) + +note right of develop #Khaki +📝 Commit: Q +Version Source: Q (develop tip) +Commits beyond Version Source: none +Version: 1.3.0-alpha.2 +end note + +note right of feature_f2 #Orange +⬅ Returned to feature/f2 at Commit J +Version Source: J (already merged to develop) +Commits beyond Version Source: none +Version recalculates to: 1.3.0-f2.1+0 +end note + +note right of feature_f2 #LightGreen +Version: 1.3.0-f2.1+0 +end note + +note right of feature_f2 #LightGreen +📝 Commit: R +Version Source: J (already merged to develop) +Commits beyond Version Source: R +Version: 1.3.0-f2.1+1 +end note + +feature_f2 --> develop: --> rebase R onto develop tip Q + +note right of feature_f2 #Thistle +📝 Commit: R′ +Version Source: Q (develop tip) +Commits beyond Version Source: R′ +Version: 1.3.0-f2.1+1 (after rebase) +end note + +@enduml diff --git a/src/GitVersion.Core.Tests/Core/RepositoryStoreTests.cs b/src/GitVersion.Core.Tests/Core/RepositoryStoreTests.cs index 9e58ad8bcb..fb606b1e2b 100644 --- a/src/GitVersion.Core.Tests/Core/RepositoryStoreTests.cs +++ b/src/GitVersion.Core.Tests/Core/RepositoryStoreTests.cs @@ -86,33 +86,56 @@ public void FindsCorrectMergeBaseForForwardMergeMovesOn() //*91bf945 58 minutes ago(main) using var fixture = new EmptyRepositoryFixture(); fixture.MakeACommit("initial"); + fixture.AssertFullSemver("0.0.1-1"); + fixture.AssertCommitsSinceVersionSource(1); fixture.BranchTo("develop"); + fixture.AssertFullSemver("0.1.0-alpha.1"); var fixtureRepository = fixture.Repository.ToGitRepository(); var expectedReleaseMergeBase = fixtureRepository.Head.Tip; + fixture.SequenceDiagram.NoteOver(string.Join(System.Environment.NewLine, ("Expected Release Merge Base" + System.Environment.NewLine + System.Environment.NewLine + + "This is the first common ancestor of both develop and release, from release's perspective.").SplitIntoLines(30)), "main", "develop"); // Create release from develop fixture.BranchTo("release-2.0.0"); + fixture.AssertFullSemver("2.0.0-beta.1+1"); // Make some commits on release fixture.MakeACommit("release 1"); + fixture.AssertCommitsSinceVersionSource(2); + fixture.AssertFullSemver("2.0.0-beta.1+2"); fixture.MakeACommit("release 2"); + fixture.AssertCommitsSinceVersionSource(3); + fixture.AssertFullSemver("2.0.0-beta.1+3"); + fixture.SequenceDiagram.NoteOver(string.Join(System.Environment.NewLine, ("Expected Develop Merge Base" + System.Environment.NewLine + System.Environment.NewLine + + "This is a common ancestor from develop's perspective because it is aware of the merge from release." + System.Environment.NewLine + + "It is NOT an common ancestor from release's perspective because release is NOT aware of the merge to develop.").SplitIntoLines(30)), "release-2.0.0"); var expectedDevelopMergeBase = fixtureRepository.Head.Tip; // First forward merge release to develop fixture.Checkout("develop"); fixture.MergeNoFF("release-2.0.0"); + fixture.AssertFullSemver("2.1.0-alpha.3"); + fixture.AssertCommitsSinceVersionSource(3); // Make some new commit on release fixture.Checkout("release-2.0.0"); fixture.MakeACommit("release 3 - after first merge"); + fixture.AssertFullSemver("2.0.0-beta.1+4"); + fixture.AssertCommitsSinceVersionSource(4); // Make new commit on develop fixture.Checkout("develop"); + fixture.AssertFullSemver("2.1.0-alpha.3"); // Checkout to release (no new commits) fixture.MakeACommit("develop after merge"); + fixture.AssertFullSemver("2.1.0-alpha.4"); + fixture.AssertCommitsSinceVersionSource(4); // Checkout to release (no new commits) fixture.Checkout("release-2.0.0"); + fixture.SequenceDiagram.NoteOver("Checkout release-2.0.0 again", "release-2.0.0"); + fixture.AssertFullSemver("2.0.0-beta.1+4"); + fixture.AssertCommitsSinceVersionSource(4); var develop = fixtureRepository.FindBranch("develop"); var release = fixtureRepository.FindBranch("release-2.0.0"); diff --git a/src/GitVersion.Core.Tests/Extensions/GitRepositoryTestingExtensions.cs b/src/GitVersion.Core.Tests/Extensions/GitRepositoryTestingExtensions.cs index 1dd6e242c8..97fad3e742 100644 --- a/src/GitVersion.Core.Tests/Extensions/GitRepositoryTestingExtensions.cs +++ b/src/GitVersion.Core.Tests/Extensions/GitRepositoryTestingExtensions.cs @@ -144,18 +144,55 @@ public static void WriteVersionVariables(this RepositoryFixtureBase fixture, str } public static void AssertFullSemver(this RepositoryFixtureBase fixture, string fullSemver, - IGitVersionConfiguration? configuration = null, IRepository? repository = null, string? commitId = null, bool onlyTrackedBranches = true, string? targetBranch = null) + IGitVersionConfiguration? configuration = null, IRepository? repository = null, string? commitId = null, bool onlyTrackedBranches = true, string? targetBranch = null, string? customMessage = null) { repository ??= fixture.Repository; var variables = GetVersion(fixture, configuration, repository, commitId, onlyTrackedBranches, targetBranch); - variables.FullSemVer.ShouldBe(fullSemver); + variables.FullSemVer.ShouldBe(fullSemver, customMessage); if (commitId == null) { fixture.SequenceDiagram.NoteOver(fullSemver, repository.Head.FriendlyName, color: "#D3D3D3"); } } + public static void AssertCommitsSinceVersionSource(this RepositoryFixtureBase fixture, int commitsSinceVersionSourceExpected, + IGitVersionConfiguration? configuration = null, IRepository? repository = null, string? commitId = null, bool onlyTrackedBranches = true, string? targetBranch = null, string? customMessage = null) + { + repository ??= fixture.Repository; + + var variables = GetVersion(fixture, configuration, repository, commitId, onlyTrackedBranches, targetBranch); + variables.CommitsSinceVersionSource.ShouldBe(commitsSinceVersionSourceExpected.ToString(), customMessage); + if (commitId == null) + { + var message = new StringBuilder(); + var versionSourceLabel = fixture.SequenceDiagram.GetOrAddSourceLabel(variables.VersionSourceSha); + message.AppendLine($" Commit: {fixture.SequenceDiagram.GetOrAddLabel(variables.Sha)}"); + message.Append($" Version source: {versionSourceLabel}"); + if (commitsSinceVersionSourceExpected != 0 && variables.CommitsSinceVersionSourceList != null) + { + bool isFirst = true; + foreach (var sha in variables.CommitsSinceVersionSourceList.Split(", ").Reverse()) + { + if (isFirst) + { + message.Append(System.Environment.NewLine); + message.Append($"Commits since version source: {variables.CommitsSinceVersionSource} - "); + isFirst = false; + } + else + { + message.Append(", "); + } + + message.Append($"{fixture.SequenceDiagram.GetOrAddLabel(sha)}"); + } + } + + fixture.SequenceDiagram.NoteOver(message.ToString(), repository.Head.FriendlyName, color: "#A9B7C6"); + } + } + /// /// Simulates running on build server /// diff --git a/src/GitVersion.Core.Tests/Helpers/CommitLabelGeneratorTests.cs b/src/GitVersion.Core.Tests/Helpers/CommitLabelGeneratorTests.cs new file mode 100644 index 0000000000..0343ad7d04 --- /dev/null +++ b/src/GitVersion.Core.Tests/Helpers/CommitLabelGeneratorTests.cs @@ -0,0 +1,94 @@ +namespace GitVersion.Testing.Helpers; + +[TestFixture] +public class CommitLabelGeneratorTests +{ + [TestCase("")] + [TestCase(" ")] + public void GetOrAdd_ShouldThrow_WhenKeyIsEmptyOrWhitespace(string key) + { + var sut = new CommitLabelGenerator(); + var ex = Should.Throw(() => sut.GetOrAdd(key)); + ex.Message.ShouldContain("SHA cannot be empty or whitespace"); + ex.ParamName.ShouldBe("key"); + } + + [Test] + public void GetOrAdd_ShouldReturnNA_WhenKeyIsNA() + { + var sut = new CommitLabelGenerator(); + sut.GetOrAdd("N/A").ShouldBe("N/A"); + } + + [Test] + public void GetOrAdd_ShouldAssignLabels_Sequentially() + { + var sut = new CommitLabelGenerator(); + + sut.GetOrAdd("sha1").ShouldBe("A"); + sut.GetOrAdd("sha2").ShouldBe("B"); + sut.GetOrAdd("sha3").ShouldBe("C"); + } + + [Test] + public void GetOrAdd_ShouldReturnSameLabel_OnRepeatedCalls() + { + var sut = new CommitLabelGenerator(); + sut.GetOrAdd("sha1").ShouldBe(sut.GetOrAdd("sha1")); + } + + [TestCase("")] + [TestCase(" ")] + public void GetOrAddRoot_ShouldThrow_WhenKeyIsEmptyOrWhitespace(string key) + { + var sut = new CommitLabelGenerator(); + var ex = Should.Throw(() => sut.GetOrAddRoot(key)); + ex.Message.ShouldContain("Version source SHA cannot be empty or whitespace"); + ex.ParamName.ShouldBe("versionSourceSha"); + } + + [Test] + public void GetOrAddRoot_ShouldReturnNA_WhenKeyIsNA() + { + var sut = new CommitLabelGenerator(); + sut.GetOrAddRoot("N/A").ShouldBe("N/A"); + } + + [Test] + public void GetOrAddRoot_ShouldAssignRootLabels_Sequentially() + { + var sut = new CommitLabelGenerator(); + + sut.GetOrAddRoot("rootsha1").ShouldBe("RootA"); + sut.GetOrAddRoot("rootsha2").ShouldBe("RootB"); + sut.GetOrAddRoot("rootsha3").ShouldBe("RootC"); + } + + [Test] + public void GetOrAddRoot_ShouldReturnSameLabel_OnRepeatedCalls() + { + var sut = new CommitLabelGenerator(); + sut.GetOrAddRoot("rootsha1").ShouldBe(sut.GetOrAddRoot("rootsha1")); + } + + [Test] + public void GetOrAddRoot_And_GetOrAdd_ShouldMaintainSequenceLabels() + { + var sut = new CommitLabelGenerator(); + + sut.GetOrAddRoot("sha1").ShouldBe("RootA"); + sut.GetOrAdd("sha2").ShouldBe("B"); + sut.GetOrAddRoot("sha3").ShouldBe("RootC"); + } + + [Test] + public void GetOrAddRoot_And_GetOrAdd_ShouldAlign() + { + var sut = new CommitLabelGenerator(); + + sut.GetOrAddRoot("sha1").ShouldBe("RootA"); + sut.GetOrAdd("sha1").ShouldBe("RootA"); + sut.GetOrAdd("sha2").ShouldBe("B"); + sut.GetOrAddRoot("sha2").ShouldBe("B"); + } +} diff --git a/src/GitVersion.Core.Tests/Helpers/TestableGitVersionVariables.cs b/src/GitVersion.Core.Tests/Helpers/TestableGitVersionVariables.cs index 5670ca86a3..0237f5b4ec 100644 --- a/src/GitVersion.Core.Tests/Helpers/TestableGitVersionVariables.cs +++ b/src/GitVersion.Core.Tests/Helpers/TestableGitVersionVariables.cs @@ -26,4 +26,5 @@ internal record TestableGitVersionVariables() : GitVersionVariables("", "", "", "", + "", ""); diff --git a/src/GitVersion.Core.Tests/IntegrationTests/GitflowScenarios.cs b/src/GitVersion.Core.Tests/IntegrationTests/GitflowScenarios.cs index a466855b43..28e8f8f7e6 100644 --- a/src/GitVersion.Core.Tests/IntegrationTests/GitflowScenarios.cs +++ b/src/GitVersion.Core.Tests/IntegrationTests/GitflowScenarios.cs @@ -1,13 +1,27 @@ +using GitVersion.Configuration; using GitVersion.Core.Tests.Helpers; +using GitVersion.Helpers; +using GitVersion.Logging; +using LibGit2Sharp; +using Microsoft.Extensions.DependencyInjection; namespace GitVersion.Core.Tests.IntegrationTests; [TestFixture] public class GitflowScenarios : TestBase { + private readonly ILog log; + + public GitflowScenarios() + { + var sp = ConfigureServices(); + this.log = sp.GetRequiredService(); + } + [Test] public void GitflowComplexExample() { + var keepBranches = true; const string developBranch = "develop"; const string feature1Branch = "feature/f1"; const string feature2Branch = "feature/f2"; @@ -15,68 +29,166 @@ public void GitflowComplexExample() const string release2Branch = "release/1.2.0"; const string hotfixBranch = "hotfix/hf"; - using var fixture = new BaseGitFlowRepositoryFixture("1.0.0"); - fixture.AssertFullSemver("1.1.0-alpha.1"); + var configuration = GitFlowConfigurationBuilder.New.Build(); + + using var fixture = new BaseGitFlowRepositoryFixture(InitialMainAction, deleteOnDispose: false); + var fullSemver = "1.1.0-alpha.1"; + fixture.AssertFullSemver(fullSemver, configuration); + fixture.AssertCommitsSinceVersionSource(1, configuration); // Feature 1 fixture.BranchTo(feature1Branch); - fixture.MakeACommit("added feature 1"); - fixture.AssertFullSemver("1.1.0-f1.1+2"); + + fixture.MakeACommit($"added feature 1 >> {fullSemver}"); + fullSemver = "1.1.0-f1.1+2"; + fixture.AssertFullSemver(fullSemver, configuration); + fixture.AssertCommitsSinceVersionSource(2, configuration); fixture.Checkout(developBranch); fixture.MergeNoFF(feature1Branch); - fixture.Repository.Branches.Remove(fixture.Repository.Branches[feature1Branch]); - fixture.AssertFullSemver("1.1.0-alpha.3"); + if (!keepBranches) fixture.Repository.Branches.Remove(fixture.Repository.Branches[feature1Branch]); + fixture.AssertFullSemver("1.1.0-alpha.3", configuration); + fixture.AssertCommitsSinceVersionSource(3, configuration); // Release 1.1.0 fixture.BranchTo(release1Branch); fixture.MakeACommit("release stabilization"); - fixture.AssertFullSemver("1.1.0-beta.1+4"); + fixture.AssertFullSemver("1.1.0-beta.1+4", configuration); + fixture.AssertCommitsSinceVersionSource(4, configuration); fixture.Checkout(MainBranch); fixture.MergeNoFF(release1Branch); - fixture.AssertFullSemver("1.1.0-5"); + fixture.AssertFullSemver("1.1.0-5", configuration); + fixture.AssertCommitsSinceVersionSource(5, configuration); fixture.ApplyTag("1.1.0"); - fixture.AssertFullSemver("1.1.0"); + fixture.AssertFullSemver("1.1.0", configuration); + fixture.AssertCommitsSinceVersionSource(0, configuration); fixture.Checkout(developBranch); fixture.MergeNoFF(release1Branch); fixture.Repository.Branches.Remove(fixture.Repository.Branches[release1Branch]); - fixture.AssertFullSemver("1.2.0-alpha.1"); + fixture.AssertFullSemver("1.2.0-alpha.1", configuration); + fixture.AssertCommitsSinceVersionSource(1, configuration); // Feature 2 fixture.BranchTo(feature2Branch); - fixture.MakeACommit("added feature 2"); - fixture.AssertFullSemver("1.2.0-f2.1+2"); + fullSemver = "1.2.0-f2.1+2"; + fixture.MakeACommit($"added feature 2 >> {fullSemver}"); + fixture.AssertFullSemver(fullSemver, configuration); + fixture.AssertCommitsSinceVersionSource(2, configuration); fixture.Checkout(developBranch); fixture.MergeNoFF(feature2Branch); - fixture.Repository.Branches.Remove(fixture.Repository.Branches[feature2Branch]); - fixture.AssertFullSemver("1.2.0-alpha.3"); + if (!keepBranches) fixture.Repository.Branches.Remove(fixture.Repository.Branches[feature2Branch]); + fixture.AssertFullSemver("1.2.0-alpha.3", configuration); + fixture.AssertCommitsSinceVersionSource(3, configuration); // Release 1.2.0 fixture.BranchTo(release2Branch); - fixture.MakeACommit("release stabilization"); - fixture.AssertFullSemver("1.2.0-beta.1+8"); + fullSemver = "1.2.0-beta.1+8"; + fixture.MakeACommit($"release stabilization >> {fullSemver}"); + fixture.AssertFullSemver(fullSemver, configuration); + fixture.AssertCommitsSinceVersionSource(8, configuration); fixture.Checkout(MainBranch); fixture.MergeNoFF(release2Branch); - fixture.AssertFullSemver("1.2.0-5"); + fixture.AssertFullSemver("1.2.0-5", configuration); + fixture.AssertCommitsSinceVersionSource(5, configuration); fixture.ApplyTag("1.2.0"); - fixture.AssertFullSemver("1.2.0"); + fixture.AssertFullSemver("1.2.0", configuration); + fixture.AssertCommitsSinceVersionSource(0, configuration); fixture.Checkout(developBranch); fixture.MergeNoFF(release2Branch); - fixture.Repository.Branches.Remove(fixture.Repository.Branches[release2Branch]); - fixture.AssertFullSemver("1.3.0-alpha.1"); + if (!keepBranches) + { + fixture.Repository.Branches.Remove(fixture.Repository.Branches[release2Branch]); + } + fixture.AssertFullSemver("1.3.0-alpha.1", configuration); + fixture.AssertCommitsSinceVersionSource(1, configuration); // Hotfix fixture.Checkout(MainBranch); fixture.BranchTo(hotfixBranch); - fixture.MakeACommit("added hotfix"); - fixture.AssertFullSemver("1.2.1-beta.1+1"); + fullSemver = "1.2.1-beta.1+1"; + fixture.MakeACommit($"added hotfix >> {fullSemver}"); + fixture.AssertFullSemver(fullSemver, configuration); + fixture.AssertCommitsSinceVersionSource(1, configuration); fixture.Checkout(MainBranch); fixture.MergeNoFF(hotfixBranch); - fixture.AssertFullSemver("1.2.1-2"); + fixture.AssertFullSemver("1.2.1-2", configuration); + fixture.AssertCommitsSinceVersionSource(2, configuration); fixture.ApplyTag("1.2.1"); - fixture.AssertFullSemver("1.2.1"); + fixture.AssertFullSemver("1.2.1", configuration); + fixture.AssertCommitsSinceVersionSource(0, configuration); fixture.Checkout(developBranch); fixture.MergeNoFF(hotfixBranch); - fixture.Repository.Branches.Remove(fixture.Repository.Branches[hotfixBranch]); - fixture.AssertFullSemver("1.3.0-alpha.2"); + if (!keepBranches) + { + fixture.Repository.Branches.Remove(fixture.Repository.Branches[hotfixBranch]); + } + fixture.AssertFullSemver("1.3.0-alpha.2", configuration); + fixture.AssertCommitsSinceVersionSource(2, configuration); + + fixture.Checkout(feature2Branch); + fixture.SequenceDiagram.NoteOver($"Checkout {feature2Branch}", feature2Branch); + fixture.AssertFullSemver("1.3.0-f2.1+0", configuration); + fixture.SequenceDiagram.NoteOver( + string.Join(System.Environment.NewLine, ("Feature branches are configured to inherit version (increment: inherit)." + System.Environment.NewLine + System.Environment.NewLine + + "GitVersion uses the merge base between the feature and develop to determine the version." + System.Environment.NewLine + System.Environment.NewLine + + "As develop progresses (e.g., by releasing 1.2.0 & 1.2.1), rebuilding old feature branches can produce different versions." + System.Environment.NewLine + System.Environment.NewLine + + "Here we've checked out commit H again and now it's it's own VersionSource and produces 1.3.0-f2.1+0").SplitIntoLines(60)), feature2Branch); + fixture.AssertCommitsSinceVersionSource(0, configuration); + + fullSemver = "1.3.0-f2.1+1"; + fixture.MakeACommit( + "feature 2 additional commit after original feature has been merged to develop " + System.Environment.NewLine + + $"and release/1.2.0 has already happened >> {fullSemver}" + ); + fixture.AssertFullSemver(fullSemver, configuration); + fixture.AssertCommitsSinceVersionSource(1, configuration); + fixture.SequenceDiagram.NoteOver( + string.Join(System.Environment.NewLine, ($"We committed again to {feature2Branch}." + System.Environment.NewLine + System.Environment.NewLine + + "Why is the VersionSource no longer H but has instead jumped to N?" + System.Environment.NewLine + System.Environment.NewLine + + $"I expected this to produce {fullSemver} and it does.").SplitIntoLines(60)), feature2Branch); + + var gitRepository = fixture.Repository.ToGitRepository(); + var gitRepoMetadataProvider = new RepositoryStore(this.log, gitRepository); + // H can't be it's own ancestor, so merge base is G + fixture.SequenceDiagram.GetOrAddLabel(gitRepoMetadataProvider.FindMergeBase(gitRepository.Branches[feature2Branch], gitRepository.Branches[developBranch]).Sha).ShouldBe("G"); + fixture.SequenceDiagram.GetOrAddLabel(gitRepoMetadataProvider.FindMergeBase(gitRepository.Branches[feature2Branch], gitRepository.Branches[MainBranch]).Sha).ShouldBe("G"); + // Why is H it's own VersionSource though if after committing with H as the ancestor we get N as the VersionSource? + + fixture.SequenceDiagram.NoteOver($"Now we rebase {feature2Branch} onto {developBranch}", feature2Branch); + + var identity = new Identity( + fixture.Repository.Head.Tip.Committer.Name, + fixture.Repository.Head.Tip.Committer.Email); + var rebaseResult = fixture.Repository.Rebase.Start( + fixture.Repository.Branches[feature2Branch], + fixture.Repository.Branches[developBranch], + fixture.Repository.Branches[developBranch], + identity, + new RebaseOptions()); + while (rebaseResult != null && rebaseResult.Status != RebaseStatus.Complete) + { + rebaseResult = fixture.Repository.Rebase.Continue(identity, new RebaseOptions()); + } + + fullSemver = "1.3.0-f2.1+3"; + fixture.AssertFullSemver(fullSemver, configuration); + fixture.AssertCommitsSinceVersionSource(3, configuration); + fixture.SequenceDiagram.NoteOver( + string.Join(System.Environment.NewLine, $"Post rebase the VersionSource is again N - the last commit on {MainBranch}." + System.Environment.NewLine + System.Environment.NewLine + + $"I expected this to produce 1.3.0-f2.1+1 and have a VersionSource of O with self as one commit since VersionSource. Instead VersionSource of N produces {fullSemver}, with a count traversal that includes both L and O!".SplitIntoLines(60)), feature2Branch); + + void InitialMainAction(IRepository r) + { + if (configuration is GitVersionConfiguration concreteConfig) + { + var yaml = new ConfigurationSerializer().Serialize(concreteConfig); + const string fileName = "GitVersion.yml"; + var filePath = FileSystemHelper.Path.Combine(r.Info.Path, "..", fileName); + File.WriteAllText(filePath, yaml); + r.Index.Add(fileName); + r.Index.Write(); + } + + r.MakeATaggedCommit("1.0.0", $"Initial commit on {MainBranch}"); + } } } diff --git a/src/GitVersion.Core.Tests/VersionCalculation/VersionSourceTests.cs b/src/GitVersion.Core.Tests/VersionCalculation/VersionSourceTests.cs index f4618e6d40..3d5fa698d0 100644 --- a/src/GitVersion.Core.Tests/VersionCalculation/VersionSourceTests.cs +++ b/src/GitVersion.Core.Tests/VersionCalculation/VersionSourceTests.cs @@ -24,6 +24,7 @@ public void VersionSourceSha() var semanticVersion = nextVersionCalculator.FindVersion(); semanticVersion.BuildMetaData.VersionSourceSha.ShouldBeNull(); + semanticVersion.BuildMetaData.CommitsSinceVersionSourceList.Length.ShouldBe((int)semanticVersion.BuildMetaData.CommitsSinceVersionSource); semanticVersion.BuildMetaData.CommitsSinceVersionSource.ShouldBe(3); } @@ -39,6 +40,7 @@ public void VersionSourceShaOneCommit() var semanticVersion = nextVersionCalculator.FindVersion(); semanticVersion.BuildMetaData.VersionSourceSha.ShouldBeNull(); + semanticVersion.BuildMetaData.CommitsSinceVersionSourceList.Length.ShouldBe((int)semanticVersion.BuildMetaData.CommitsSinceVersionSource); semanticVersion.BuildMetaData.CommitsSinceVersionSource.ShouldBe(1); } @@ -58,6 +60,9 @@ public void VersionSourceShaUsingTag() var semanticVersion = nextVersionCalculator.FindVersion(); semanticVersion.BuildMetaData.VersionSourceSha.ShouldBe(secondCommitSha); +#if DEBUG + semanticVersion.BuildMetaData.CommitsSinceVersionSourceList.Length.ShouldBe((int)semanticVersion.BuildMetaData.CommitsSinceVersionSource); +#endif semanticVersion.BuildMetaData.CommitsSinceVersionSource.ShouldBe(1); } diff --git a/src/GitVersion.Core/OutputVariables/GitVersionVariables.cs b/src/GitVersion.Core/OutputVariables/GitVersionVariables.cs index 53a8b55172..b378a6e748 100644 --- a/src/GitVersion.Core/OutputVariables/GitVersionVariables.cs +++ b/src/GitVersion.Core/OutputVariables/GitVersionVariables.cs @@ -24,6 +24,7 @@ public record GitVersionVariables(string Major, string? CommitDate, string? VersionSourceSha, string? CommitsSinceVersionSource, + string? CommitsSinceVersionSourceList, string? UncommittedChanges) : IEnumerable> { internal static readonly List AvailableVariables = @@ -52,6 +53,7 @@ public record GitVersionVariables(string Major, nameof(CommitDate), nameof(VersionSourceSha), nameof(CommitsSinceVersionSource), + nameof(CommitsSinceVersionSourceList), nameof(UncommittedChanges) ]; @@ -81,6 +83,7 @@ public record GitVersionVariables(string Major, { nameof(CommitDate), CommitDate }, { nameof(VersionSourceSha), VersionSourceSha }, { nameof(CommitsSinceVersionSource), CommitsSinceVersionSource }, + { nameof(CommitsSinceVersionSourceList), CommitsSinceVersionSourceList }, { nameof(UncommittedChanges), UncommittedChanges } }; diff --git a/src/GitVersion.Core/PublicAPI.Shipped.txt b/src/GitVersion.Core/PublicAPI.Shipped.txt index 2b3133a9e8..7d55a5ccb2 100644 --- a/src/GitVersion.Core/PublicAPI.Shipped.txt +++ b/src/GitVersion.Core/PublicAPI.Shipped.txt @@ -426,6 +426,8 @@ GitVersion.OutputVariables.GitVersionVariables.CommitDate.get -> string? GitVersion.OutputVariables.GitVersionVariables.CommitDate.init -> void GitVersion.OutputVariables.GitVersionVariables.CommitsSinceVersionSource.get -> string? GitVersion.OutputVariables.GitVersionVariables.CommitsSinceVersionSource.init -> void +GitVersion.OutputVariables.GitVersionVariables.CommitsSinceVersionSourceList.get -> string? +GitVersion.OutputVariables.GitVersionVariables.CommitsSinceVersionSourceList.init -> void GitVersion.OutputVariables.GitVersionVariables.EscapedBranchName.get -> string? GitVersion.OutputVariables.GitVersionVariables.EscapedBranchName.init -> void GitVersion.OutputVariables.GitVersionVariables.FullBuildMetaData.get -> string? @@ -433,7 +435,7 @@ GitVersion.OutputVariables.GitVersionVariables.FullBuildMetaData.init -> void GitVersion.OutputVariables.GitVersionVariables.FullSemVer.get -> string! GitVersion.OutputVariables.GitVersionVariables.FullSemVer.init -> void GitVersion.OutputVariables.GitVersionVariables.GetEnumerator() -> System.Collections.Generic.IEnumerator>! -GitVersion.OutputVariables.GitVersionVariables.GitVersionVariables(string! Major, string! Minor, string! Patch, string? BuildMetaData, string? FullBuildMetaData, string? BranchName, string? EscapedBranchName, string? Sha, string? ShortSha, string! MajorMinorPatch, string! SemVer, string! FullSemVer, string? AssemblySemVer, string? AssemblySemFileVer, string? PreReleaseTag, string? PreReleaseTagWithDash, string? PreReleaseLabel, string? PreReleaseLabelWithDash, string? PreReleaseNumber, string! WeightedPreReleaseNumber, string? InformationalVersion, string? CommitDate, string? VersionSourceSha, string? CommitsSinceVersionSource, string? UncommittedChanges) -> void +GitVersion.OutputVariables.GitVersionVariables.GitVersionVariables(string! Major, string! Minor, string! Patch, string? BuildMetaData, string? FullBuildMetaData, string? BranchName, string? EscapedBranchName, string? Sha, string? ShortSha, string! MajorMinorPatch, string! SemVer, string! FullSemVer, string? AssemblySemVer, string? AssemblySemFileVer, string? PreReleaseTag, string? PreReleaseTagWithDash, string? PreReleaseLabel, string? PreReleaseLabelWithDash, string? PreReleaseNumber, string! WeightedPreReleaseNumber, string? InformationalVersion, string? CommitDate, string? VersionSourceSha, string? CommitsSinceVersionSource, string? CommitsSinceVersionSourceList, string? UncommittedChanges) -> void GitVersion.OutputVariables.GitVersionVariables.InformationalVersion.get -> string? GitVersion.OutputVariables.GitVersionVariables.InformationalVersion.init -> void GitVersion.OutputVariables.GitVersionVariables.Major.get -> string! @@ -520,6 +522,8 @@ GitVersion.SemanticVersionBuildMetaData.CommitsSinceTag.get -> long? GitVersion.SemanticVersionBuildMetaData.CommitsSinceTag.init -> void GitVersion.SemanticVersionBuildMetaData.CommitsSinceVersionSource.get -> long GitVersion.SemanticVersionBuildMetaData.CommitsSinceVersionSource.init -> void +GitVersion.SemanticVersionBuildMetaData.CommitsSinceVersionSourceList.get -> string[] +GitVersion.SemanticVersionBuildMetaData.CommitsSinceVersionSourceList.init -> void GitVersion.SemanticVersionBuildMetaData.Equals(GitVersion.SemanticVersionBuildMetaData? other) -> bool GitVersion.SemanticVersionBuildMetaData.OtherMetaData.get -> string? GitVersion.SemanticVersionBuildMetaData.OtherMetaData.init -> void diff --git a/src/GitVersion.Core/PublicAPI.Unshipped.txt b/src/GitVersion.Core/PublicAPI.Unshipped.txt index 505df687a4..d77a85acd0 100644 --- a/src/GitVersion.Core/PublicAPI.Unshipped.txt +++ b/src/GitVersion.Core/PublicAPI.Unshipped.txt @@ -11,10 +11,12 @@ GitVersion.Git.AuthenticationInfo.AuthenticationInfo() -> void GitVersion.Git.AuthenticationInfo.AuthenticationInfo(GitVersion.Git.AuthenticationInfo! original) -> void GitVersion.Git.CommitFilter.CommitFilter() -> void GitVersion.Git.CommitFilter.CommitFilter(GitVersion.Git.CommitFilter! original) -> void -GitVersion.OutputVariables.GitVersionVariables.Deconstruct(out string! Major, out string! Minor, out string! Patch, out string? BuildMetaData, out string? FullBuildMetaData, out string? BranchName, out string? EscapedBranchName, out string? Sha, out string? ShortSha, out string! MajorMinorPatch, out string! SemVer, out string! FullSemVer, out string? AssemblySemVer, out string? AssemblySemFileVer, out string? PreReleaseTag, out string? PreReleaseTagWithDash, out string? PreReleaseLabel, out string? PreReleaseLabelWithDash, out string? PreReleaseNumber, out string! WeightedPreReleaseNumber, out string? InformationalVersion, out string? CommitDate, out string? VersionSourceSha, out string? CommitsSinceVersionSource, out string? UncommittedChanges) -> void +GitVersion.OutputVariables.GitVersionVariables.Deconstruct(out string! Major, out string! Minor, out string! Patch, out string? BuildMetaData, out string? FullBuildMetaData, out string? BranchName, out string? EscapedBranchName, out string? Sha, out string? ShortSha, out string! MajorMinorPatch, out string! SemVer, out string! FullSemVer, out string? AssemblySemVer, out string? AssemblySemFileVer, out string? PreReleaseTag, out string? PreReleaseTagWithDash, out string? PreReleaseLabel, out string? PreReleaseLabelWithDash, out string? PreReleaseNumber, out string! WeightedPreReleaseNumber, out string? InformationalVersion, out string? CommitDate, out string? VersionSourceSha, out string? CommitsSinceVersionSource, out string? CommitsSinceVersionSourceList, out string? UncommittedChanges) -> void GitVersion.OutputVariables.GitVersionVariables.GitVersionVariables(GitVersion.OutputVariables.GitVersionVariables! original) -> void GitVersion.RepositoryInfo.RepositoryInfo() -> void GitVersion.RepositoryInfo.RepositoryInfo(GitVersion.RepositoryInfo! original) -> void +GitVersion.SemanticVersionBuildMetaData.CommitsSinceVersionSourceList.get -> string![]! +GitVersion.SemanticVersionFormatValues.CommitsSinceVersionSourceList.get -> string? GitVersion.SemanticVersionWithTag.$() -> GitVersion.SemanticVersionWithTag! GitVersion.SemanticVersionWithTag.Deconstruct(out GitVersion.SemanticVersion! Value, out GitVersion.Git.ITag! Tag) -> void GitVersion.SemanticVersionWithTag.Equals(GitVersion.SemanticVersionWithTag? other) -> bool diff --git a/src/GitVersion.Core/SemVer/SemanticVersionBuildMetaData.cs b/src/GitVersion.Core/SemVer/SemanticVersionBuildMetaData.cs index 88f2fc658a..14daa65f3e 100644 --- a/src/GitVersion.Core/SemVer/SemanticVersionBuildMetaData.cs +++ b/src/GitVersion.Core/SemVer/SemanticVersionBuildMetaData.cs @@ -26,7 +26,17 @@ public class SemanticVersionBuildMetaData : IFormattable, IEquatable semver.BuildMetaData.CommitsSinceVersionSource.ToString(CultureInfo.InvariantCulture); + public string? CommitsSinceVersionSourceList => string.Join(", ", semver.BuildMetaData.CommitsSinceVersionSourceList ?? []).ToString(CultureInfo.InvariantCulture); + public string UncommittedChanges => semver.BuildMetaData.UncommittedChanges.ToString(CultureInfo.InvariantCulture); } diff --git a/src/GitVersion.Core/VersionCalculation/VariableProvider.cs b/src/GitVersion.Core/VersionCalculation/VariableProvider.cs index 0cb26fa7a7..9f35b84c41 100644 --- a/src/GitVersion.Core/VersionCalculation/VariableProvider.cs +++ b/src/GitVersion.Core/VersionCalculation/VariableProvider.cs @@ -63,6 +63,7 @@ public GitVersionVariables GetVariablesFor( semverFormatValues.CommitDate, semverFormatValues.VersionSourceSha, semverFormatValues.CommitsSinceVersionSource, + semverFormatValues.CommitsSinceVersionSourceList, semverFormatValues.UncommittedChanges ); } diff --git a/src/GitVersion.Core/VersionCalculation/VersionCalculators/ContinuousDeliveryVersionCalculator.cs b/src/GitVersion.Core/VersionCalculation/VersionCalculators/ContinuousDeliveryVersionCalculator.cs index 91098dae31..5c67205b87 100644 --- a/src/GitVersion.Core/VersionCalculation/VersionCalculators/ContinuousDeliveryVersionCalculator.cs +++ b/src/GitVersion.Core/VersionCalculation/VersionCalculators/ContinuousDeliveryVersionCalculator.cs @@ -35,6 +35,7 @@ private SemanticVersion CalculateInternal(SemanticVersion semanticVersion, IComm BuildMetaData = new SemanticVersionBuildMetaData(buildMetaData) { CommitsSinceVersionSource = buildMetaData.CommitsSinceTag!.Value, + CommitsSinceVersionSourceList = buildMetaData.CommitsSinceVersionSourceList, CommitsSinceTag = null } }; diff --git a/src/GitVersion.Core/VersionCalculation/VersionCalculators/VersionCalculatorBase.cs b/src/GitVersion.Core/VersionCalculation/VersionCalculators/VersionCalculatorBase.cs index db6c3eb135..f321c15ab4 100644 --- a/src/GitVersion.Core/VersionCalculation/VersionCalculators/VersionCalculatorBase.cs +++ b/src/GitVersion.Core/VersionCalculation/VersionCalculators/VersionCalculatorBase.cs @@ -33,7 +33,9 @@ protected SemanticVersionBuildMetaData CreateVersionBuildMetaData(ICommit? baseV commitSha: Context.CurrentCommit.Sha, commitShortSha: shortSha, commitDate: Context.CurrentCommit.When, - numberOfUnCommittedChanges: Context.NumberOfUncommittedChanges + numberOfUnCommittedChanges: Context.NumberOfUncommittedChanges, + otherMetadata: null, + commitSinceTagList: [.. commitLogs.Select(c => c.Sha)] ); } } diff --git a/src/GitVersion.Testing/Fixtures/BaseGitFlowRepositoryFixture.cs b/src/GitVersion.Testing/Fixtures/BaseGitFlowRepositoryFixture.cs index ebf88bf939..efdc4b95f3 100644 --- a/src/GitVersion.Testing/Fixtures/BaseGitFlowRepositoryFixture.cs +++ b/src/GitVersion.Testing/Fixtures/BaseGitFlowRepositoryFixture.cs @@ -12,8 +12,8 @@ public class BaseGitFlowRepositoryFixture : EmptyRepositoryFixture /// Creates a repo with a develop branch off main which is a single commit ahead of main branch /// Main will be tagged with the initial version before branching develop /// - public BaseGitFlowRepositoryFixture(string initialVersion, string branchName = "main") : - this(r => r.MakeATaggedCommit(initialVersion), branchName) + public BaseGitFlowRepositoryFixture(string initialVersion, string branchName = MainBranch, bool deleteOnDispose = true) : + this(r => r.MakeATaggedCommit(initialVersion), branchName, deleteOnDispose) { } @@ -21,8 +21,8 @@ public BaseGitFlowRepositoryFixture(string initialVersion, string branchName = " /// Creates a repo with a develop branch off main which is a single commit ahead of main /// The initial setup actions will be performed before branching develop /// - public BaseGitFlowRepositoryFixture(Action initialMainAction, string branchName = "main") : - base(branchName) => SetupRepo(initialMainAction); + public BaseGitFlowRepositoryFixture(Action initialMainAction, string branchName = MainBranch, bool deleteOnDispose = true) : + base(branchName, deleteOnDispose) => SetupRepo(initialMainAction); private void SetupRepo(Action initialMainAction) { @@ -33,6 +33,6 @@ private void SetupRepo(Action initialMainAction) initialMainAction(Repository); Commands.Checkout(Repository, Repository.CreateBranch("develop")); - Repository.MakeACommit(); + Repository.MakeACommit("First commit on new branch 'develop'"); } } diff --git a/src/GitVersion.Testing/Fixtures/EmptyRepositoryFixture.cs b/src/GitVersion.Testing/Fixtures/EmptyRepositoryFixture.cs index b864b31247..bf394da67b 100644 --- a/src/GitVersion.Testing/Fixtures/EmptyRepositoryFixture.cs +++ b/src/GitVersion.Testing/Fixtures/EmptyRepositoryFixture.cs @@ -1,3 +1,3 @@ namespace GitVersion.Testing; -public class EmptyRepositoryFixture(string branchName = "main") : RepositoryFixtureBase(path => CreateNewRepository(path, branchName)); +public class EmptyRepositoryFixture(string branchName = RepositoryFixtureBase.MainBranch, bool deleteOnDispose = true) : RepositoryFixtureBase(path => CreateNewRepository(path, branchName), deleteOnDispose); diff --git a/src/GitVersion.Testing/Fixtures/RemoteRepositoryFixture.cs b/src/GitVersion.Testing/Fixtures/RemoteRepositoryFixture.cs index 2312d47f62..71245172ab 100644 --- a/src/GitVersion.Testing/Fixtures/RemoteRepositoryFixture.cs +++ b/src/GitVersion.Testing/Fixtures/RemoteRepositoryFixture.cs @@ -12,7 +12,7 @@ public class RemoteRepositoryFixture : RepositoryFixtureBase public RemoteRepositoryFixture(Func builder) : base(builder) => LocalRepositoryFixture = CloneRepository(); - public RemoteRepositoryFixture(string branchName = "main") + public RemoteRepositoryFixture(string branchName = MainBranch) : this(path => CreateNewRepository(path, branchName, 5)) { } diff --git a/src/GitVersion.Testing/Fixtures/RepositoryFixtureBase.cs b/src/GitVersion.Testing/Fixtures/RepositoryFixtureBase.cs index be177bbf0a..c8ae3140d5 100644 --- a/src/GitVersion.Testing/Fixtures/RepositoryFixtureBase.cs +++ b/src/GitVersion.Testing/Fixtures/RepositoryFixtureBase.cs @@ -9,17 +9,21 @@ namespace GitVersion.Testing; /// public abstract class RepositoryFixtureBase : IDisposable { - protected RepositoryFixtureBase(Func repositoryBuilder) - : this(repositoryBuilder(FileSystemHelper.Path.GetRepositoryTempPath())) + public const string MainBranch = "main"; + private readonly bool deleteOnDispose; + + protected RepositoryFixtureBase(Func repositoryBuilder, bool deleteOnDispose = true) + : this(repositoryBuilder(FileSystemHelper.Path.GetRepositoryTempPath()), deleteOnDispose) { } - protected RepositoryFixtureBase(Repository repository) + protected RepositoryFixtureBase(Repository repository, bool deleteOnDispose = true) { SequenceDiagram = new(); Repository = repository.ShouldNotBeNull(); Repository.Config.Set("user.name", "Test"); Repository.Config.Set("user.email", "test@email.com"); + this.deleteOnDispose = deleteOnDispose; } public Repository Repository { get; } @@ -47,16 +51,23 @@ protected virtual void Dispose(bool disposing) Repository.Dispose(); var directoryPath = FileSystemHelper.Path.GetFileName(RepositoryPath); - try + if (deleteOnDispose) { - Console.WriteLine("Cleaning up repository path at {0}", directoryPath); - FileSystemHelper.Directory.DeleteDirectory(RepositoryPath); - Console.WriteLine("Cleaned up repository path at {0}", directoryPath); + try + { + Console.WriteLine("Cleaning up repository path at {0}", directoryPath); + FileSystemHelper.Directory.DeleteDirectory(RepositoryPath); + Console.WriteLine("Cleaned up repository path at {0}", directoryPath); + } + catch (Exception e) + { + Console.WriteLine("Failed to clean up repository path at {0}. Received exception: {1}", directoryPath, e.Message); + // throw; + } } - catch (Exception e) + else { - Console.WriteLine("Failed to clean up repository path at {0}. Received exception: {1}", directoryPath, e.Message); - // throw; + Console.WriteLine("Leaving repository path at {0} intact", directoryPath); } this.SequenceDiagram.End(); @@ -73,7 +84,7 @@ public void Remove(string branch) SequenceDiagram.Destroy(branch); } - public static void Init(string path, string branchName = "main") => GitTestExtensions.ExecuteGitCmd($"init {path} -b {branchName}", "."); + public static void Init(string path, string branchName = MainBranch) => GitTestExtensions.ExecuteGitCmd($"init {path} -b {branchName}", "."); public string MakeATaggedCommit(string tag) { diff --git a/src/GitVersion.Testing/Fixtures/SequenceDiagram.cs b/src/GitVersion.Testing/Fixtures/SequenceDiagram.cs index 35e73d21cf..6338968241 100644 --- a/src/GitVersion.Testing/Fixtures/SequenceDiagram.cs +++ b/src/GitVersion.Testing/Fixtures/SequenceDiagram.cs @@ -10,6 +10,7 @@ public class SequenceDiagram { private readonly Dictionary participants = []; private readonly StringBuilder diagramBuilder; + private readonly CommitLabelGenerator commitLabelGenerator = new(); /// /// Initializes a new instance of the class. @@ -125,4 +126,8 @@ public void BranchToFromTag(string branchName, string fromTag, string onBranch, /// returns the plantUML representation of the Sequence Diagram /// public string GetDiagram() => this.diagramBuilder.ToString(); + + public string GetOrAddLabel(string? key) => commitLabelGenerator.GetOrAdd(key ?? "N/A"); + + public string GetOrAddSourceLabel(string? key) => commitLabelGenerator.GetOrAddRoot(key ?? "N/A"); } diff --git a/src/GitVersion.Testing/GitTestExtensions.cs b/src/GitVersion.Testing/GitTestExtensions.cs index be1a0518cf..28f11143e1 100644 --- a/src/GitVersion.Testing/GitTestExtensions.cs +++ b/src/GitVersion.Testing/GitTestExtensions.cs @@ -38,9 +38,9 @@ private static Commit CreateFileAndCommit(this IRepository repository, string re Generate.SignatureNow(), Generate.SignatureNow()); } - public static Tag MakeATaggedCommit(this IRepository repository, string tag) + public static Tag MakeATaggedCommit(this IRepository repository, string tag, string? commitMessage = null) { - var commit = repository.MakeACommit(); + var commit = repository.MakeACommit(commitMessage); var existingTag = repository.Tags.SingleOrDefault(t => t.FriendlyName == tag); return existingTag ?? repository.Tags.Add(tag, commit); } diff --git a/src/GitVersion.Testing/Helpers/CommitLabelGenerator.cs b/src/GitVersion.Testing/Helpers/CommitLabelGenerator.cs new file mode 100644 index 0000000000..1b1a7818e3 --- /dev/null +++ b/src/GitVersion.Testing/Helpers/CommitLabelGenerator.cs @@ -0,0 +1,87 @@ +namespace GitVersion.Testing.Helpers; + +/// +/// Generates unique uppercase letter labels (A, B, ..., Z, AA, AB, etc.) for unique keys. +/// +public class CommitLabelGenerator +{ + private readonly Dictionary lookup = new(); + + /// + /// Gets the label assigned to the specified key, creating a new one if not already assigned. + /// + public string GetOrAdd(string key) + { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentException("SHA cannot be empty or whitespace.", nameof(key)); + } + + if (key == "N/A") + { + return "N/A"; + } + + if (lookup.TryGetValue(key, out var existing)) + { + return existing; + } + + var label = GetNextLabel(); + lookup[key] = label; + return label; + } + + public string GetOrAddRoot(string? versionSourceSha) + { + if (string.IsNullOrWhiteSpace(versionSourceSha)) + { + throw new ArgumentException("Version source SHA cannot be empty or whitespace.", nameof(versionSourceSha)); + } + + if (versionSourceSha == "N/A") + { + return "N/A"; + } + + return GetOrAddRootPrivate(versionSourceSha); + } + + private string GetOrAddRootPrivate(string key) + { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentException("SHA cannot be empty or whitespace.", nameof(key)); + } + + if (key == "N/A") + { + return "N/A"; + } + + if (lookup.TryGetValue(key, out var existing)) + { + return existing; + } + + var label = GetNextRootLabel(); + lookup[key] = label; + return label; + } + + private string GetNextRootLabel() => "Root" + GetNextLabel(); + + private string GetNextLabel() + { + var index = lookup.Count; + var label = string.Empty; + + do + { + label = (char)('A' + (index % 26)) + label; + index = (index / 26) - 1; + } while (index >= 0); + + return label; + } +}