Skip to content

Commit ef1add0

Browse files
authored
Merge pull request #1540 from wjrogers/fix-mainline-develop-version-#1035
Always compute mainline version with mainline commits
2 parents 6130867 + 9a1c6f7 commit ef1add0

File tree

2 files changed

+220
-55
lines changed

2 files changed

+220
-55
lines changed

src/GitVersionCore.Tests/IntegrationTests/MainlineDevelopmentMode.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,94 @@ public void VerifySupportForwardMerge()
198198
}
199199
}
200200

201+
[Test]
202+
public void VerifyDevelopTracksMasterVersion()
203+
{
204+
using (var fixture = new EmptyRepositoryFixture())
205+
{
206+
fixture.Repository.MakeACommit("1");
207+
fixture.MakeATaggedCommit("1.0.0");
208+
fixture.MakeACommit();
209+
210+
// branching increments the version
211+
fixture.BranchTo("develop");
212+
fixture.AssertFullSemver(config, "1.1.0-alpha.0");
213+
fixture.MakeACommit();
214+
fixture.AssertFullSemver(config, "1.1.0-alpha.1");
215+
216+
// merging develop into master increments minor version on master
217+
fixture.Checkout("master");
218+
fixture.MergeNoFF("develop");
219+
fixture.AssertFullSemver(config, "1.1.0");
220+
221+
// a commit on develop before the merge still has the same version number
222+
fixture.Checkout("develop");
223+
fixture.AssertFullSemver(config, "1.1.0-alpha.1");
224+
225+
// moving on to further work on develop tracks master's version from the merge
226+
fixture.MakeACommit();
227+
fixture.AssertFullSemver(config, "1.2.0-alpha.1");
228+
229+
// adding a commit to master increments patch
230+
fixture.Checkout("master");
231+
fixture.MakeACommit();
232+
fixture.AssertFullSemver(config, "1.1.1");
233+
234+
// adding a commit to master doesn't change develop's version
235+
fixture.Checkout("develop");
236+
fixture.AssertFullSemver(config, "1.2.0-alpha.1");
237+
}
238+
}
239+
240+
[Test]
241+
public void VerifyDevelopFeatureTracksMasterVersion()
242+
{
243+
using (var fixture = new EmptyRepositoryFixture())
244+
{
245+
fixture.Repository.MakeACommit("1");
246+
fixture.MakeATaggedCommit("1.0.0");
247+
fixture.MakeACommit();
248+
249+
// branching increments the version
250+
fixture.BranchTo("develop");
251+
fixture.AssertFullSemver(config, "1.1.0-alpha.0");
252+
fixture.MakeACommit();
253+
fixture.AssertFullSemver(config, "1.1.0-alpha.1");
254+
255+
// merging develop into master increments minor version on master
256+
fixture.Checkout("master");
257+
fixture.MergeNoFF("develop");
258+
fixture.AssertFullSemver(config, "1.1.0");
259+
260+
// a commit on develop before the merge still has the same version number
261+
fixture.Checkout("develop");
262+
fixture.AssertFullSemver(config, "1.1.0-alpha.1");
263+
264+
// a branch from develop before the merge tracks the pre-merge version from master
265+
// (note: the commit on develop looks like a commit to this branch, thus the .1)
266+
fixture.BranchTo("feature/foo");
267+
fixture.AssertFullSemver(config, "1.0.2-foo.1");
268+
269+
// further work on the branch tracks the merged version from master
270+
fixture.MakeACommit();
271+
fixture.AssertFullSemver(config, "1.1.1-foo.1");
272+
273+
// adding a commit to master increments patch
274+
fixture.Checkout("master");
275+
fixture.MakeACommit();
276+
fixture.AssertFullSemver(config, "1.1.1");
277+
278+
// adding a commit to master doesn't change the feature's version
279+
fixture.Checkout("feature/foo");
280+
fixture.AssertFullSemver(config, "1.1.1-foo.1");
281+
282+
// merging the feature to develop increments develop
283+
fixture.Checkout("develop");
284+
fixture.MergeNoFF("feature/foo");
285+
fixture.AssertFullSemver(config, "1.2.0-alpha.2");
286+
}
287+
}
288+
201289
[Test]
202290
public void VerifyMergingMasterToFeatureDoesNotCauseBranchCommitsToIncrementVersion()
203291
{

src/GitVersionCore/VersionCalculation/MainlineVersionCalculator.cs

Lines changed: 132 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using GitVersion.VersionCalculation.BaseVersionCalculators;
1+
using GitVersion.VersionCalculation.BaseVersionCalculators;
22
using LibGit2Sharp;
33
using System;
44
using System.Collections.Generic;
@@ -33,67 +33,55 @@ public SemanticVersion FindMainlineModeVersion(BaseVersion baseVersion, GitVersi
3333
// master * *
3434
//
3535

36-
var mainlineTip = GetMainlineTip(context);
37-
var commitsNotOnMainline = context.Repository.Commits.QueryBy(new CommitFilter
36+
var mergeBase = baseVersion.BaseVersionSource;
37+
var mainline = GetMainline(context, baseVersion.BaseVersionSource);
38+
var mainlineTip = mainline.Tip;
39+
40+
// when the current branch is not mainline, find the effective mainline tip for versioning the branch
41+
if (!context.CurrentBranch.IsSameBranch(mainline))
3842
{
39-
IncludeReachableFrom = context.CurrentBranch,
40-
ExcludeReachableFrom = mainlineTip,
41-
SortBy = CommitSortStrategies.Reverse,
42-
FirstParentOnly = true
43-
}).Where(c => c.Sha != baseVersion.BaseVersionSource.Sha && c.Parents.Count() == 1).ToList();
44-
var commitLog = context.Repository.Commits.QueryBy(new CommitFilter
43+
mergeBase = FindMergeBaseBeforeForwardMerge(context, baseVersion.BaseVersionSource, mainline, out mainlineTip);
44+
Logger.WriteInfo(string.Format("Current branch ({0}) was branch from {1}", context.CurrentBranch.FriendlyName, mergeBase));
45+
}
46+
47+
var mainlineCommitLog = context.Repository.Commits.QueryBy(new CommitFilter
4548
{
46-
IncludeReachableFrom = context.CurrentBranch,
49+
IncludeReachableFrom = mainlineTip,
4750
ExcludeReachableFrom = baseVersion.BaseVersionSource,
4851
SortBy = CommitSortStrategies.Reverse,
4952
FirstParentOnly = true
5053
})
51-
.Where(c => c.Sha != baseVersion.BaseVersionSource.Sha)
52-
.Except(commitsNotOnMainline)
5354
.ToList();
54-
55-
var directCommits = new List<Commit>();
55+
var directCommits = new List<Commit>(mainlineCommitLog.Count);
5656

5757
// Scans commit log in reverse, aggregating merge commits
58-
foreach (var commit in commitLog)
58+
foreach (var commit in mainlineCommitLog)
5959
{
6060
directCommits.Add(commit);
6161
if (commit.Parents.Count() > 1)
6262
{
63-
mainlineVersion = AggregateMergeCommitIncrement(context, commit, directCommits, mainlineVersion);
63+
mainlineVersion = AggregateMergeCommitIncrement(context, commit, directCommits, mainlineVersion, mainline);
6464
}
6565
}
6666

67+
// This will increment for any direct commits on mainline
68+
mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion, mainline);
69+
mainlineVersion.BuildMetaData = metaDataCalculator.Create(mergeBase, context);
70+
71+
// branches other than master always get a bump for the act of branching
6772
if (context.CurrentBranch.FriendlyName != "master")
6873
{
69-
var mergedHead = context.CurrentCommit;
70-
var findMergeBase = context.Repository.ObjectDatabase.FindMergeBase(context.CurrentCommit, mainlineTip);
71-
Logger.WriteInfo(string.Format("Current branch ({0}) was branch from {1}", context.CurrentBranch.FriendlyName, findMergeBase));
72-
73-
var branchIncrement = FindMessageIncrement(context, null, mergedHead, findMergeBase, directCommits);
74-
// This will increment for any direct commits on master
75-
mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion, "master");
76-
mainlineVersion.BuildMetaData = metaDataCalculator.Create(findMergeBase, context);
77-
// Don't increment if the merge commit is a merge into mainline
78-
// this ensures PR's and forward merges end up correct.
79-
if (mergedHead.Parents.Count() == 1 || mergedHead.Parents.First() != mainlineTip)
80-
{
81-
Logger.WriteInfo(string.Format("Performing {0} increment for current branch ", branchIncrement));
82-
mainlineVersion = mainlineVersion.IncrementVersion(branchIncrement);
83-
}
84-
}
85-
else
86-
{
87-
// If we are on master, make sure no commits get left behind
88-
mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion);
89-
mainlineVersion.BuildMetaData = metaDataCalculator.Create(baseVersion.BaseVersionSource, context);
74+
var branchIncrement = FindMessageIncrement(context, null, context.CurrentCommit, mergeBase, mainlineCommitLog);
75+
Logger.WriteInfo(string.Format("Performing {0} increment for current branch ", branchIncrement));
76+
77+
mainlineVersion = mainlineVersion.IncrementVersion(branchIncrement);
9078
}
9179

