Skip to content

Introduce basic merging #579

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

Closed
wants to merge 5 commits into from
Closed
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
188 changes: 188 additions & 0 deletions LibGit2Sharp.Tests/MergeFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,193 @@ public void CanRetrieveTheBranchBeingMerged()
Assert.Null(mergedHeads[1].Tip);
}
}

[Fact]
public void CanMergeRepos()
{
string firstBranchFileName = "first branch file.txt";
string secondBranchFileName = "second branch file.txt";
string sharedBranchFileName = "first+second branch file.txt";

string path = CloneStandardTestRepo();
using (var repo = new Repository(path))
{
var firstBranch = repo.CreateBranch("FirstBranch");
firstBranch.Checkout();
var originalTreeCount = firstBranch.Tip.Tree.Count;

// Commit with ONE new file to both first & second branch (SecondBranch is created on this commit).
AddFileCommitToRepo(repo, sharedBranchFileName);

var secondBranch = repo.CreateBranch("SecondBranch");
// Commit with ONE new file to first branch (FirstBranch moves forward as it is checked out, SecondBranch stays back one).
var firstBranchCommit = AddFileCommitToRepo(repo, firstBranchFileName);

secondBranch.Checkout();
// Commit with ONE new file to second branch (FirstBranch and SecondBranch now point to separate commits that both have the same parent commit).
var secondBranchCommit = AddFileCommitToRepo(repo, secondBranchFileName);

MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we assert anything regarding the state of MergeResult here?


Assert.False(mergeResult.IsUpToDate);
Assert.False(mergeResult.IsFastForward);

var mergeCommit = repo.Commit("Merge First+Second", Constants.Signature, Constants.Signature);

Assert.Equal(mergeCommit.Tree.Count, originalTreeCount + 3); // Expecting original tree count plussed by the 3 added files.
Assert.Equal(mergeCommit.Parents.Count(), 2); // Merge commit should have 2 parents
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expected value should come first in the Assert.Equal methods, with the actual value as the second parameter.

}
}

[Fact]
public void IsUpToDateMerge()
{
string sharedBranchFileName = "first+second branch file.txt";

string path = CloneStandardTestRepo();
using (var repo = new Repository(path))
{
var firstBranch = repo.CreateBranch("FirstBranch");
firstBranch.Checkout();
var originalTreeCount = firstBranch.Tip.Tree.Count;

// Commit with ONE new file to both first & second branch (SecondBranch is created on this commit).
AddFileCommitToRepo(repo, sharedBranchFileName);

var secondBranch = repo.CreateBranch("SecondBranch");

secondBranch.Checkout();

MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip);

Assert.True(mergeResult.IsUpToDate);
Assert.False(mergeResult.IsFastForward);
}
}

[Fact]
public void CanFastForwardRepos()
{
string firstBranchFileName = "first branch file.txt";
string sharedBranchFileName = "first+second branch file.txt";

string path = CloneStandardTestRepo();
using (var repo = new Repository(path))
{
var firstBranch = repo.CreateBranch("FirstBranch");
firstBranch.Checkout();
var originalTreeCount = firstBranch.Tip.Tree.Count;

// Commit with ONE new file to both first & second branch (SecondBranch is created on this commit).
AddFileCommitToRepo(repo, sharedBranchFileName);

var secondBranch = repo.CreateBranch("SecondBranch");
// Commit with ONE new file to first branch (FirstBranch moves forward as it is checked out, SecondBranch stays back one).
var firstBranchCommit = AddFileCommitToRepo(repo, firstBranchFileName);

secondBranch.Checkout();

MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip);

Assert.False(mergeResult.IsUpToDate);
Assert.True(mergeResult.IsFastForward);

var mergeCommit = repo.Commit("Merge First+Second", Constants.Signature, Constants.Signature);

Assert.Equal(mergeCommit.Tree.Count, originalTreeCount + 2); // Expecting original tree count plussed by the 3 added files.
}
}

