Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
187 changes: 132 additions & 55 deletions src/GitVersionCore/VersionCalculation/MainlineVersionCalculator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using GitVersion.VersionCalculation.BaseVersionCalculators;
using GitVersion.VersionCalculation.BaseVersionCalculators;
using LibGit2Sharp;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -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<Commit>();
var directCommits = new List<Commit>(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<Commit> directCommits, SemanticVersion mainlineVersion)
SemanticVersion AggregateMergeCommitIncrement(GitVersionContext context, Commit commit, List<Commit> directCommits, SemanticVersion mainlineVersion, Branch mainline)
{
// Merge commit, process all merged commits as a batch
var mergeCommit = commit;
Expand All @@ -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
Expand All @@ -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<string>();
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),
Expand All @@ -152,25 +129,125 @@ 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];
Logger.WriteInfo(string.Format(
"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;
}

/// <summary>
/// Gets the commit on mainline at which <paramref name="mergeBase"/> was fully integrated.
/// </summary>
/// <param name="mainlineCommitLog">The collection of commits made directly to mainline, in reverse order.</param>
/// <param name="mergeBase">The best possible merge base between <paramref name="mainlineTip"/> and the current commit.</param>
/// <param name="mainlineTip">The tip of the mainline branch.</param>
/// <returns>The commit on mainline at which <paramref name="mergeBase"/> was merged, if such a commit exists; otherwise, <paramref name="mainlineTip"/>.</returns>
/// <remarks>
/// This method gets the most recent commit on mainline that should be considered for versioning the current branch.
/// </remarks>
private static Commit GetEffectiveMainlineTip(IEnumerable<Commit> 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;
}

/// <summary>
/// Gets the best possible merge base between the current commit and <paramref name="mainline"/> that is not the child of a forward merge.
/// </summary>
/// <param name="context">The current versioning context.</param>
/// <param name="baseVersionSource">The commit that establishes the contextual base version.</param>
/// <param name="mainline">The mainline branch.</param>
/// <param name="mainlineTip">The commit on mainline at which the returned merge base was fully integrated.</param>
/// <returns>The best possible merge base between the current commit and <paramref name="mainline"/> that is not the child of a forward merge.</returns>
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<Commit> directCommits, SemanticVersion mainlineVersion, string branch = null)
private static SemanticVersion IncrementForEachCommit(GitVersionContext context, List<Commit> 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));
Expand Down