9280
return mainlineVersion;
9381
}
9482
}
9583

96-
SemanticVersion AggregateMergeCommitIncrement(GitVersionContext context, Commit commit, List<Commit> directCommits, SemanticVersion mainlineVersion)
84+
SemanticVersion AggregateMergeCommitIncrement(GitVersionContext context, Commit commit, List<Commit> directCommits, SemanticVersion mainlineVersion, Branch mainline)
9785
{
9886
// Merge commit, process all merged commits as a batch
9987
var mergeCommit = commit;
@@ -103,7 +91,7 @@ SemanticVersion AggregateMergeCommitIncrement(GitVersionContext context, Commit
10391

10492
// If this collection is not empty there has been some direct commits against master
10593
// Treat each commit as it's own 'release', we need to do this before we increment the branch
106-
mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion);
94+
mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion, mainline);
10795
directCommits.Clear();
10896

10997
// Finally increment for the branch
@@ -113,25 +101,14 @@ SemanticVersion AggregateMergeCommitIncrement(GitVersionContext context, Commit
113101
return mainlineVersion;
114102
}
115103

116-
static Commit GetMainlineTip(GitVersionContext context)
104+
static Branch GetMainline(GitVersionContext context, Commit baseVersionSource)
117105
{
118106
var mainlineBranchConfigs = context.FullConfiguration.Branches.Where(b => b.Value.IsMainline == true).ToList();
119-
var seenMainlineTips = new List<string>();
120107
var mainlineBranches = context.Repository.Branches
121108
.Where(b =>
122109
{
123110
return mainlineBranchConfigs.Any(c => Regex.IsMatch(b.FriendlyName, c.Value.Regex));
124111
})
125-
.Where(b =>
126-
{
127-
if (seenMainlineTips.Contains(b.Tip.Sha))
128-
{
129-
Logger.WriteInfo("Multiple possible mainlines pointing at the same commit, dropping " + b.FriendlyName);
130-
return false;
131-
}
132-
seenMainlineTips.Add(b.Tip.Sha);
133-
return true;
134-
})
135112
.Select(b => new
136113
{
137114
MergeBase = context.Repository.ObjectDatabase.FindMergeBase(b.Tip, context.CurrentCommit),
@@ -152,25 +129,125 @@ static Commit GetMainlineTip(GitVersionContext context)
152129
{
153130
var mainlineBranch = possibleMainlineBranches[0];
154131
Logger.WriteInfo("Mainline for current branch is " + mainlineBranch.FriendlyName);
155-
return mainlineBranch.Tip;
132+
return mainlineBranch;
133+
}
134+
135+
// prefer current branch, if it is a mainline branch
136+
if (possibleMainlineBranches.Any(context.CurrentBranch.IsSameBranch))
137+
{
138+
Logger.WriteInfo(string.Format("Choosing {0} as mainline because it is the current branch", context.CurrentBranch.FriendlyName));
139+
return context.CurrentBranch;
140+
}
141+
142+
// prefer a branch on which the merge base was a direct commit, if there is such a branch
143+
var firstMatchingCommitBranch = possibleMainlineBranches
144+
.FirstOrDefault(b =>
145+
{
146+
var filter = new CommitFilter
147+
{
148+
IncludeReachableFrom = b,
149+
ExcludeReachableFrom = baseVersionSource,
150+
FirstParentOnly = true,
151+
};
152+
var query = context.Repository.Commits.QueryBy(filter);
153+
154+
return query.Contains(firstMatchingCommit);
155+
});
156+
if (firstMatchingCommitBranch != null)
157+
{
158+
var message = string.Format(
159+
"Choosing {0} as mainline because {1}'s merge base was a direct commit to {0}",
160+
firstMatchingCommitBranch.FriendlyName,
161+
context.CurrentBranch.FriendlyName);
162+
Logger.WriteInfo(message);
163+
164+
return firstMatchingCommitBranch;
156165
}
157166

158167
var chosenMainline = possibleMainlineBranches[0];
159168
Logger.WriteInfo(string.Format(
160169
"Multiple mainlines ({0}) have the same merge base for the current branch, choosing {1} because we found that branch first...",
161170
string.Join(", ", possibleMainlineBranches.Select(b => b.FriendlyName)),
162171
chosenMainline.FriendlyName));
163-
return chosenMainline.Tip;
172+
return chosenMainline;
173+
}
174+
175+
/// <summary>
176+
/// Gets the commit on mainline at which <paramref name="mergeBase"/> was fully integrated.
177+
/// </summary>
178+
/// <param name="mainlineCommitLog">The collection of commits made directly to mainline, in reverse order.</param>
179+
/// <param name="mergeBase">The best possible merge base between <paramref name="mainlineTip"/> and the current commit.</param>
180+
/// <param name="mainlineTip">The tip of the mainline branch.</param>
181+
/// <returns>The commit on mainline at which <paramref name="mergeBase"/> was merged, if such a commit exists; otherwise, <paramref name="mainlineTip"/>.</returns>
182+
/// <remarks>
183+
/// This method gets the most recent commit on mainline that should be considered for versioning the current branch.
184+
/// </remarks>
185+
private static Commit GetEffectiveMainlineTip(IEnumerable<Commit> mainlineCommitLog, Commit mergeBase, Commit mainlineTip)
186+
{
187+
// find the commit that merged mergeBase into mainline
188+
foreach (var commit in mainlineCommitLog)
189+
{
190+
if (commit == mergeBase || commit.Parents.Contains(mergeBase))
191+
{
192+
Logger.WriteInfo(string.Format("Found branch merge point; choosing {0} as effective mainline tip", commit));
193+
return commit;
194+
}
195+
}
196+
197+
return mainlineTip;
198+
}
199+
200+
/// <summary>
201+
/// Gets the best possible merge base between the current commit and <paramref name="mainline"/> that is not the child of a forward merge.
202+
/// </summary>
203+
/// <param name="context">The current versioning context.</param>
204+
/// <param name="baseVersionSource">The commit that establishes the contextual base version.</param>
205+
/// <param name="mainline">The mainline branch.</param>
206+
/// <param name="mainlineTip">The commit on mainline at which the returned merge base was fully integrated.</param>
207+
/// <returns>The best possible merge base between the current commit and <paramref name="mainline"/> that is not the child of a forward merge.</returns>
208+
private static Commit FindMergeBaseBeforeForwardMerge(GitVersionContext context, Commit baseVersionSource, Branch mainline, out Commit mainlineTip)
209+
{
210+
var mergeBase = context.Repository.ObjectDatabase.FindMergeBase(context.CurrentCommit, mainline.Tip);
211+
var mainlineCommitLog = context.Repository.Commits
212+
.QueryBy(new CommitFilter
213+
{
214+
IncludeReachableFrom = mainline.Tip,
215+
ExcludeReachableFrom = baseVersionSource,
216+
SortBy = CommitSortStrategies.Reverse,
217+
FirstParentOnly = true
218+
})
219+
.ToList();
220+
221+
// find the mainline commit effective for versioning the current branch
222+
mainlineTip = GetEffectiveMainlineTip(mainlineCommitLog, mergeBase, mainline.Tip);
223+
224+
// detect forward merge and rewind mainlineTip to before it
225+
if (mergeBase == context.CurrentCommit && !mainlineCommitLog.Contains(mergeBase))
226+
{
227+
var mainlineTipPrevious = mainlineTip.Parents.First();
228+
var message = string.Format(
229+
"Detected forward merge at {0}; rewinding mainline to previous commit {1}",
230+
mainlineTip,
231+
mainlineTipPrevious);
232+
233+
Logger.WriteInfo(message);
234+
235+
// re-do mergeBase detection before the forward merge
236+
mergeBase = context.Repository.ObjectDatabase.FindMergeBase(context.CurrentCommit, mainlineTipPrevious);
237+
mainlineTip = GetEffectiveMainlineTip(mainlineCommitLog, mergeBase, mainlineTipPrevious);
238+
}
239+
240+
return mergeBase;
164241
}
165242

166-
private static SemanticVersion IncrementForEachCommit(GitVersionContext context, List<Commit> directCommits, SemanticVersion mainlineVersion, string branch = null)
243+
private static SemanticVersion IncrementForEachCommit(GitVersionContext context, List<Commit> directCommits, SemanticVersion mainlineVersion, Branch mainline)
167244
{
168245
foreach (var directCommit in directCommits)
169246
{
170247
var directCommitIncrement = IncrementStrategyFinder.GetIncrementForCommits(context, new[]
171248
{
172249
directCommit
173-
}) ?? IncrementStrategyFinder.FindDefaultIncrementForBranch(context, branch);
250+
}) ?? IncrementStrategyFinder.FindDefaultIncrementForBranch(context, mainline.FriendlyName);
174251
mainlineVersion = mainlineVersion.IncrementVersion(directCommitIncrement);
175252
Logger.WriteInfo(string.Format("Direct commit on master {0} incremented base versions {1}, now {2}",
176253
directCommit.Sha, directCommitIncrement, mainlineVersion));

0 commit comments

Comments
 (0)