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

Added ObjectDatabase.MergeTrees() to merge trees directly #2097

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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