diff --git a/src/GitVersionCore.Tests/IntegrationTests/MainlineDevelopmentMode.cs b/src/GitVersionCore.Tests/IntegrationTests/MainlineDevelopmentMode.cs index e5836e87e3..9aa32c42c9 100644 --- a/src/GitVersionCore.Tests/IntegrationTests/MainlineDevelopmentMode.cs +++ b/src/GitVersionCore.Tests/IntegrationTests/MainlineDevelopmentMode.cs @@ -198,6 +198,94 @@ public void VerifySupportForwardMerge() } } + [Test] + public void VerifyDevelopTracksMasterVersion() + { + using (var fixture = new EmptyRepositoryFixture()) + { + fixture.Repository.MakeACommit("1"); + fixture.MakeATaggedCommit("1.0.0"); + fixture.MakeACommit(); + + // branching increments the version + fixture.BranchTo("develop"); + fixture.AssertFullSemver(config, "1.1.0-alpha.0"); + fixture.MakeACommit(); + fixture.AssertFullSemver(config, "1.1.0-alpha.1"); + + // merging develop into master increments minor version on master + fixture.Checkout("master"); + fixture.MergeNoFF("develop"); + fixture.AssertFullSemver(config, "1.1.0"); + + // a commit on develop before the merge still has the same version number + fixture.Checkout("develop"); + fixture.AssertFullSemver(config, "1.1.0-alpha.1"); + + // moving on to further work on develop tracks master's version from the merge + fixture.MakeACommit(); + fixture.AssertFullSemver(config, "1.2.0-alpha.1"); + + // adding a commit to master increments patch + fixture.Checkout("master"); + fixture.MakeACommit(); + fixture.AssertFullSemver(config, "1.1.1"); + + // adding a commit to master doesn't change develop's version + fixture.Checkout("develop"); + fixture.AssertFullSemver(config, "1.2.0-alpha.1"); + } + } + + [Test] + public void VerifyDevelopFeatureTracksMasterVersion() + { + using (var fixture = new EmptyRepositoryFixture()) + { + fixture.Repository.MakeACommit("1"); + fixture.MakeATaggedCommit("1.0.0"); + fixture.MakeACommit(); + + // branching increments the version + fixture.BranchTo("develop"); + fixture.AssertFullSemver(config, "1.1.0-alpha.0"); + fixture.MakeACommit(); + fixture.AssertFullSemver(config, "1.1.0-alpha.1"); + + // merging develop into master increments minor version on master + fixture.Checkout("master"); + fixture.MergeNoFF("develop"); + fixture.AssertFullSemver(config, "1.1.0"); + + // a commit on develop before the merge still has the same version number + fixture.Checkout("develop"); + fixture.AssertFullSemver(config, "1.1.0-alpha.1"); + + // a branch from develop before the merge tracks the pre-merge version from master + // (note: the commit on develop looks like a commit to this branch, thus the .1) + fixture.BranchTo("feature/foo"); + fixture.AssertFullSemver(config, "1.0.2-foo.1"); + + // further work on the branch tracks the merged version from master + fixture.MakeACommit(); + fixture.AssertFullSemver(config, "1.1.1-foo.1"); + + // adding a commit to master increments patch + fixture.Checkout("master"); + fixture.MakeACommit(); + fixture.AssertFullSemver(config, "1.1.1"); + + // adding a commit to master doesn't change the feature's version + fixture.Checkout("feature/foo"); + fixture.AssertFullSemver(config, "1.1.1-foo.1"); + + // merging the feature to develop increments develop + fixture.Checkout("develop"); + fixture.MergeNoFF("feature/foo"); + fixture.AssertFullSemver(config, "1.2.0-alpha.2"); + } + } + [Test] public void VerifyMergingMasterToFeatureDoesNotCauseBranchCommitsToIncrementVersion() { diff --git a/src/GitVersionCore/VersionCalculation/MainlineVersionCalculator.cs b/src/GitVersionCore/VersionCalculation/MainlineVersionCalculator.cs index 6039c3e06e..ef9d1a7a03 100644 --- a/src/GitVersionCore/VersionCalculation/MainlineVersionCalculator.cs +++ b/src/GitVersionCore/VersionCalculation/MainlineVersionCalculator.cs @@ -1,4 +1,4 @@ -using GitVersion.VersionCalculation.BaseVersionCalculators; +using GitVersion.VersionCalculation.BaseVersionCalculators; using LibGit2Sharp; using System; using System.Collections.Generic; @@ -33,67 +33,55 @@ public SemanticVersion FindMainlineModeVersion(BaseVersion baseVersion, GitVersi // master * * // - var mainlineTip = GetMainlineTip(context); - var commitsNotOnMainline = context.Repository.Commits.QueryBy(new CommitFilter + var mergeBase = baseVersion.BaseVersionSource; + var mainline = GetMainline(context, baseVersion.BaseVersionSource); + var mainlineTip = mainline.Tip; + + // when the current branch is not mainline, find the effective mainline tip for versioning the branch + if (!context.CurrentBranch.IsSameBranch(mainline)) { - IncludeReachableFrom = context.CurrentBranch, - ExcludeReachableFrom = mainlineTip, - SortBy = CommitSortStrategies.Reverse, - FirstParentOnly = true - }).Where(c => c.Sha != baseVersion.BaseVersionSource.Sha && c.Parents.Count() == 1).ToList(); - var commitLog = context.Repository.Commits.QueryBy(new CommitFilter + mergeBase = FindMergeBaseBeforeForwardMerge(context, baseVersion.BaseVersionSource, mainline, out mainlineTip); + Logger.WriteInfo(string.Format("Current branch ({0}) was branch from {1}", context.CurrentBranch.FriendlyName, mergeBase)); + } + + var mainlineCommitLog = context.Repository.Commits.QueryBy(new CommitFilter { - IncludeReachableFrom = context.CurrentBranch, + IncludeReachableFrom = mainlineTip, ExcludeReachableFrom = baseVersion.BaseVersionSource, SortBy = CommitSortStrategies.Reverse, FirstParentOnly = true }) - .Where(c => c.Sha != baseVersion.BaseVersionSource.Sha) - .Except(commitsNotOnMainline) .ToList(); - - var directCommits = new List(); + var directCommits = new List(mainlineCommitLog.Count); // Scans commit log in reverse, aggregating merge commits - foreach (var commit in commitLog) + foreach (var commit in mainlineCommitLog) { directCommits.Add(commit); if (commit.Parents.Count() > 1) { - mainlineVersion = AggregateMergeCommitIncrement(context, commit, directCommits, mainlineVersion); + mainlineVersion = AggregateMergeCommitIncrement(context, commit, directCommits, mainlineVersion, mainline); } } + // This will increment for any direct commits on mainline + mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion, mainline); + mainlineVersion.BuildMetaData = metaDataCalculator.Create(mergeBase, context); + + // branches other than master always get a bump for the act of branching if (context.CurrentBranch.FriendlyName != "master") { - var mergedHead = context.CurrentCommit; - var findMergeBase = context.Repository.ObjectDatabase.FindMergeBase(context.CurrentCommit, mainlineTip); - Logger.WriteInfo(string.Format("Current branch ({0}) was branch from {1}", context.CurrentBranch.FriendlyName, findMergeBase)); - - var branchIncrement = FindMessageIncrement(context, null, mergedHead, findMergeBase, directCommits); - // This will increment for any direct commits on master - mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion, "master"); - mainlineVersion.BuildMetaData = metaDataCalculator.Create(findMergeBase, context); - // Don't increment if the merge commit is a merge into mainline - // this ensures PR's and forward merges end up correct. - if (mergedHead.Parents.Count() == 1 || mergedHead.Parents.First() != mainlineTip) - { - Logger.WriteInfo(string.Format("Performing {0} increment for current branch ", branchIncrement)); - mainlineVersion = mainlineVersion.IncrementVersion(branchIncrement); - } - } - else - { - // If we are on master, make sure no commits get left behind - mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion); - mainlineVersion.BuildMetaData = metaDataCalculator.Create(baseVersion.BaseVersionSource, context); + var branchIncrement = FindMessageIncrement(context, null, context.CurrentCommit, mergeBase, mainlineCommitLog); + Logger.WriteInfo(string.Format("Performing {0} increment for current branch ", branchIncrement)); + + mainlineVersion = mainlineVersion.IncrementVersion(branchIncrement); } return mainlineVersion; } } - SemanticVersion AggregateMergeCommitIncrement(GitVersionContext context, Commit commit, List directCommits, SemanticVersion mainlineVersion) + SemanticVersion AggregateMergeCommitIncrement(GitVersionContext context, Commit commit, List directCommits, SemanticVersion mainlineVersion, Branch mainline) { // Merge commit, process all merged commits as a batch var mergeCommit = commit; @@ -103,7 +91,7 @@ SemanticVersion AggregateMergeCommitIncrement(GitVersionContext context, Commit // If this collection is not empty there has been some direct commits against master // Treat each commit as it's own 'release', we need to do this before we increment the branch - mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion); + mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion, mainline); directCommits.Clear(); // Finally increment for the branch @@ -113,25 +101,14 @@ SemanticVersion AggregateMergeCommitIncrement(GitVersionContext context, Commit return mainlineVersion; } - static Commit GetMainlineTip(GitVersionContext context) + static Branch GetMainline(GitVersionContext context, Commit baseVersionSource) { var mainlineBranchConfigs = context.FullConfiguration.Branches.Where(b => b.Value.IsMainline == true).ToList(); - var seenMainlineTips = new List(); var mainlineBranches = context.Repository.Branches .Where(b => { return mainlineBranchConfigs.Any(c => Regex.IsMatch(b.FriendlyName, c.Value.Regex)); }) - .Where(b => - { - if (seenMainlineTips.Contains(b.Tip.Sha)) - { - Logger.WriteInfo("Multiple possible mainlines pointing at the same commit, dropping " + b.FriendlyName); - return false; - } - seenMainlineTips.Add(b.Tip.Sha); - return true; - }) .Select(b => new { MergeBase = context.Repository.ObjectDatabase.FindMergeBase(b.Tip, context.CurrentCommit), @@ -152,7 +129,39 @@ static Commit GetMainlineTip(GitVersionContext context) { var mainlineBranch = possibleMainlineBranches[0]; Logger.WriteInfo("Mainline for current branch is " + mainlineBranch.FriendlyName); - return mainlineBranch.Tip; + return mainlineBranch; + } + + // prefer current branch, if it is a mainline branch + if (possibleMainlineBranches.Any(context.CurrentBranch.IsSameBranch)) + { + Logger.WriteInfo(string.Format("Choosing {0} as mainline because it is the current branch", context.CurrentBranch.FriendlyName)); + return context.CurrentBranch; + } + + // prefer a branch on which the merge base was a direct commit, if there is such a branch + var firstMatchingCommitBranch = possibleMainlineBranches + .FirstOrDefault(b => + { + var filter = new CommitFilter + { + IncludeReachableFrom = b, + ExcludeReachableFrom = baseVersionSource, + FirstParentOnly = true, + }; + var query = context.Repository.Commits.QueryBy(filter); + + return query.Contains(firstMatchingCommit); + }); + if (firstMatchingCommitBranch != null) + { + var message = string.Format( + "Choosing {0} as mainline because {1}'s merge base was a direct commit to {0}", + firstMatchingCommitBranch.FriendlyName, + context.CurrentBranch.FriendlyName); + Logger.WriteInfo(message); + + return firstMatchingCommitBranch; } var chosenMainline = possibleMainlineBranches[0]; @@ -160,17 +169,85 @@ static Commit GetMainlineTip(GitVersionContext context) "Multiple mainlines ({0}) have the same merge base for the current branch, choosing {1} because we found that branch first...", string.Join(", ", possibleMainlineBranches.Select(b => b.FriendlyName)), chosenMainline.FriendlyName)); - return chosenMainline.Tip; + return chosenMainline; + } + + /// + /// Gets the commit on mainline at which was fully integrated. + /// + /// The collection of commits made directly to mainline, in reverse order. + /// The best possible merge base between and the current commit. + /// The tip of the mainline branch. + /// The commit on mainline at which was merged, if such a commit exists; otherwise, . + /// + /// This method gets the most recent commit on mainline that should be considered for versioning the current branch. + /// + private static Commit GetEffectiveMainlineTip(IEnumerable mainlineCommitLog, Commit mergeBase, Commit mainlineTip) + { + // find the commit that merged mergeBase into mainline + foreach (var commit in mainlineCommitLog) + { + if (commit == mergeBase || commit.Parents.Contains(mergeBase)) + { + Logger.WriteInfo(string.Format("Found branch merge point; choosing {0} as effective mainline tip", commit)); + return commit; + } + } + + return mainlineTip; + } + + /// + /// Gets the best possible merge base between the current commit and that is not the child of a forward merge. + /// + /// The current versioning context. + /// The commit that establishes the contextual base version. + /// The mainline branch. + /// The commit on mainline at which the returned merge base was fully integrated. + /// The best possible merge base between the current commit and that is not the child of a forward merge. + private static Commit FindMergeBaseBeforeForwardMerge(GitVersionContext context, Commit baseVersionSource, Branch mainline, out Commit mainlineTip) + { + var mergeBase = context.Repository.ObjectDatabase.FindMergeBase(context.CurrentCommit, mainline.Tip); + var mainlineCommitLog = context.Repository.Commits + .QueryBy(new CommitFilter + { + IncludeReachableFrom = mainline.Tip, + ExcludeReachableFrom = baseVersionSource, + SortBy = CommitSortStrategies.Reverse, + FirstParentOnly = true + }) + .ToList(); + + // find the mainline commit effective for versioning the current branch + mainlineTip = GetEffectiveMainlineTip(mainlineCommitLog, mergeBase, mainline.Tip); + + // detect forward merge and rewind mainlineTip to before it + if (mergeBase == context.CurrentCommit && !mainlineCommitLog.Contains(mergeBase)) + { + var mainlineTipPrevious = mainlineTip.Parents.First(); + var message = string.Format( + "Detected forward merge at {0}; rewinding mainline to previous commit {1}", + mainlineTip, + mainlineTipPrevious); + + Logger.WriteInfo(message); + + // re-do mergeBase detection before the forward merge + mergeBase = context.Repository.ObjectDatabase.FindMergeBase(context.CurrentCommit, mainlineTipPrevious); + mainlineTip = GetEffectiveMainlineTip(mainlineCommitLog, mergeBase, mainlineTipPrevious); + } + + return mergeBase; } - private static SemanticVersion IncrementForEachCommit(GitVersionContext context, List directCommits, SemanticVersion mainlineVersion, string branch = null) + private static SemanticVersion IncrementForEachCommit(GitVersionContext context, List directCommits, SemanticVersion mainlineVersion, Branch mainline) { foreach (var directCommit in directCommits) { var directCommitIncrement = IncrementStrategyFinder.GetIncrementForCommits(context, new[] { directCommit - }) ?? IncrementStrategyFinder.FindDefaultIncrementForBranch(context, branch); + }) ?? IncrementStrategyFinder.FindDefaultIncrementForBranch(context, mainline.FriendlyName); mainlineVersion = mainlineVersion.IncrementVersion(directCommitIncrement); Logger.WriteInfo(string.Format("Direct commit on master {0} incremented base versions {1}, now {2}", directCommit.Sha, directCommitIncrement, mainlineVersion));