Skip to content

Commit d4801d7

Browse files
authored
Merge pull request #1100 from DanielRose/cache-merge-base
Caching to improve performance
2 parents a88e863 + d8c600a commit d4801d7

14 files changed

+304
-194
lines changed

src/GitVersionCore.Tests/ConfigProviderTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ public void WarnOnObsoleteIsDevelopBranchConfigurationSetting()
322322
LegacyConfigNotifier.Notify(new StringReader(text));
323323
});
324324

325-
var expecedMessage = string.Format("'is-develop' is deprecated, use 'track-release-branches' instead.");
326-
exception.Message.ShouldContain(expecedMessage);
325+
const string expectedMessage = @"'is-develop' is deprecated, use 'track-release-branches' instead.";
326+
exception.Message.ShouldContain(expectedMessage);
327327
}
328328
}

src/GitVersionCore.Tests/IntegrationTests/FeatureBranchScenarios.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -514,9 +514,8 @@ public void PickUpVersionFromMasterMarkedWithIsTracksReleaseBranches()
514514
fixture.AssertFullSemver(config, "0.10.1-pre.1+1");
515515

516516
// create a feature branch from master and verify the version
517-
// TODO this will pass once default becomes inherit
518-
//fixture.BranchTo("MyFeatureD");
519-
//fixture.AssertFullSemver(config, "0.10.1-MyFeatureD.1+1");
517+
fixture.BranchTo("MyFeatureD");
518+
fixture.AssertFullSemver(config, "0.10.1-MyFeatureD.1+1");
520519
}
521520
}
522521
}

src/GitVersionCore/BranchConfigurationCalculator.cs

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ public class BranchConfigurationCalculator
1212
/// <summary>
1313
/// Gets the <see cref="BranchConfig"/> for the current commit.
1414
/// </summary>
15-
public static BranchConfig GetBranchConfiguration(Commit currentCommit, IRepository repository, bool onlyEvaluateTrackedBranches, Config config, Branch currentBranch, IList<Branch> excludedInheritBranches = null)
15+
public static BranchConfig GetBranchConfiguration(GitVersionContext context, Branch targetBranch, IList<Branch> excludedInheritBranches = null)
1616
{
17-
var matchingBranches = LookupBranchConfiguration(config, currentBranch).ToArray();
17+
var matchingBranches = LookupBranchConfiguration(context.FullConfiguration, targetBranch).ToArray();
1818

1919
BranchConfig branchConfiguration;
2020
if (matchingBranches.Length > 0)
@@ -25,7 +25,7 @@ public static BranchConfig GetBranchConfiguration(Commit currentCommit, IReposit
2525
{
2626
Logger.WriteWarning(string.Format(
2727
"Multiple branch configurations match the current branch branchName of '{0}'. Using the first matching configuration, '{1}'. Matching configurations include: '{2}'",
28-
currentBranch.FriendlyName,
28+
targetBranch.FriendlyName,
2929
branchConfiguration.Name,
3030
string.Join("', '", matchingBranches.Select(b => b.Name))));
3131
}
@@ -34,14 +34,14 @@ public static BranchConfig GetBranchConfiguration(Commit currentCommit, IReposit
3434
{
3535
Logger.WriteInfo(string.Format(
3636
"No branch configuration found for branch {0}, falling back to default configuration",
37-
currentBranch.FriendlyName));
37+
targetBranch.FriendlyName));
3838

3939
branchConfiguration = new BranchConfig { Name = string.Empty };
40-
ConfigurationProvider.ApplyBranchDefaults(config, branchConfiguration, "");
40+
ConfigurationProvider.ApplyBranchDefaults(context.FullConfiguration, branchConfiguration, "");
4141
}
4242

4343
return branchConfiguration.Increment == IncrementStrategy.Inherit ?
44-
InheritBranchConfiguration(onlyEvaluateTrackedBranches, repository, currentCommit, currentBranch, branchConfiguration, config, excludedInheritBranches) :
44+
InheritBranchConfiguration(context, targetBranch, branchConfiguration, excludedInheritBranches) :
4545
branchConfiguration;
4646
}
4747

