Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache merge base #1106

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -514,9 +514,8 @@ public void PickUpVersionFromMasterMarkedWithIsTracksReleaseBranches()
fixture.AssertFullSemver(config, "0.10.1-pre.1+1");

// create a feature branch from master and verify the version
// TODO this will pass once default becomes inherit
//fixture.BranchTo("MyFeatureD");
//fixture.AssertFullSemver(config, "0.10.1-MyFeatureD.1+1");
fixture.BranchTo("MyFeatureD");
fixture.AssertFullSemver(config, "0.10.1-MyFeatureD.1+1");
}
}
}
39 changes: 22 additions & 17 deletions src/GitVersionCore/BranchConfigurationCalculator.cs
Expand Up @@ -12,9 +12,9 @@ public class BranchConfigurationCalculator
/// <summary>
/// Gets the <see cref="BranchConfig"/> for the current commit.
/// </summary>
public static BranchConfig GetBranchConfiguration(Commit currentCommit, IRepository repository, bool onlyEvaluateTrackedBranches, Config config, Branch currentBranch, IList<Branch> excludedInheritBranches = null)
public static BranchConfig GetBranchConfiguration(GitVersionContext context, Branch targetBranch, IList<Branch> excludedInheritBranches = null)
{
var matchingBranches = LookupBranchConfiguration(config, currentBranch).ToArray();
var matchingBranches = LookupBranchConfiguration(context.FullConfiguration, targetBranch).ToArray();

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

branchConfiguration = new BranchConfig { Name = string.Empty };
ConfigurationProvider.ApplyBranchDefaults(config, branchConfiguration, "");
ConfigurationProvider.ApplyBranchDefaults(context.FullConfiguration, branchConfiguration, "");
}

return branchConfiguration.Increment == IncrementStrategy.Inherit ?
InheritBranchConfiguration(onlyEvaluateTrackedBranches, repository, currentCommit, currentBranch, branchConfiguration, config, excludedInheritBranches) :
InheritBranchConfiguration(context, targetBranch, branchConfiguration, excludedInheritBranches) :
branchConfiguration;
}

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

