Skip to content

Commit e5f1c1d

Browse files
committed
Extracted Mainline mode logic into it's own class
1 parent bd16d24 commit e5f1c1d

File tree

3 files changed

+219
-189
lines changed

3 files changed

+219
-189
lines changed

src/GitVersionCore/GitVersionCore.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
<Compile Include="SemanticVersionExtensions.cs" />
134134
<Compile Include="SemanticVersionFormatValues.cs" />
135135
<Compile Include="VerbosityLevel.cs" />
136+
<Compile Include="VersionCalculation\MainlineVersionCalculator.cs" />
136137
<Compile Include="VersionFilters\MinDateVersionFilter.cs" />
137138
<Compile Include="VersionFilters\IVersionFilter.cs" />
138139
<Compile Include="VersionFilters\ShaVersionFilter.cs" />
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
using GitVersion.VersionCalculation.BaseVersionCalculators;
2+
using LibGit2Sharp;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Text.RegularExpressions;
8+
9+
namespace GitVersion.VersionCalculation
10+
{
11+
class MainlineVersionCalculator
12+
{
13+
IMetaDataCalculator metaDataCalculator;
14+
15+
public MainlineVersionCalculator(IMetaDataCalculator metaDataCalculator)
16+
{
17+
this.metaDataCalculator = metaDataCalculator;
18+
}
19+
20+
public SemanticVersion FindMainlineModeVersion(BaseVersion baseVersion, GitVersionContext context)
21+
{
22+
if (baseVersion.SemanticVersion.PreReleaseTag.HasTag())
23+
{
24+
throw new NotSupportedException("Mainline development mode doesn't yet support pre-release tags on master");
25+
}
26+
27+
using (Logger.IndentLog("Using mainline development mode to calculate current version"))
28+
{
29+
var mainlineVersion = baseVersion.SemanticVersion;
30+
31+
// Forward merge / PR
32+
// * feature/foo
33+
// / |
34+
// master * *
35+
//
36+
37+
var mainlineTip = GetMainlineTip(context);
38+
var commitsNotOnMainline = context.Repository.Commits.QueryBy(new CommitFilter
39+
{
40+
IncludeReachableFrom = context.CurrentBranch,
41+
ExcludeReachableFrom = mainlineTip,
42+
SortBy = CommitSortStrategies.Reverse,
43+
FirstParentOnly = true
44+
}).Where(c => c.Sha != baseVersion.BaseVersionSource.Sha && c.Parents.Count() == 1).ToList();
45+
var commitLog = context.Repository.Commits.QueryBy(new CommitFilter
46+
{
47+
IncludeReachableFrom = context.CurrentBranch,
48+
ExcludeReachableFrom = baseVersion.BaseVersionSource,
49+
SortBy = CommitSortStrategies.Reverse,
50+
FirstParentOnly = true
51+
})
52+
.Where(c => c.Sha != baseVersion.BaseVersionSource.Sha)
53+
.Except(commitsNotOnMainline)
54+
.ToList();
55+
56+
var directCommits = new List<Commit>();
57+
58+
// Scans commit log in reverse, aggregating merge commits
59+
foreach (var commit in commitLog)
60+
{
61+
directCommits.Add(commit);
62+
if (commit.Parents.Count() > 1)
63+
{
64+
mainlineVersion = AggregateMergeCommitIncrement(context, commit, directCommits, mainlineVersion);
65+
}
66+
}
67+
68+
if (context.CurrentBranch.FriendlyName != "master")
69+
{
70+
var mergedHead = context.CurrentCommit;
71+
var findMergeBase = context.Repository.ObjectDatabase.FindMergeBase(context.CurrentCommit, mainlineTip);
72+
Logger.WriteInfo(string.Format("Current branch ({0}) was branch from {1}", context.CurrentBranch.FriendlyName, findMergeBase));
73+
74+
var branchIncrement = FindMessageIncrement(context, null, mergedHead, findMergeBase, directCommits);
75+
// This will increment for any direct commits on master
76+
mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion);
77+
mainlineVersion.BuildMetaData = metaDataCalculator.Create(findMergeBase, context);
78+
// Don't increment if the merge commit is a merge into mainline
79+
// this ensures PR's and forward merges end up correct.
80+
if (mergedHead.Parents.Count() == 1 || mergedHead.Parents.First() != mainlineTip)
81+
{
82+
Logger.WriteInfo(string.Format("Performing {0} increment for current branch ", branchIncrement));
83+
mainlineVersion = mainlineVersion.IncrementVersion(branchIncrement);
84+
}
85+
}
86+
else
87+
{
88+
// If we are on master, make sure no commits get left behind
89+
mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion);
90+
mainlineVersion.BuildMetaData = metaDataCalculator.Create(baseVersion.BaseVersionSource, context);
91+
}
92+
93+
return mainlineVersion;
94+
}
95+
}
96+
97+
SemanticVersion AggregateMergeCommitIncrement(GitVersionContext context, Commit commit, List<Commit> directCommits, SemanticVersion mainlineVersion)
98+
{
99+
// Merge commit, process all merged commits as a batch
100+
var mergeCommit = commit;
101+
var mergedHead = GetMergedHead(mergeCommit);
102+
var findMergeBase = context.Repository.ObjectDatabase.FindMergeBase(mergeCommit.Parents.First(), mergedHead);
103+
var findMessageIncrement = FindMessageIncrement(context, mergeCommit, mergedHead, findMergeBase, directCommits);
104+
105+
// If this collection is not empty there has been some direct commits against master
106+
// Treat each commit as it's own 'release', we need to do this before we increment the branch
107+
mainlineVersion = IncrementForEachCommit(context, directCommits, mainlineVersion);
108+
directCommits.Clear();
109+
110+
// Finally increment for the branch
111+
mainlineVersion = mainlineVersion.IncrementVersion(findMessageIncrement);
112+
Logger.WriteInfo(string.Format("Merge commit {0} incremented base versions {1}, now {2}",
113+
mergeCommit.Sha, findMessageIncrement, mainlineVersion));
114+
return mainlineVersion;
115+
}
116+
117+
static Commit GetMainlineTip(GitVersionContext context)
118+
{
119+
var mainlineBranchConfigs = context.FullConfiguration.Branches.Where(b => b.Value.IsMainline == true).ToList();
120+
var seenMainlineTips = new List<string>();
121+
var mainlineBranches = context.Repository.Branches
122+
.Where(b =>
123+
{
124+
return mainlineBranchConfigs.Any(c => Regex.IsMatch(b.FriendlyName, c.Key));
125+
})
126+
.Where(b =>
127+
{
128+
if (seenMainlineTips.Contains(b.Tip.Sha))
129+
{
130+
Logger.WriteInfo("Multiple possible mainlines pointing at the same commit, dropping " + b.FriendlyName);
131+
return false;
132+
}
133+
seenMainlineTips.Add(b.Tip.Sha);
134+
return true;
135+
})
136+
.Select(b => new
137+
{
138+
MergeBase = context.Repository.ObjectDatabase.FindMergeBase(b.Tip, context.CurrentCommit),
139+
Branch = b
140+
})
141+
.Where(a => a.MergeBase != null)
142+
.GroupBy(b => b.MergeBase.Sha, b => b.Branch)
143+
.ToDictionary(b => b.Key, b => b.ToList());
144+
145+
var allMainlines = mainlineBranches.Values.SelectMany(branches => branches.Select(b => b.FriendlyName));
146+
Logger.WriteInfo("Found possible mainline branches: " + string.Join(", ", allMainlines));
147+
148+
// Find closest mainline branch
149+
var firstMatchingCommit = context.CurrentBranch.Commits.First(c => mainlineBranches.ContainsKey(c.Sha));
150+
var possibleMainlineBranches = mainlineBranches[firstMatchingCommit.Sha];
151+
152+
if (possibleMainlineBranches.Count == 1)
153+
{
154+
var mainlineBranch = possibleMainlineBranches[0];
155+
Logger.WriteInfo("Mainline for current branch is " + mainlineBranch.FriendlyName);
156+
return mainlineBranch.Tip;
157+
}
158+
159+
var chosenMainline = possibleMainlineBranches[0];
160+
Logger.WriteInfo(string.Format(
161+
"Multiple mainlines ({0}) have the same merge base for the current branch, choosing {1} because we found that branch first...",
162+
string.Join(", ", possibleMainlineBranches.Select(b => b.FriendlyName)),
163+
chosenMainline.FriendlyName));
164+
return chosenMainline.Tip;
165+
}
166+
167+
private static SemanticVersion IncrementForEachCommit(GitVersionContext context, List<Commit> directCommits, SemanticVersion mainlineVersion)
168+
{
169+
foreach (var directCommit in directCommits)
170+
{
171+
var directCommitIncrement = IncrementStrategyFinder.GetIncrementForCommits(context, new[]
172+
{
173+
directCommit
174+
}) ?? VersionField.Patch;
175+
mainlineVersion = mainlineVersion.IncrementVersion(directCommitIncrement);
176+
Logger.WriteInfo(string.Format("Direct commit on master {0} incremented base versions {1}, now {2}",
177+
directCommit.Sha, directCommitIncrement, mainlineVersion));
178+
}
179+
return mainlineVersion;
180+
}
181+
182+
private static VersionField FindMessageIncrement(
183+
GitVersionContext context, Commit mergeCommit, Commit mergedHead, Commit findMergeBase, List<Commit> commitLog)
184+
{
185+
var filter = new CommitFilter
186+
{
187+
IncludeReachableFrom = mergedHead,
188+
ExcludeReachableFrom = findMergeBase
189+
};
190+
var commits = mergeCommit == null ?
191+
context.Repository.Commits.QueryBy(filter).ToList() :
192+
new[] { mergeCommit }.Union(context.Repository.Commits.QueryBy(filter)).ToList();
193+
commitLog.RemoveAll(c => commits.Any(c1 => c1.Sha == c.Sha));
194+
return IncrementStrategyFinder.GetIncrementForCommits(context, commits)
195+
?? TryFindIncrementFromMergeMessage(mergeCommit);
196+
}
197+
198+
private static VersionField TryFindIncrementFromMergeMessage(Commit mergeCommit)
199+
{
200+
201+
202+
// Fallback to patch
203+
return VersionField.Patch;
204+
}
205+
206+
private Commit GetMergedHead(Commit mergeCommit)
207+
{
208+
var parents = mergeCommit.Parents.Skip(1).ToList();
209+
if (parents.Count > 1)
210+
throw new NotSupportedException("Mainline development does not support more than one merge source in a single commit yet");
211+
return parents.Single();
212+
}
213+
}
214+
}

0 commit comments

Comments
 (0)