[Fact]
public void ConflictingMergeRepos()
{
string firstBranchFileName = "first branch file.txt";
string secondBranchFileName = "second branch file.txt";
string sharedBranchFileName = "first+second branch file.txt";

string path = CloneStandardTestRepo();
using (var repo = new Repository(path))
{
var firstBranch = repo.CreateBranch("FirstBranch");
firstBranch.Checkout();
var originalTreeCount = firstBranch.Tip.Tree.Count;

// Commit with ONE new file to both first & second branch (SecondBranch is created on this commit).
AddFileCommitToRepo(repo, sharedBranchFileName);

var secondBranch = repo.CreateBranch("SecondBranch");
// Commit with ONE new file to first branch (FirstBranch moves forward as it is checked out, SecondBranch stays back one).
AddFileCommitToRepo(repo, firstBranchFileName);
AddFileCommitToRepo(repo, sharedBranchFileName, "The first branches comment"); // Change file in first branch

secondBranch.Checkout();
// Commit with ONE new file to second branch (FirstBranch and SecondBranch now point to separate commits that both have the same parent commit).
AddFileCommitToRepo(repo, secondBranchFileName);
AddFileCommitToRepo(repo, sharedBranchFileName, "The second branches comment"); // Change file in second branch

MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip);

Assert.False(mergeResult.IsUpToDate);
Assert.False(mergeResult.IsFastForward);

Assert.Equal(repo.Index.Conflicts.Count(), 1);

var conflict = repo.Index.Conflicts.First();
var changes = repo.Diff.Compare(repo.Lookup<Blob>(conflict.Theirs.Id), repo.Lookup<Blob>(conflict.Ours.Id));

Assert.False(changes.IsBinaryComparison);
}
}

[Fact]
public void ConflictingMergeReposBinary()
{
string firstBranchFileName = "first branch file.bin";
string secondBranchFileName = "second branch file.bin";
string sharedBranchFileName = "first+second branch file.bin";

string path = CloneStandardTestRepo();
using (var repo = new Repository(path))
{
var firstBranch = repo.CreateBranch("FirstBranch");
firstBranch.Checkout();
var originalTreeCount = firstBranch.Tip.Tree.Count;

// Commit with ONE new file to both first & second branch (SecondBranch is created on this commit).
AddFileCommitToRepo(repo, sharedBranchFileName);

var secondBranch = repo.CreateBranch("SecondBranch");
// Commit with ONE new file to first branch (FirstBranch moves forward as it is checked out, SecondBranch stays back one).
AddFileCommitToRepo(repo, firstBranchFileName);
AddFileCommitToRepo(repo, sharedBranchFileName, "\0The first branches comment\0"); // Change file in first branch

secondBranch.Checkout();
// Commit with ONE new file to second branch (FirstBranch and SecondBranch now point to separate commits that both have the same parent commit).
AddFileCommitToRepo(repo, secondBranchFileName);
AddFileCommitToRepo(repo, sharedBranchFileName, "\0The second branches comment\0"); // Change file in second branch

MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip);

Assert.False(mergeResult.IsUpToDate);
Assert.False(mergeResult.IsFastForward);

Assert.Equal(repo.Index.Conflicts.Count(), 1);

Conflict conflict = repo.Index.Conflicts.First();

var changes = repo.Diff.Compare(repo.Lookup<Blob>(conflict.Theirs.Id), repo.Lookup<Blob>(conflict.Ours.Id));

Assert.True(changes.IsBinaryComparison);
}
}

private Commit AddFileCommitToRepo(IRepository repository, string filename, string content = null)
{
Touch(repository.Info.WorkingDirectory, filename, content);

repository.Index.Stage(filename);

return repository.Commit("New commit", Constants.Signature, Constants.Signature);
}
}
}
22 changes: 22 additions & 0 deletions LibGit2Sharp/Core/GitMergeOpts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Runtime.InteropServices;

namespace LibGit2Sharp.Core
{
[Flags]
internal enum MergeFlags
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ethomson Can one combine those flags?

IOW, would GIT_MERGE_NO_FASTFORWARD | GIT_MERGE_FASTFORWARD_ONLY make sense?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One cannot, it definitely doesn't make sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But(!) my thinking is that this is a bitfield and we may have other options that could be combined later. You can't combine GIT_MERGE_NO_FASTFORWARD with GIT_MERGE_FASTFORWARD_ONLY but maybe you could combine it with something else like GIT_MERGE_DONT_WRITE_CRAPPY_METADATA_FILES or something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ethomson - This is not currently represented as a bit field in libgit2. If that is the intention of this type, should we update the libgit2 representation?

In addition, should there be a named default value (both here and in libgit2) - GIT_MERGE_DEFAULT = 0?

{
GIT_MERGE_NO_FASTFORWARD = 1,
GIT_MERGE_FASTFORWARD_ONLY = 2,
}

[StructLayout(LayoutKind.Sequential)]
internal struct GitMergeOpts
{
public uint Version;

public MergeFlags MergeFlags;
public GitMergeTreeOpts MergeTreeOpts;
public GitCheckoutOpts CheckoutOpts;
}
}
33 changes: 33 additions & 0 deletions LibGit2Sharp/Core/GitMergeTreeOpts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Runtime.InteropServices;