static BranchConfig InheritBranchConfiguration(bool onlyEvaluateTrackedBranches, IRepository repository, Commit currentCommit, Branch currentBranch, BranchConfig branchConfiguration, Config config, IList<Branch> excludedInheritBranches)
static BranchConfig InheritBranchConfiguration(GitVersionContext context, Branch targetBranch, BranchConfig branchConfiguration, IList<Branch> excludedInheritBranches)
{
var repository = context.Repository;
var config = context.FullConfiguration;
using (Logger.IndentLog("Attempting to inherit branch configuration from parent branch"))
{
var excludedBranches = new[] { currentBranch };
var excludedBranches = new[] { targetBranch };
// Check if we are a merge commit. If so likely we are a pull request
var parentCount = currentCommit.Parents.Count();
var parentCount = context.CurrentCommit.Parents.Count();
if (parentCount == 2)
{
excludedBranches = CalculateWhenMultipleParents(repository, currentCommit, ref currentBranch, excludedBranches);
excludedBranches = CalculateWhenMultipleParents(repository, context.CurrentCommit, ref targetBranch, excludedBranches);
}

if (excludedInheritBranches == null)
Expand All @@ -90,22 +92,25 @@ static BranchConfig InheritBranchConfiguration(bool onlyEvaluateTrackedBranches,
}
var branchesToEvaluate = repository.Branches.Except(excludedInheritBranches).ToList();

var branchPoint = currentBranch.FindCommitBranchWasBranchedFrom(repository, excludedInheritBranches.ToArray());
var branchPoint = context.RepostioryMetadataProvider
.FindCommitBranchWasBranchedFrom(targetBranch, repository, excludedInheritBranches.ToArray());
List<Branch> possibleParents;
if (branchPoint == BranchCommit.Empty)
{
possibleParents = currentCommit.GetBranchesContainingCommit(repository, branchesToEvaluate, true)
possibleParents = context.RepostioryMetadataProvider.GetBranchesContainingCommit(context.CurrentCommit, repository, branchesToEvaluate, true)
// It fails to inherit Increment branch configuration if more than 1 parent;
// therefore no point to get more than 2 parents
.Take(2)
.ToList();
}
else
{
var branches = branchPoint.Commit.GetBranchesContainingCommit(repository, branchesToEvaluate, true).ToList();
var branches = context.RepostioryMetadataProvider
.GetBranchesContainingCommit(branchPoint.Commit, repository, branchesToEvaluate, true).ToList();
if (branches.Count > 1)
{
var currentTipBranches = currentCommit.GetBranchesContainingCommit(repository, branchesToEvaluate, true).ToList();
var currentTipBranches = context.RepostioryMetadataProvider
.GetBranchesContainingCommit(context.CurrentCommit, repository, branchesToEvaluate, true).ToList();
possibleParents = branches.Except(currentTipBranches).ToList();
}
else
Expand All @@ -118,7 +123,7 @@ static BranchConfig InheritBranchConfiguration(bool onlyEvaluateTrackedBranches,

if (possibleParents.Count == 1)
{
var branchConfig = GetBranchConfiguration(currentCommit, repository, onlyEvaluateTrackedBranches, config, possibleParents[0], excludedInheritBranches);
var branchConfig = GetBranchConfiguration(context, possibleParents[0], excludedInheritBranches);
return new BranchConfig(branchConfiguration)
{
Increment = branchConfig.Increment,
Expand Down Expand Up @@ -149,7 +154,7 @@ static BranchConfig InheritBranchConfiguration(bool onlyEvaluateTrackedBranches,
Logger.WriteWarning(errorMessage + Environment.NewLine + Environment.NewLine + "Falling back to " + branchName + " branch config");

// To prevent infinite loops, make sure that a new branch was chosen.
if (LibGitExtensions.IsSameBranch(currentBranch, chosenBranch))
if (LibGitExtensions.IsSameBranch(targetBranch, chosenBranch))
{
Logger.WriteWarning("Fallback branch wants to inherit Increment branch configuration from itself. Using patch increment instead.");
return new BranchConfig(branchConfiguration)
Expand All @@ -158,7 +163,7 @@ static BranchConfig InheritBranchConfiguration(bool onlyEvaluateTrackedBranches,
};
}

var inheritingBranchConfig = GetBranchConfiguration(currentCommit, repository, onlyEvaluateTrackedBranches, config, chosenBranch, excludedInheritBranches);
var inheritingBranchConfig = GetBranchConfiguration(context, chosenBranch, excludedInheritBranches);
return new BranchConfig(branchConfiguration)
{
Increment = inheritingBranchConfig.Increment,
Expand Down
222 changes: 222 additions & 0 deletions src/GitVersionCore/GitRepoMetadataProvider.cs
@@ -0,0 +1,222 @@
using JetBrains.Annotations;
using LibGit2Sharp;
using System;
using System.Collections.Generic;
using System.Linq;

namespace GitVersion
{
public class GitRepoMetadataProvider
{
private Dictionary<Branch, List<BranchCommit>> mergeBaseCommitsCache;
private Dictionary<Tuple<Branch, Branch>, MergeBaseData> mergeBaseCache;
private Dictionary<Branch, List<SemanticVersion>> semanticVersionTagsOnBranchCache;
private IRepository repository;
const string missingTipFormat = "{0} has no tip. Please see http://example.com/docs for information on how to fix this.";


public GitRepoMetadataProvider(IRepository repository)
{
mergeBaseCache = new Dictionary<Tuple<Branch, Branch>, MergeBaseData>();
mergeBaseCommitsCache = new Dictionary<Branch, List<BranchCommit>>();
semanticVersionTagsOnBranchCache = new Dictionary<Branch, List<SemanticVersion>>();
this.repository = repository;
}

public IEnumerable<SemanticVersion> GetVersionTagsOnBranch(Branch branch, IRepository repository, string tagPrefixRegex)
{
if (semanticVersionTagsOnBranchCache.ContainsKey(branch))
{
return semanticVersionTagsOnBranchCache[branch];
}
var tags = repository.Tags.Select(t => t).ToList();

var versionTags = repository.Commits.QueryBy(new CommitFilter
{
IncludeReachableFrom = branch.Tip
})
.SelectMany(c => tags.Where(t => c.Sha == t.Target.Sha).SelectMany(t =>
{
SemanticVersion semver;
if (SemanticVersion.TryParse(t.FriendlyName, tagPrefixRegex, out semver))
return new[] { semver };
return new SemanticVersion[0];
})).ToList();

semanticVersionTagsOnBranchCache.Add(branch, versionTags);
return versionTags;
}

// TODO Should we cache this?
public IEnumerable<Branch> GetBranchesContainingCommit([NotNull] Commit commit, IRepository repository, IList<Branch> branches, bool onlyTrackedBranches)
{
if (commit == null)
{
throw new ArgumentNullException("commit");
}

using (Logger.IndentLog(string.Format("Getting branches containing the commit '{0}'.", commit.Id)))
{
var directBranchHasBeenFound = false;
Logger.WriteInfo("Trying to find direct branches.");
// TODO: It looks wasteful looping through the branches twice. Can't these loops be merged somehow? @asbjornu
foreach (var branch in branches)
{
if (branch.Tip != null && branch.Tip.Sha != commit.Sha || (onlyTrackedBranches && !branch.IsTracking))
{
continue;
}

directBranchHasBeenFound = true;
Logger.WriteInfo(string.Format("Direct branch found: '{0}'.", branch.FriendlyName));
yield return branch;
}

if (directBranchHasBeenFound)
{
yield break;
}

Logger.WriteInfo(string.Format("No direct branches found, searching through {0} branches.", onlyTrackedBranches ? "tracked" : "all"));
foreach (var branch in branches.Where(b => onlyTrackedBranches && !b.IsTracking))
{
Logger.WriteInfo(string.Format("Searching for commits reachable from '{0}'.", branch.FriendlyName));

var commits = repository.Commits.QueryBy(new CommitFilter
{
IncludeReachableFrom = branch
}).Where(c => c.Sha == commit.Sha);

if (!commits.Any())
{
Logger.WriteInfo(string.Format("The branch '{0}' has no matching commits.", branch.FriendlyName));
continue;
}

Logger.WriteInfo(string.Format("The branch '{0}' has a matching commit.", branch.FriendlyName));
yield return branch;
}
}
}

/// <summary>
/// Find the merge base of the two branches, i.e. the best common ancestor of the two branches' tips.
/// </summary>
public Commit FindMergeBase(Branch branch, Branch otherBranch, IRepository repository)
{
var key = Tuple.Create(branch, otherBranch);

if (mergeBaseCache.ContainsKey(key))
{
return mergeBaseCache[key].MergeBase;
}

using (Logger.IndentLog(string.Format("Finding merge base between '{0}' and '{1}'.", branch.FriendlyName, otherBranch.FriendlyName)))
{
// Otherbranch tip is a forward merge
var commitToFindCommonBase = otherBranch.Tip;
var commit = branch.Tip;
if (otherBranch.Tip.Parents.Contains(commit))
{
commitToFindCommonBase = otherBranch.Tip.Parents.First();
}

var findMergeBase = repository.ObjectDatabase.FindMergeBase(commit, commitToFindCommonBase);
if (findMergeBase != null)
{
Logger.WriteInfo(string.Format("Found merge base of {0}", findMergeBase.Sha));
// We do not want to include merge base commits which got forward merged into the other branch
bool mergeBaseWasForwardMerge;
do
{
// Now make sure that the merge base is not a forward merge
mergeBaseWasForwardMerge = otherBranch.Commits
.SkipWhile(c => c != commitToFindCommonBase)
.TakeWhile(c => c != findMergeBase)
.Any(c => c.Parents.Contains(findMergeBase));
if (mergeBaseWasForwardMerge)
{
var second = commitToFindCommonBase.Parents.First();
var mergeBase = repository.ObjectDatabase.FindMergeBase(commit, second);
if (mergeBase == findMergeBase)
{
break;
}
findMergeBase = mergeBase;
Logger.WriteInfo(string.Format("Merge base was due to a forward merge, next merge base is {0}", findMergeBase));
}
} while (mergeBaseWasForwardMerge);
}

// Store in cache.
mergeBaseCache.Add(key, new MergeBaseData(branch, otherBranch, repository, findMergeBase));

return findMergeBase;
}
}

/// <summary>
/// Find the commit where the given branch was branched from another branch.
/// If there are multiple such commits and branches, returns the newest commit.
/// </summary>
public BranchCommit FindCommitBranchWasBranchedFrom([NotNull] Branch branch, IRepository repository, params Branch[] excludedBranches)
{
if (branch == null)
{
throw new ArgumentNullException("branch");
}

using (Logger.IndentLog(string.Format("Finding branch source of '{0}'", branch.FriendlyName)))
{
if (branch.Tip == null)
{
Logger.WriteWarning(string.Format(missingTipFormat, branch.FriendlyName));
return BranchCommit.Empty;
}

return GetMergeCommitsForBranch(branch).ExcludingBranches(excludedBranches).FirstOrDefault(b => !branch.IsSameBranch(b.Branch));
}
}


List<BranchCommit> GetMergeCommitsForBranch(Branch branch)
{
if (mergeBaseCommitsCache.ContainsKey(branch))
{
return mergeBaseCommitsCache[branch];
}

var branchMergeBases = repository.Branches.Select(otherBranch =>
{
if (otherBranch.Tip == null)
{
Logger.WriteWarning(string.Format(missingTipFormat, otherBranch.FriendlyName));
return BranchCommit.Empty;
}

var findMergeBase = FindMergeBase(branch, otherBranch, repository);
return new BranchCommit(findMergeBase, otherBranch);
}).Where(b => b.Commit != null).OrderByDescending(b => b.Commit.Committer.When).ToList();
mergeBaseCommitsCache.Add(branch, branchMergeBases);

return branchMergeBases;
}

private class MergeBaseData
{
public Branch Branch { get; private set; }
public Branch OtherBranch { get; private set; }
public IRepository Repository { get; private set; }

public Commit MergeBase { get; private set; }

public MergeBaseData(Branch branch, Branch otherBranch, IRepository repository, Commit mergeBase)
{
Branch = branch;
OtherBranch = otherBranch;
Repository = repository;
MergeBase = mergeBase;
}
}
}
}