Skip to content

Commit

Permalink
Added ObjectDatabase.MergeTrees() to merge trees directly (specifying…
Browse files Browse the repository at this point in the history
… the ancestor tree to use in the 3-way merge)
  • Loading branch information
frindler committed May 25, 2024
1 parent 5085a0c commit f47d3c2
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 0 deletions.
94 changes: 94 additions & 0 deletions LibGit2Sharp.Tests/MergeFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Commit>("master");
var branch = repo.Lookup<Commit>("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<Commit>("master");
var branch = repo.Lookup<Commit>("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()
{
Expand Down
9 changes: 9 additions & 0 deletions LibGit2Sharp/Core/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
18 changes: 18 additions & 0 deletions LibGit2Sharp/Core/Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
112 changes: 112 additions & 0 deletions LibGit2Sharp/ObjectDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,74 @@ public virtual MergeTreeResult MergeCommits(Commit ours, Commit theirs, MergeTre
}
}

/// <summary>
/// Perform a three-way merge of two trees relative to the provided ancestor.
/// The returned <see cref="MergeTreeResult"/> will contain the results
/// of the merge and can be examined for conflicts.
/// </summary>
/// <param name="ancestor">The ancestor tree</param>
/// <param name="ours">The first tree</param>
/// <param name="theirs">The second tree</param>
/// <param name="options">The <see cref="MergeTreeOptions"/> controlling the merge</param>
/// <returns>The <see cref="MergeTreeResult"/> containing the merged trees and any conflicts</returns>
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<Conflict> conflicts = new List<Conflict>();
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<Tree>(treeId));
}

return mergeResult;
}
}

/// <summary>
/// Packs all the objects in the <see cref="ObjectDatabase"/> and write a pack (.pack) and index (.idx) files for them.
/// </summary>
Expand Down Expand Up @@ -940,6 +1008,50 @@ private IndexHandle MergeCommits(Commit ours, Commit theirs, MergeTreeOptions op
}
}

/// <summary>
/// Perform a three-way merge of two trees relative to the provided ancestor.
/// The returned <see cref="MergeTreeResult"/> will contain the results
/// of the merge and can be examined for conflicts.
/// </summary>
/// <param name="ancestor">The ancestor tree</param>
/// <param name="ours">The first tree</param>
/// <param name="theirs">The second tree</param>
/// <param name="options">The <see cref="MergeTreeOptions"/> controlling the merge</param>
/// <param name="earlyStop">True if the merge stopped early due to conflicts</param>
/// <returns>The <see cref="MergeTreeResult"/> containing the merged trees and any conflicts</returns>
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;
}
}

/// <summary>
/// Performs a cherry-pick of <paramref name="cherryPickCommit"/> onto <paramref name="cherryPickOnto"/> commit.
/// </summary>
Expand Down

0 comments on commit f47d3c2

Please sign in to comment.