diff --git a/src/GitVersionCore/GitRepoMetadataProvider.cs b/src/GitVersionCore/GitRepoMetadataProvider.cs index f9aa811506..80bb0629ee 100644 --- a/src/GitVersionCore/GitRepoMetadataProvider.cs +++ b/src/GitVersionCore/GitRepoMetadataProvider.cs @@ -2,6 +2,7 @@ using LibGit2Sharp; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace GitVersion @@ -9,17 +10,17 @@ namespace GitVersion public class GitRepoMetadataProvider { private Dictionary> mergeBaseCommitsCache; - private Dictionary, MergeBaseData> mergeBaseCache; + private Dictionary mergeBaseCache; private Dictionary> semanticVersionTagsOnBranchCache; private IRepository Repository { get; set; } 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, MergeBaseData>(); + mergeBaseCache = new Dictionary(); mergeBaseCommitsCache = new Dictionary>(); semanticVersionTagsOnBranchCache = new Dictionary>(); - this.Repository = repository; + Repository = repository; } public IEnumerable GetVersionTagsOnBranch(Branch branch, string tagPrefixRegex) @@ -32,9 +33,9 @@ public IEnumerable GetVersionTagsOnBranch(Branch branch, string using (Logger.IndentLog(string.Format("Getting version tags from branch '{0}'.", branch.CanonicalName))) { - var tags = this.Repository.Tags.Select(t => t).ToList(); + var tags = Repository.Tags.Select(t => t).ToList(); - var versionTags = this.Repository.Commits.QueryBy(new CommitFilter + var versionTags = Repository.Commits.QueryBy(new CommitFilter { IncludeReachableFrom = branch.Tip }) @@ -86,7 +87,7 @@ public IEnumerable GetBranchesContainingCommit([NotNull] Commit commit, { Logger.WriteInfo(string.Format("Searching for commits reachable from '{0}'.", branch.FriendlyName)); - var commits = this.Repository.Commits.QueryBy(new CommitFilter + var commits = Repository.Commits.QueryBy(new CommitFilter { IncludeReachableFrom = branch }).Where(c => c.Sha == commit.Sha); @@ -105,17 +106,26 @@ public IEnumerable GetBranchesContainingCommit([NotNull] Commit commit, /// /// Find the merge base of the two branches, i.e. the best common ancestor of the two branches' tips. + /// Note that a local branch and a remote branch tracked by it (or when untracked, has the same name) are considered the same. /// public Commit FindMergeBase(Branch branch, Branch otherBranch) { - var key = Tuple.Create(branch, otherBranch); + if (branch.IsSameBranch(otherBranch)) + { + Logger.WriteDebug(string.Format( + "The branches '{0}' and '{1}' are considered equal.", + branch.FriendlyName, otherBranch.FriendlyName)); + return null; + } - if (mergeBaseCache.ContainsKey(key)) + Commit mergeBase; + var key = new MergeBaseKey(branch, otherBranch); + if (mergeBaseCache.TryGetValue(key, out mergeBase)) { Logger.WriteDebug(string.Format( "Cache hit for merge base between '{0}' and '{1}'.", branch.FriendlyName, otherBranch.FriendlyName)); - return mergeBaseCache[key].MergeBase; + return mergeBase; } using (Logger.IndentLog(string.Format("Finding merge base between '{0}' and '{1}'.", branch.FriendlyName, otherBranch.FriendlyName))) @@ -128,10 +138,10 @@ public Commit FindMergeBase(Branch branch, Branch otherBranch) commitToFindCommonBase = otherBranch.Tip.Parents.First(); } - var findMergeBase = this.Repository.ObjectDatabase.FindMergeBase(commit, commitToFindCommonBase); - if (findMergeBase != null) + mergeBase = Repository.ObjectDatabase.FindMergeBase(commit, commitToFindCommonBase); + if (mergeBase != null) { - Logger.WriteInfo(string.Format("Found merge base of {0}", findMergeBase.Sha)); + Logger.WriteInfo(string.Format("Found merge base of {0}", mergeBase.Sha)); // We do not want to include merge base commits which got forward merged into the other branch Commit mergeBaseAsForwardMerge; do @@ -139,23 +149,23 @@ public Commit FindMergeBase(Branch branch, Branch otherBranch) // Now make sure that the merge base is not a forward merge mergeBaseAsForwardMerge = otherBranch.Commits .SkipWhile(c => c != commitToFindCommonBase) - .TakeWhile(c => c != findMergeBase) - .LastOrDefault(c => c.Parents.Contains(findMergeBase)); + .TakeWhile(c => c != mergeBase) + .LastOrDefault(c => c.Parents.Contains(mergeBase)); if (mergeBaseAsForwardMerge != null) { commitToFindCommonBase = mergeBaseAsForwardMerge.Parents.First(); - findMergeBase = this.Repository.ObjectDatabase.FindMergeBase(commit, commitToFindCommonBase); + mergeBase = this.Repository.ObjectDatabase.FindMergeBase(commit, commitToFindCommonBase); - Logger.WriteInfo(string.Format("Merge base was due to a forward merge, next merge base is {0}", findMergeBase)); + Logger.WriteInfo(string.Format("Merge base was due to a forward merge, next merge base is {0}", mergeBase)); } } while (mergeBaseAsForwardMerge != null); } // Store in cache. - mergeBaseCache.Add(key, new MergeBaseData(branch, otherBranch, this.Repository, findMergeBase)); + mergeBaseCache.Add(key, mergeBase); - return findMergeBase; + return mergeBase; } } @@ -192,7 +202,9 @@ List GetMergeCommitsForBranch(Branch branch) return mergeBaseCommitsCache[branch]; } - var branchMergeBases = Repository.Branches.Select(otherBranch => + // Since local and remote branches are considered equal, make sure that local branches are considered first. + var branchesSortedLocalFirst = Repository.Branches.Where(b => !b.IsRemote).Concat(Repository.Branches.Where(b => b.IsRemote)); + var branchMergeBases = branchesSortedLocalFirst.Select(otherBranch => { if (otherBranch.Tip == null) { @@ -208,20 +220,49 @@ List GetMergeCommitsForBranch(Branch branch) return branchMergeBases; } - private class MergeBaseData + /// + /// The key for the merge base data. + /// Note that the merge base of two branches is symmetric, + /// i.e. the merge base of 'branchA' and 'branchB' is the same as 'branchB' and 'branchA'. + /// + [DebuggerDisplay("A: {BranchA.CanonicalName}; B: {BranchB.CanonicalName}")] + private struct MergeBaseKey : IEquatable { - public Branch Branch { get; private set; } - public Branch OtherBranch { get; private set; } - public IRepository Repository { get; private set; } + private Branch BranchA { get; set; } + private Branch BranchB { get; set; } + + public MergeBaseKey(Branch branchA, Branch branchB) : this() + { + BranchA = branchA; + BranchB = branchB; + } + + public bool Equals(MergeBaseKey other) + { + return (BranchA.IsSameBranch(other.BranchA) && BranchB.IsSameBranch(other.BranchB)) || + (BranchB.IsSameBranch(other.BranchA) && BranchA.IsSameBranch(other.BranchB)); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + return false; + return obj is MergeBaseKey && Equals((MergeBaseKey)obj); + } - public Commit MergeBase { get; private set; } + public override int GetHashCode() + { + return BranchA.GetComparisonBranchName().GetHashCode() ^ BranchB.GetComparisonBranchName().GetHashCode(); + } + + public static bool operator ==(MergeBaseKey left, MergeBaseKey right) + { + return left.Equals(right); + } - public MergeBaseData(Branch branch, Branch otherBranch, IRepository repository, Commit mergeBase) + public static bool operator !=(MergeBaseKey left, MergeBaseKey right) { - Branch = branch; - OtherBranch = otherBranch; - Repository = repository; - MergeBase = mergeBase; + return !left.Equals(right); } } } diff --git a/src/GitVersionCore/LibGitExtensions.cs b/src/GitVersionCore/LibGitExtensions.cs index 3f009112f0..91e8003b46 100644 --- a/src/GitVersionCore/LibGitExtensions.cs +++ b/src/GitVersionCore/LibGitExtensions.cs @@ -16,19 +16,40 @@ public static DateTimeOffset When(this Commit commit) } /// - /// Checks if the two branch objects refer to the same branch (have the same friendly name). + /// Checks if the two branch objects refer to the same branch. + /// If the two branches are the local and (tracking) remote, they are also considered the same. /// public static bool IsSameBranch(this Branch branch, Branch otherBranch) { - // For each branch, fixup the friendly name if the branch is remote. - var otherBranchFriendlyName = otherBranch.IsRemote ? - otherBranch.FriendlyName.Substring(otherBranch.FriendlyName.IndexOf("/", StringComparison.Ordinal) + 1) : - otherBranch.FriendlyName; - var branchFriendlyName = branch.IsRemote ? - branch.FriendlyName.Substring(branch.FriendlyName.IndexOf("/", StringComparison.Ordinal) + 1) : - branch.FriendlyName; - - return otherBranchFriendlyName == branchFriendlyName; + return branch.GetComparisonBranchName() == otherBranch.GetComparisonBranchName(); + } + + /// + /// For comparison, find the "best" branch name, + /// either the name of the remote (tracked) branch, or the local branch name. + /// + public static string GetComparisonBranchName(this Branch branch) + { + // There are several possibilities of the state of a branch: + // 1. local branch, tracks a remote branch with same name + // 2. local branch, tracks a remote branch with *different* name + // 3. local branch, without a remote branch + // 4. remote branch, for which a local tracking branch exists + // 5. remote branch, for which no local tracking branch exists + // branch.UpstreamBranchCanonicalName - Cases 1,2,4,5: 'refs/heads/[remote-branch-name]'; Case 3: null + // branch.FriendlyName - Cases 1-3: '[local-branch-name]'; Cases 4,5: '{branch.RemoteName}/[remote-branch-name]' + // + // We want the the branch name itself, and the remote name should win over the local name, + // since the local and remote version of the branch should be the same. + // Thus, we'll use UpstreamBranchCanonicalName, stripping the /refs/heads/ prefix. + // If that is null, we use FriendlyName instead. + var upstreamName = branch.UpstreamBranchCanonicalName; + if (upstreamName == null) + { + return branch.FriendlyName; + } + + return upstreamName.Substring("refs/heads/".Length); } ///