diff --git a/LibGit2Sharp.Tests/MergeFixture.cs b/LibGit2Sharp.Tests/MergeFixture.cs index 7ce3ff496..5f3e3e9eb 100644 --- a/LibGit2Sharp.Tests/MergeFixture.cs +++ b/LibGit2Sharp.Tests/MergeFixture.cs @@ -902,6 +902,100 @@ public void CanIgnoreWhitespaceChangeMergeConflict(string branchName) } } + [Fact] + public void CanTreeMergeTreeIntoSameTree() + { + string path = SandboxMergeTestRepo(); + using (var repo = new Repository(path)) + { + var master = repo.Branches["master"].Tip; + + var result = repo.ObjectDatabase.MergeTrees(master.Tree, master.Tree, master.Tree, null); + Assert.Equal(MergeTreeStatus.Succeeded, result.Status); + Assert.Empty(result.Conflicts); + } + } + + [Fact] + public void CanTreeMergeFastForwardTreeWithoutConflicts() + { + string path = SandboxMergeTestRepo(); + using (var repo = new Repository(path)) + { + var master = repo.Lookup("master"); + var branch = repo.Lookup("fast_forward"); + var ancestor = repo.ObjectDatabase.FindMergeBase(master, branch); + + var result = repo.ObjectDatabase.MergeTrees(ancestor.Tree, master.Tree, branch.Tree, null); + Assert.Equal(MergeTreeStatus.Succeeded, result.Status); + Assert.NotNull(result.Tree); + Assert.Empty(result.Conflicts); + } + } + + [Fact] + public void CanIdentifyConflictsInMergeTrees() + { + string path = SandboxMergeTestRepo(); + using (var repo = new Repository(path)) + { + var master = repo.Lookup("master"); + var branch = repo.Lookup("conflicts"); + var ancestor = repo.ObjectDatabase.FindMergeBase(master, branch); + + var result = repo.ObjectDatabase.MergeTrees(ancestor.Tree, master.Tree, branch.Tree, null); + + Assert.Equal(MergeTreeStatus.Conflicts, result.Status); + + Assert.Null(result.Tree); + Assert.Single(result.Conflicts); + + var conflict = result.Conflicts.First(); + Assert.Equal(new ObjectId("8e9daea300fbfef6c0da9744c6214f546d55b279"), conflict.Ancestor.Id); + Assert.Equal(new ObjectId("610b16886ca829cebd2767d9196f3c4378fe60b5"), conflict.Ours.Id); + Assert.Equal(new ObjectId("3dd9738af654bbf1c363f6c3bbc323bacdefa179"), conflict.Theirs.Id); + } + } + + [Theory] + [InlineData("conflicts_spaces")] + [InlineData("conflicts_tabs")] + public void CanConflictOnWhitespaceChangeMergeTreesConflict(string branchName) + { + string path = SandboxMergeTestRepo(); + using (var repo = new Repository(path)) + { + var mergeResult = repo.Merge(branchName, Constants.Signature, new MergeOptions()); + Assert.Equal(MergeStatus.Conflicts, mergeResult.Status); + + var master = repo.Branches["master"].Tip; + var branch = repo.Branches[branchName].Tip; + var ancestor = repo.ObjectDatabase.FindMergeBase(master, branch); + var mergeTreeResult = repo.ObjectDatabase.MergeTrees(ancestor.Tree, master.Tree, branch.Tree, new MergeTreeOptions()); + Assert.Equal(MergeTreeStatus.Conflicts, mergeTreeResult.Status); + } + } + + [Theory] + [InlineData("conflicts_spaces")] + [InlineData("conflicts_tabs")] + public void CanIgnoreWhitespaceChangeMergeTreesConflict(string branchName) + { + string path = SandboxMergeTestRepo(); + using (var repo = new Repository(path)) + { + var mergeResult = repo.Merge(branchName, Constants.Signature, new MergeOptions() { IgnoreWhitespaceChange = true }); + Assert.NotEqual(MergeStatus.Conflicts, mergeResult.Status); + + var master = repo.Branches["master"].Tip; + var branch = repo.Branches[branchName].Tip; + var ancestor = repo.ObjectDatabase.FindMergeBase(master, branch); + var mergeTreeResult = repo.ObjectDatabase.MergeTrees(ancestor.Tree, master.Tree, branch.Tree, new MergeTreeOptions() { IgnoreWhitespaceChange = true }); + Assert.NotEqual(MergeTreeStatus.Conflicts, mergeTreeResult.Status); + Assert.Empty(mergeTreeResult.Conflicts); + } + } + [Fact] public void CanMergeIntoIndex() { diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index e8e59843e..0e8bdc368 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -1987,6 +1987,15 @@ internal static extern int git_transport_smart_credentials( internal static extern int git_transport_unregister( [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string prefix); + [DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)] + internal static extern unsafe int git_merge_trees( + out git_index* outIndex, + git_repository* repo, + git_object* ancestorTree, + git_object* ourTree, + git_object* theirTree, + ref GitMergeOpts options); + [DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)] internal static extern unsafe uint git_tree_entry_filemode(git_tree_entry* entry); diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index 18e952e68..5e0d6db6c 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -3249,6 +3249,24 @@ public static int git_transport_smart_credentials(out IntPtr cred, IntPtr transp #region git_tree_ + public static unsafe IndexHandle git_merge_trees(RepositoryHandle repo, ObjectHandle ancestorTree, + ObjectHandle ourTree, ObjectHandle theirTree, GitMergeOpts opts, out bool earlyStop) + { + git_index* index; + int res = NativeMethods.git_merge_trees(out index, repo, ancestorTree, ourTree, theirTree, ref opts); + if (res == (int)GitErrorCode.MergeConflict) + { + earlyStop = true; + } + else + { + earlyStop = false; + Ensure.ZeroResult(res); + } + + return new IndexHandle(index, true); + } + public static unsafe Mode git_tree_entry_attributes(git_tree_entry* entry) { return (Mode)NativeMethods.git_tree_entry_filemode(entry); diff --git a/LibGit2Sharp/ObjectDatabase.cs b/LibGit2Sharp/ObjectDatabase.cs index 42b65d7d0..26710d023 100644 --- a/LibGit2Sharp/ObjectDatabase.cs +++ b/LibGit2Sharp/ObjectDatabase.cs @@ -808,6 +808,74 @@ public virtual MergeTreeResult MergeCommits(Commit ours, Commit theirs, MergeTre } } + /// + /// Perform a three-way merge of two trees relative to the provided ancestor. + /// The returned will contain the results + /// of the merge and can be examined for conflicts. + /// + /// The ancestor tree + /// The first tree + /// The second tree + /// The controlling the merge + /// The containing the merged trees and any conflicts + public virtual MergeTreeResult MergeTrees(Tree ancestor, Tree ours, Tree theirs, MergeTreeOptions options) + { + Ensure.ArgumentNotNull(ancestor, "ancestor"); + Ensure.ArgumentNotNull(ours, "ours"); + Ensure.ArgumentNotNull(theirs, "theirs"); + + var modifiedOptions = new MergeTreeOptions(); + + // We throw away the index after looking at the conflicts, so we'll never need the REUC + // entries to be there + modifiedOptions.SkipReuc = true; + + if (options != null) + { + modifiedOptions.FailOnConflict = options.FailOnConflict; + modifiedOptions.FindRenames = options.FindRenames; + modifiedOptions.IgnoreWhitespaceChange = options.IgnoreWhitespaceChange; + modifiedOptions.MergeFileFavor = options.MergeFileFavor; + modifiedOptions.RenameThreshold = options.RenameThreshold; + modifiedOptions.TargetLimit = options.TargetLimit; + } + + bool earlyStop; + using (var indexHandle = MergeTrees(ancestor, ours, theirs, modifiedOptions, out earlyStop)) + { + MergeTreeResult mergeResult; + + // Stopped due to FailOnConflict so there's no index or conflict list + if (earlyStop) + { + return new MergeTreeResult(new Conflict[] { }); + } + + if (Proxy.git_index_has_conflicts(indexHandle)) + { + List conflicts = new List(); + Conflict conflict; + + using (ConflictIteratorHandle iterator = Proxy.git_index_conflict_iterator_new(indexHandle)) + { + while ((conflict = Proxy.git_index_conflict_next(iterator)) != null) + { + conflicts.Add(conflict); + } + } + + mergeResult = new MergeTreeResult(conflicts); + } + else + { + var treeId = Proxy.git_index_write_tree_to(indexHandle, repo.Handle); + mergeResult = new MergeTreeResult(this.repo.Lookup(treeId)); + } + + return mergeResult; + } + } + /// /// Packs all the objects in the and write a pack (.pack) and index (.idx) files for them. /// @@ -940,6 +1008,50 @@ private IndexHandle MergeCommits(Commit ours, Commit theirs, MergeTreeOptions op } } + /// + /// Perform a three-way merge of two trees relative to the provided ancestor. + /// The returned will contain the results + /// of the merge and can be examined for conflicts. + /// + /// The ancestor tree + /// The first tree + /// The second tree + /// The controlling the merge + /// True if the merge stopped early due to conflicts + /// The containing the merged trees and any conflicts + private IndexHandle MergeTrees(Tree ancestor, Tree ours, Tree theirs, MergeTreeOptions options, out bool earlyStop) + { + GitMergeFlag mergeFlags = GitMergeFlag.GIT_MERGE_NORMAL; + if (options.SkipReuc) + { + mergeFlags |= GitMergeFlag.GIT_MERGE_SKIP_REUC; + } + if (options.FindRenames) + { + mergeFlags |= GitMergeFlag.GIT_MERGE_FIND_RENAMES; + } + if (options.FailOnConflict) + { + mergeFlags |= GitMergeFlag.GIT_MERGE_FAIL_ON_CONFLICT; + } + + var mergeOptions = new GitMergeOpts + { + Version = 1, + MergeFileFavorFlags = options.MergeFileFavor, + MergeTreeFlags = mergeFlags, + RenameThreshold = (uint)options.RenameThreshold, + TargetLimit = (uint)options.TargetLimit, + }; + using (var ancestorHandle = Proxy.git_object_lookup(repo.Handle, ancestor.Id, GitObjectType.Tree)) + using (var oursHandle = Proxy.git_object_lookup(repo.Handle, ours.Id, GitObjectType.Tree)) + using (var theirHandle = Proxy.git_object_lookup(repo.Handle, theirs.Id, GitObjectType.Tree)) + { + var indexHandle = Proxy.git_merge_trees(repo.Handle, ancestorHandle, oursHandle, theirHandle, mergeOptions, out earlyStop); + return indexHandle; + } + } + /// /// Performs a cherry-pick of onto commit. ///