Skip to content

Commit f47d3c2

Browse files
committed
Added ObjectDatabase.MergeTrees() to merge trees directly (specifying the ancestor tree to use in the 3-way merge)
1 parent 5085a0c commit f47d3c2

File tree

4 files changed

+233
-0
lines changed

4 files changed

+233
-0
lines changed

LibGit2Sharp.Tests/MergeFixture.cs

+94
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,100 @@ public void CanIgnoreWhitespaceChangeMergeConflict(string branchName)
902902
}
903903
}
904904

905+
[Fact]
906+
public void CanTreeMergeTreeIntoSameTree()
907+
{
908+
string path = SandboxMergeTestRepo();
909+
using (var repo = new Repository(path))
910+
{
911+
var master = repo.Branches["master"].Tip;
912+
913+
var result = repo.ObjectDatabase.MergeTrees(master.Tree, master.Tree, master.Tree, null);
914+
Assert.Equal(MergeTreeStatus.Succeeded, result.Status);
915+
Assert.Empty(result.Conflicts);
916+
}
917+
}
918+
919+
[Fact]
920+
public void CanTreeMergeFastForwardTreeWithoutConflicts()
921+
{
922+
string path = SandboxMergeTestRepo();
923+
using (var repo = new Repository(path))
924+
{
925+
var master = repo.Lookup<Commit>("master");
926+
var branch = repo.Lookup<Commit>("fast_forward");
927+
var ancestor = repo.ObjectDatabase.FindMergeBase(master, branch);
928+
929+
var result = repo.ObjectDatabase.MergeTrees(ancestor.Tree, master.Tree, branch.Tree, null);
930+
Assert.Equal(MergeTreeStatus.Succeeded, result.Status);
931+
Assert.NotNull(result.Tree);
932+
Assert.Empty(result.Conflicts);
933+
}
934+
}
935+
936+
[Fact]
937+
public void CanIdentifyConflictsInMergeTrees()
938+
{
939+
string path = SandboxMergeTestRepo();
940+
using (var repo = new Repository(path))
941+
{
942+
var master = repo.Lookup<Commit>("master");
943+
var branch = repo.Lookup<Commit>("conflicts");
944+
var ancestor = repo.ObjectDatabase.FindMergeBase(master, branch);
945+
946+
var result = repo.ObjectDatabase.MergeTrees(ancestor.Tree, master.Tree, branch.Tree, null);
947+
948+
Assert.Equal(MergeTreeStatus.Conflicts, result.Status);
949+
950+
Assert.Null(result.Tree);
951+
Assert.Single(result.Conflicts);
952+
953+
var conflict = result.Conflicts.First();
954+
Assert.Equal(new ObjectId("8e9daea300fbfef6c0da9744c6214f546d55b279"), conflict.Ancestor.Id);
955+
Assert.Equal(new ObjectId("610b16886ca829cebd2767d9196f3c4378fe60b5"), conflict.Ours.Id);
956+
Assert.Equal(new ObjectId("3dd9738af654bbf1c363f6c3bbc323bacdefa179"), conflict.Theirs.Id);
957+
}
958+
}
959+
960+
[Theory]
961+
[InlineData("conflicts_spaces")]
962+
[InlineData("conflicts_tabs")]
963+
public void CanConflictOnWhitespaceChangeMergeTreesConflict(string branchName)
964+
{
965+
string path = SandboxMergeTestRepo();
966+
using (var repo = new Repository(path))
967+
{
968+
var mergeResult = repo.Merge(branchName, Constants.Signature, new MergeOptions());
969+
Assert.Equal(MergeStatus.Conflicts, mergeResult.Status);
970+
971+
var master = repo.Branches["master"].Tip;
972+
var branch = repo.Branches[branchName].Tip;
973+
var ancestor = repo.ObjectDatabase.FindMergeBase(master, branch);
974+
var mergeTreeResult = repo.ObjectDatabase.MergeTrees(ancestor.Tree, master.Tree, branch.Tree, new MergeTreeOptions());
975+
Assert.Equal(MergeTreeStatus.Conflicts, mergeTreeResult.Status);
976+
}
977+
}
978+
979+
[Theory]
980+
[InlineData("conflicts_spaces")]
981+
[InlineData("conflicts_tabs")]
982+
public void CanIgnoreWhitespaceChangeMergeTreesConflict(string branchName)
983+
{
984+
string path = SandboxMergeTestRepo();
985+
using (var repo = new Repository(path))
986+
{
987+
var mergeResult = repo.Merge(branchName, Constants.Signature, new MergeOptions() { IgnoreWhitespaceChange = true });
988+
Assert.NotEqual(MergeStatus.Conflicts, mergeResult.Status);
989+
990+
var master = repo.Branches["master"].Tip;
991+
var branch = repo.Branches[branchName].Tip;
992+
var ancestor = repo.ObjectDatabase.FindMergeBase(master, branch);
993+
var mergeTreeResult = repo.ObjectDatabase.MergeTrees(ancestor.Tree, master.Tree, branch.Tree, new MergeTreeOptions() { IgnoreWhitespaceChange = true });
994+
Assert.NotEqual(MergeTreeStatus.Conflicts, mergeTreeResult.Status);
995+
Assert.Empty(mergeTreeResult.Conflicts);
996+
}
997+
}
998+
905999
[Fact]
9061000
public void CanMergeIntoIndex()
9071001
{

LibGit2Sharp/Core/NativeMethods.cs

+9
Original file line numberDiff line numberDiff line change
@@ -1987,6 +1987,15 @@ internal static extern int git_transport_smart_credentials(
19871987
internal static extern int git_transport_unregister(
19881988
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string prefix);
19891989

1990+
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
1991+
internal static extern unsafe int git_merge_trees(
1992+
out git_index* outIndex,
1993+
git_repository* repo,
1994+
git_object* ancestorTree,
1995+
git_object* ourTree,
1996+
git_object* theirTree,
1997+
ref GitMergeOpts options);
1998+
19901999
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
19912000
internal static extern unsafe uint git_tree_entry_filemode(git_tree_entry* entry);
19922001

LibGit2Sharp/Core/Proxy.cs

+18
Original file line numberDiff line numberDiff line change
@@ -3249,6 +3249,24 @@ public static int git_transport_smart_credentials(out IntPtr cred, IntPtr transp
32493249

32503250
#region git_tree_
32513251

3252+
public static unsafe IndexHandle git_merge_trees(RepositoryHandle repo, ObjectHandle ancestorTree,
3253+
ObjectHandle ourTree, ObjectHandle theirTree, GitMergeOpts opts, out bool earlyStop)
3254+
{
3255+
git_index* index;
3256+
int res = NativeMethods.git_merge_trees(out index, repo, ancestorTree, ourTree, theirTree, ref opts);
3257+
if (res == (int)GitErrorCode.MergeConflict)
3258+
{
3259+
earlyStop = true;
3260+
}
3261+
else
3262+
{
3263+
earlyStop = false;
3264+
Ensure.ZeroResult(res);
3265+
}
3266+
3267+
return new IndexHandle(index, true);
3268+
}
3269+
32523270
public static unsafe Mode git_tree_entry_attributes(git_tree_entry* entry)
32533271
{
32543272
return (Mode)NativeMethods.git_tree_entry_filemode(entry);

LibGit2Sharp/ObjectDatabase.cs

+112
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,74 @@ public virtual MergeTreeResult MergeCommits(Commit ours, Commit theirs, MergeTre
808808
}
809809
}
810810

811+
/// <summary>
812+
/// Perform a three-way merge of two trees relative to the provided ancestor.
813+
/// The returned <see cref="MergeTreeResult"/> will contain the results
814+
/// of the merge and can be examined for conflicts.
815+
/// </summary>
816+
/// <param name="ancestor">The ancestor tree</param>
817+
/// <param name="ours">The first tree</param>
818+
/// <param name="theirs">The second tree</param>
819+
/// <param name="options">The <see cref="MergeTreeOptions"/> controlling the merge</param>
820+
/// <returns>The <see cref="MergeTreeResult"/> containing the merged trees and any conflicts</returns>
821+
public virtual MergeTreeResult MergeTrees(Tree ancestor, Tree ours, Tree theirs, MergeTreeOptions options)
822+
{
823+
Ensure.ArgumentNotNull(ancestor, "ancestor");
824+
Ensure.ArgumentNotNull(ours, "ours");
825+
Ensure.ArgumentNotNull(theirs, "theirs");
826+
827+
var modifiedOptions = new MergeTreeOptions();
828+
829+
// We throw away the index after looking at the conflicts, so we'll never need the REUC
830+
// entries to be there
831+
modifiedOptions.SkipReuc = true;
832+
833+
if (options != null)
834+
{
835+
modifiedOptions.FailOnConflict = options.FailOnConflict;
836+
modifiedOptions.FindRenames = options.FindRenames;
837+
modifiedOptions.IgnoreWhitespaceChange = options.IgnoreWhitespaceChange;
838+
modifiedOptions.MergeFileFavor = options.MergeFileFavor;
839+
modifiedOptions.RenameThreshold = options.RenameThreshold;
840+
modifiedOptions.TargetLimit = options.TargetLimit;
841+
}
842+
843+
bool earlyStop;
844+
using (var indexHandle = MergeTrees(ancestor, ours, theirs, modifiedOptions, out earlyStop))
845+
{
846+
MergeTreeResult mergeResult;
847+
848+
// Stopped due to FailOnConflict so there's no index or conflict list
849+
if (earlyStop)
850+
{
851+
return new MergeTreeResult(new Conflict[] { });
852+
}
853+
854+
if (Proxy.git_index_has_conflicts(indexHandle))
855+
{
856+
List<Conflict> conflicts = new List<Conflict>();
857+
Conflict conflict;
858+
859+
using (ConflictIteratorHandle iterator = Proxy.git_index_conflict_iterator_new(indexHandle))
860+
{
861+
while ((conflict = Proxy.git_index_conflict_next(iterator)) != null)
862+
{
863+
conflicts.Add(conflict);
864+
}
865+
}
866+
867+
mergeResult = new MergeTreeResult(conflicts);
868+
}
869+
else
870+
{
871+
var treeId = Proxy.git_index_write_tree_to(indexHandle, repo.Handle);
872+
mergeResult = new MergeTreeResult(this.repo.Lookup<Tree>(treeId));
873+
}
874+
875+
return mergeResult;
876+
}
877+
}
878+
811879
/// <summary>
812880
/// Packs all the objects in the <see cref="ObjectDatabase"/> and write a pack (.pack) and index (.idx) files for them.
813881
/// </summary>
@@ -940,6 +1008,50 @@ private IndexHandle MergeCommits(Commit ours, Commit theirs, MergeTreeOptions op
9401008
}
9411009
}
9421010

1011+
/// <summary>
1012+
/// Perform a three-way merge of two trees relative to the provided ancestor.
1013+
/// The returned <see cref="MergeTreeResult"/> will contain the results
1014+
/// of the merge and can be examined for conflicts.
1015+
/// </summary>
1016+
/// <param name="ancestor">The ancestor tree</param>
1017+
/// <param name="ours">The first tree</param>
1018+
/// <param name="theirs">The second tree</param>
1019+
/// <param name="options">The <see cref="MergeTreeOptions"/> controlling the merge</param>
1020+
/// <param name="earlyStop">True if the merge stopped early due to conflicts</param>
1021+
/// <returns>The <see cref="MergeTreeResult"/> containing the merged trees and any conflicts</returns>
1022+
private IndexHandle MergeTrees(Tree ancestor, Tree ours, Tree theirs, MergeTreeOptions options, out bool earlyStop)
1023+
{
1024+
GitMergeFlag mergeFlags = GitMergeFlag.GIT_MERGE_NORMAL;
1025+
if (options.SkipReuc)
1026+
{
1027+
mergeFlags |= GitMergeFlag.GIT_MERGE_SKIP_REUC;
1028+
}
1029+
if (options.FindRenames)
1030+
{
1031+
mergeFlags |= GitMergeFlag.GIT_MERGE_FIND_RENAMES;
1032+
}
1033+
if (options.FailOnConflict)
1034+
{
1035+
mergeFlags |= GitMergeFlag.GIT_MERGE_FAIL_ON_CONFLICT;
1036+
}
1037+
1038+
var mergeOptions = new GitMergeOpts
1039+
{
1040+
Version = 1,
1041+
MergeFileFavorFlags = options.MergeFileFavor,
1042+
MergeTreeFlags = mergeFlags,
1043+
RenameThreshold = (uint)options.RenameThreshold,
1044+
TargetLimit = (uint)options.TargetLimit,
1045+
};
1046+
using (var ancestorHandle = Proxy.git_object_lookup(repo.Handle, ancestor.Id, GitObjectType.Tree))
1047+
using (var oursHandle = Proxy.git_object_lookup(repo.Handle, ours.Id, GitObjectType.Tree))
1048+
using (var theirHandle = Proxy.git_object_lookup(repo.Handle, theirs.Id, GitObjectType.Tree))
1049+
{
1050+
var indexHandle = Proxy.git_merge_trees(repo.Handle, ancestorHandle, oursHandle, theirHandle, mergeOptions, out earlyStop);
1051+
return indexHandle;
1052+
}
1053+
}
1054+
9431055
/// <summary>
9441056
/// Performs a cherry-pick of <paramref name="cherryPickCommit"/> onto <paramref name="cherryPickOnto"/> commit.
9451057
/// </summary>

0 commit comments

Comments
 (0)