@@ -60,16 +60,18 @@ static IEnumerable<BranchConfig> LookupBranchConfiguration([NotNull] Config conf
6060
return config.Branches.Where(b => Regex.IsMatch(currentBranch.FriendlyName, "^" + b.Value.Regex, RegexOptions.IgnoreCase)).Select(kvp => kvp.Value);
6161
}
6262

63-
static BranchConfig InheritBranchConfiguration(bool onlyEvaluateTrackedBranches, IRepository repository, Commit currentCommit, Branch currentBranch, BranchConfig branchConfiguration, Config config, IList<Branch> excludedInheritBranches)
63+
static BranchConfig InheritBranchConfiguration(GitVersionContext context, Branch targetBranch, BranchConfig branchConfiguration, IList<Branch> excludedInheritBranches)
6464
{
65+
var repository = context.Repository;
66+
var config = context.FullConfiguration;
6567
using (Logger.IndentLog("Attempting to inherit branch configuration from parent branch"))
6668
{
67-
var excludedBranches = new[] { currentBranch };
69+
var excludedBranches = new[] { targetBranch };
6870
// Check if we are a merge commit. If so likely we are a pull request
69-
var parentCount = currentCommit.Parents.Count();
71+
var parentCount = context.CurrentCommit.Parents.Count();
7072
if (parentCount == 2)
7173
{
72-
excludedBranches = CalculateWhenMultipleParents(repository, currentCommit, ref currentBranch, excludedBranches);
74+
excludedBranches = CalculateWhenMultipleParents(repository, context.CurrentCommit, ref targetBranch, excludedBranches);
7375
}
7476

7577
if (excludedInheritBranches == null)
@@ -83,25 +85,32 @@ static BranchConfig InheritBranchConfiguration(bool onlyEvaluateTrackedBranches,
8385
return (branchConfig.Length != 1) || (branchConfig.Length == 1 && branchConfig[0].Increment == IncrementStrategy.Inherit);
8486
}).ToList();
8587
}
86-
excludedBranches.ToList().ForEach(excludedInheritBranches.Add);
88+
// Add new excluded branches.
89+
foreach (var excludedBranch in excludedBranches.ExcludingBranches(excludedInheritBranches))
90+
{
91+
excludedInheritBranches.Add(excludedBranch);
92+
}
8793
var branchesToEvaluate = repository.Branches.Except(excludedInheritBranches).ToList();
8894