namespace LibGit2Sharp.Core
{
[Flags]
internal enum MergeTreeFlags
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Most of the enums are of the form Git* (i.e. GitMergeTreeFlags) (although there are a couple of exceptions to this pattern).

{
GIT_MERGE_TREE_FIND_RENAMES = (1 << 0),
}

internal enum MergeAutomergeFlags
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, are those exclusive options?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are indeed exclusive to one another.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is not an actual flag type, maybe we should suffix with Strategy - (e.g. maybe GitMergeAutomergeStrategy)

{
GIT_MERGE_AUTOMERGE_NORMAL = 0,
GIT_MERGE_AUTOMERGE_NONE = 1,
GIT_MERGE_AUTOMERGE_FAVOR_OURS = 2,
GIT_MERGE_AUTOMERGE_FAVOR_THEIRS = 3,
}

[StructLayout(LayoutKind.Sequential)]
internal struct GitMergeTreeOpts
{
public uint Version;

public MergeTreeFlags MergeTreeFlags;
public uint RenameThreshold;
public uint TargetLimit;

public UIntPtr Metric;

public MergeAutomergeFlags MergeAutomergeFlags;
}
}
13 changes: 13 additions & 0 deletions LibGit2Sharp/Core/Handles/GitMergeHeadHandle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Runtime.InteropServices;

namespace LibGit2Sharp.Core.Handles
{
internal class GitMergeHeadHandle : SafeHandleBase
{
protected override bool ReleaseHandleImpl()
{
Proxy.git_merge_head_free(handle);
return true;
}
}
}
13 changes: 13 additions & 0 deletions LibGit2Sharp/Core/Handles/GitMergeResultHandle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Runtime.InteropServices;

namespace LibGit2Sharp.Core.Handles
{
internal class GitMergeResultHandle : SafeHandleBase
{
protected override bool ReleaseHandleImpl()
{
Proxy.git_merge_result_free(handle);
return true;
}
}
}
49 changes: 49 additions & 0 deletions LibGit2Sharp/Core/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,55 @@ internal static extern int git_merge_base(
GitObjectSafeHandle one,
GitObjectSafeHandle two);

[DllImport(libgit2)]
internal static extern int git_merge_head_from_ref(
out GitMergeHeadHandle mergehead,
RepositorySafeHandle repo,
ReferenceSafeHandle reference);

[DllImport(libgit2)]
internal static extern int git_merge_head_from_fetchhead(
out GitMergeHeadHandle mergehead,
RepositorySafeHandle repo,
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string branch_name,
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string remote_url,
ref GitOid oid);

[DllImport(libgit2)]
internal static extern int git_merge_head_from_oid(
out GitMergeHeadHandle mergehead,
RepositorySafeHandle repo,
ref GitOid oid);

[DllImport(libgit2)]
internal static extern int git_merge(
out GitMergeResultHandle mergeResult,
RepositorySafeHandle repo,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] [In] IntPtr[] their_heads,
UIntPtr their_heads_len,
ref GitMergeOpts given_opts);

[DllImport(libgit2)]
internal static extern int git_merge_result_is_uptodate(
GitMergeResultHandle merge_result);

[DllImport(libgit2)]
internal static extern int git_merge_result_is_fastforward(
GitMergeResultHandle merge_result);

[DllImport(libgit2)]
internal static extern int git_merge_result_fastforward_oid(
out GitOid oid,
GitMergeResultHandle merge_result);

[DllImport(libgit2)]
internal static extern void git_merge_result_free(
IntPtr merge_result);

[DllImport(libgit2)]
internal static extern void git_merge_head_free(
IntPtr merge_head);

[DllImport(libgit2)]
internal static extern int git_message_prettify(
byte[] message_out, // NB: This is more properly a StringBuilder, but it's UTF8
Expand Down
Loading