89-
var branchPoint = currentBranch.FindCommitBranchWasBranchedFrom(repository, excludedInheritBranches.ToArray());
95+
var branchPoint = context.RepositoryMetadataProvider
96+
.FindCommitBranchWasBranchedFrom(targetBranch, excludedInheritBranches.ToArray());
9097
List<Branch> possibleParents;
9198
if (branchPoint == BranchCommit.Empty)
9299
{
93-
possibleParents = currentCommit.GetBranchesContainingCommit(repository, branchesToEvaluate, true)
100+
possibleParents = context.RepositoryMetadataProvider.GetBranchesContainingCommit(context.CurrentCommit, branchesToEvaluate, true)
94101
// It fails to inherit Increment branch configuration if more than 1 parent;
95102
// therefore no point to get more than 2 parents
96103
.Take(2)
97104
.ToList();
98105
}
99106
else
100107
{
101-
var branches = branchPoint.Commit.GetBranchesContainingCommit(repository, branchesToEvaluate, true).ToList();
108+
var branches = context.RepositoryMetadataProvider
109+
.GetBranchesContainingCommit(branchPoint.Commit, branchesToEvaluate, true).ToList();
102110
if (branches.Count > 1)
103111
{
104-
var currentTipBranches = currentCommit.GetBranchesContainingCommit(repository, branchesToEvaluate, true).ToList();
112+
var currentTipBranches = context.RepositoryMetadataProvider
113+
.GetBranchesContainingCommit(context.CurrentCommit, branchesToEvaluate, true).ToList();
105114
possibleParents = branches.Except(currentTipBranches).ToList();
106115
}
107116
else
@@ -114,7 +123,7 @@ static BranchConfig InheritBranchConfiguration(bool onlyEvaluateTrackedBranches,
114123

115124
if (possibleParents.Count == 1)
116125
{
117-
var branchConfig = GetBranchConfiguration(currentCommit, repository, onlyEvaluateTrackedBranches, config, possibleParents[0], excludedInheritBranches);
126+
var branchConfig = GetBranchConfiguration(context, possibleParents[0], excludedInheritBranches);
118127
return new BranchConfig(branchConfiguration)
119128
{
120129
Increment = branchConfig.Increment,
@@ -144,7 +153,17 @@ static BranchConfig InheritBranchConfiguration(bool onlyEvaluateTrackedBranches,
144153
var branchName = chosenBranch.FriendlyName;
145154
Logger.WriteWarning(errorMessage + Environment.NewLine + Environment.NewLine + "Falling back to " + branchName + " branch config");
146155

147-
var inheritingBranchConfig = GetBranchConfiguration(currentCommit, repository, onlyEvaluateTrackedBranches, config, chosenBranch);
156+
// To prevent infinite loops, make sure that a new branch was chosen.
157+
if (targetBranch.IsSameBranch(chosenBranch))
158+
{
159+
Logger.WriteWarning("Fallback branch wants to inherit Increment branch configuration from itself. Using patch increment instead.");
160+
return new BranchConfig(branchConfiguration)
161+
{
162+
Increment = IncrementStrategy.Patch
163+
};
164+
}
165+
166+
var inheritingBranchConfig = GetBranchConfiguration(context, chosenBranch, excludedInheritBranches);
148167
return new BranchConfig(branchConfiguration)
149168
{
150169
Increment = inheritingBranchConfig.Increment,

src/GitVersionCore/Configuration/ConfigurationProvider.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ namespace GitVersion
22
{
33
using Configuration.Init.Wizard;
44
using GitVersion.Helpers;
5-
using System;
65
using System.IO;
76
using System.Linq;
87
using System.Text;
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
using JetBrains.Annotations;
2+
using LibGit2Sharp;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
7+
namespace GitVersion
8+
{
9+
public class GitRepoMetadataProvider
10+
{
11+
private Dictionary<Branch, List<BranchCommit>> mergeBaseCommitsCache;
12+
private Dictionary<Tuple<Branch, Branch>, MergeBaseData> mergeBaseCache;
13+
private Dictionary<Branch, List<SemanticVersion>> semanticVersionTagsOnBranchCache;
14+
private IRepository Repository { get; set; }
15+
const string missingTipFormat = "{0} has no tip. Please see http://example.com/docs for information on how to fix this.";
16+
17+
public GitRepoMetadataProvider(IRepository repository)
18+
{
19+
mergeBaseCache = new Dictionary<Tuple<Branch, Branch>, MergeBaseData>();
20+
mergeBaseCommitsCache = new Dictionary<Branch, List<BranchCommit>>();
21+
semanticVersionTagsOnBranchCache = new Dictionary<Branch, List<SemanticVersion>>();
22+
this.Repository = repository;
23+
}
24+
25+
public IEnumerable<SemanticVersion> GetVersionTagsOnBranch(Branch branch, string tagPrefixRegex)
26+
{
27+
if (semanticVersionTagsOnBranchCache.ContainsKey(branch))
28+
{
29+
Logger.WriteDebug(string.Format("Cache hit for version tags on branch '{0}", branch.CanonicalName));
30+
return semanticVersionTagsOnBranchCache[branch];
31+
}
32+
33+
using (Logger.IndentLog(string.Format("Getting version tags from branch '{0}'.", branch.CanonicalName)))
34+
{
35+
var tags = this.Repository.Tags.Select(t => t).ToList();
36+
37+
var versionTags = this.Repository.Commits.QueryBy(new CommitFilter
38+
{
39+
IncludeReachableFrom = branch.Tip
40+
})
41+
.SelectMany(c => tags.Where(t => c.Sha == t.Target.Sha).SelectMany(t =>
42+
{
43+
SemanticVersion semver;
44+
if (SemanticVersion.TryParse(t.FriendlyName, tagPrefixRegex, out semver))
45+
return new[] { semver };
46+
return new SemanticVersion[0];
47+
})).ToList();
48+
49+
semanticVersionTagsOnBranchCache.Add(branch, versionTags);
50+
return versionTags;
51+
}
52+
}
53+
54+
// TODO Should we cache this?
55+
public IEnumerable<Branch> GetBranchesContainingCommit([NotNull] Commit commit, IList<Branch> branches, bool onlyTrackedBranches)
56+
{
57+
if (commit == null)
58+
{
59+
throw new ArgumentNullException("commit");
60+
}
61+
62+
using (Logger.IndentLog(string.Format("Getting branches containing the commit '{0}'.", commit.Id)))
63+
{
64+
var directBranchHasBeenFound = false;
65+
Logger.WriteInfo("Trying to find direct branches.");
66+
// TODO: It looks wasteful looping through the branches twice. Can't these loops be merged somehow? @asbjornu
67+
foreach (var branch in branches)
68+
{
69+
if (branch.Tip != null && branch.Tip.Sha != commit.Sha || (onlyTrackedBranches && !branch.IsTracking))
70+
{
71+
continue;
72+
}
73+
74+
directBranchHasBeenFound = true;
75+
Logger.WriteInfo(string.Format("Direct branch found: '{0}'.", branch.FriendlyName));
76+
yield return branch;
77+
}
78+
79+
if (directBranchHasBeenFound)
80+
{
81+
yield break;
82+
}
83+
84+
Logger.WriteInfo(string.Format("No direct branches found, searching through {0} branches.", onlyTrackedBranches ? "tracked" : "all"));
85+
foreach (var branch in branches.Where(b => onlyTrackedBranches && !b.IsTracking))
86+
{
87+
Logger.WriteInfo(string.Format("Searching for commits reachable from '{0}'.", branch.FriendlyName));
88+
89+
var commits = this.Repository.Commits.QueryBy(new CommitFilter
90+
{
91+
IncludeReachableFrom = branch
92+
}).Where(c => c.Sha == commit.Sha);
93+
94+
if (!commits.Any())
95+
{
96+
Logger.WriteInfo(string.Format("The branch '{0}' has no matching commits.", branch.FriendlyName));
97+
continue;
98+
}
99+
100+
Logger.WriteInfo(string.Format("The branch '{0}' has a matching commit.", branch.FriendlyName));
101+
yield return branch;
102+
}
103+
}
104+
}
105+
106+
/// <summary>
107+
/// Find the merge base of the two branches, i.e. the best common ancestor of the two branches' tips.
108+
/// </summary>
109+
public Commit FindMergeBase(Branch branch, Branch otherBranch)
110+
{
111+
var key = Tuple.Create(branch, otherBranch);
112+
113+
if (mergeBaseCache.ContainsKey(key))
114+
{
115+
Logger.WriteDebug(string.Format(
116+
"Cache hit for merge base between '{0}' and '{1}'.",
117+
branch.FriendlyName, otherBranch.FriendlyName));
118+
return mergeBaseCache[key].MergeBase;
119+
}
120+
121+
using (Logger.IndentLog(string.Format("Finding merge base between '{0}' and '{1}'.", branch.FriendlyName, otherBranch.FriendlyName)))
122+
{
123+
// Otherbranch tip is a forward merge
124+
var commitToFindCommonBase = otherBranch.Tip;
125+
var commit = branch.Tip;
126+
if (otherBranch.Tip.Parents.Contains(commit))
127+
{
128+
commitToFindCommonBase = otherBranch.Tip.Parents.First();
129+
}
130+
131+
var findMergeBase = this.Repository.ObjectDatabase.FindMergeBase(commit, commitToFindCommonBase);
132+
if (findMergeBase != null)
133+
{
134+
Logger.WriteInfo(string.Format("Found merge base of {0}", findMergeBase.Sha));
135+
// We do not want to include merge base commits which got forward merged into the other branch
136+
bool mergeBaseWasForwardMerge;
137+
do
138+
{
139+
// Now make sure that the merge base is not a forward merge
140+
mergeBaseWasForwardMerge = otherBranch.Commits
141+
.SkipWhile(c => c != commitToFindCommonBase)
142+
.TakeWhile(c => c != findMergeBase)
143+
.Any(c => c.Parents.Contains(findMergeBase));
144+
if (mergeBaseWasForwardMerge)
145+
{
146+
var second = commitToFindCommonBase.Parents.First();
147+
var mergeBase = this.Repository.ObjectDatabase.FindMergeBase(commit, second);
148+
if (mergeBase == findMergeBase)
149+
{
150+
break;
151+
}
152+
findMergeBase = mergeBase;
153+
Logger.WriteInfo(string.Format("Merge base was due to a forward merge, next merge base is {0}", findMergeBase));
154+
}
155+
} while (mergeBaseWasForwardMerge);
156+
}
157+
158+
// Store in cache.
159+
mergeBaseCache.Add(key, new MergeBaseData(branch, otherBranch, this.Repository, findMergeBase));
160+
161+
return findMergeBase;
162+
}
163+
}
164+
165+
/// <summary>
166+
/// Find the commit where the given branch was branched from another branch.
167+
/// If there are multiple such commits and branches, returns the newest commit.
168+
/// </summary>
169+
public BranchCommit FindCommitBranchWasBranchedFrom([NotNull] Branch branch, params Branch[] excludedBranches)
170+
{
171+
if (branch == null)
172+
{
173+
throw new ArgumentNullException("branch");
174+
}
175+
176+
using (Logger.IndentLog(string.Format("Finding branch source of '{0}'", branch.FriendlyName)))
177+
{
178+
if (branch.Tip == null)
179+
{
180+
Logger.WriteWarning(string.Format(missingTipFormat, branch.FriendlyName));
181+
return BranchCommit.Empty;
182+
}
183+
184+
return GetMergeCommitsForBranch(branch).ExcludingBranches(excludedBranches).FirstOrDefault(b => !branch.IsSameBranch(b.Branch));
185+
}
186+
}
187+
188+
List<BranchCommit> GetMergeCommitsForBranch(Branch branch)
189+
{
190+
if (mergeBaseCommitsCache.ContainsKey(branch))
191+
{
192+
Logger.WriteDebug(string.Format(
193+
"Cache hit for getting merge commits for branch {0}.",
194+
branch.CanonicalName));
195+
return mergeBaseCommitsCache[branch];
196+
}
197+
198+
var branchMergeBases = Repository.Branches.Select(otherBranch =>
199+
{
200+
if (otherBranch.Tip == null)
201+
{
202+
Logger.WriteWarning(string.Format(missingTipFormat, otherBranch.FriendlyName));
203+
return BranchCommit.Empty;
204+
}
205+
206+
var findMergeBase = FindMergeBase(branch, otherBranch);
207+
return new BranchCommit(findMergeBase, otherBranch);
208+
}).Where(b => b.Commit != null).OrderByDescending(b => b.Commit.Committer.When).ToList();
209+
mergeBaseCommitsCache.Add(branch, branchMergeBases);
210+
211+
return branchMergeBases;
212+
}
213+
214+
private class MergeBaseData
215+
{
216+
public Branch Branch { get; private set; }
217+
public Branch OtherBranch { get; private set; }
218+
public IRepository Repository { get; private set; }
219+
220+
public Commit MergeBase { get; private set; }
221+
222+
public MergeBaseData(Branch branch, Branch otherBranch, IRepository repository, Commit mergeBase)
223+
{
224+
Branch = branch;
225+
OtherBranch = otherBranch;
226+
Repository = repository;
227+
MergeBase = mergeBase;
228+
}
229+
}
230+
}
231+
}

0 commit comments

Comments
 (0)