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

Add support for registering custom merge drivers. #2107

Open
wants to merge 2 commits 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
255 changes: 255 additions & 0 deletions LibGit2Sharp.Tests/MergeDriverFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
using System;
using System.IO;
using System.Linq;
using LibGit2Sharp.Tests.TestHelpers;
using Xunit;

namespace LibGit2Sharp.Tests
{
public class MergeDriverFixture : BaseFixture
{
private const string MergeDriverName = "the-merge-driver";

[Fact]
public void CanRegisterAndUnregisterTheSameMergeDriver()
{
var mergeDriver = new EmptyMergeDriver(MergeDriverName);

var registration = GlobalSettings.RegisterMergeDriver(mergeDriver);
GlobalSettings.DeregisterMergeDriver(registration);

var secondRegistration = GlobalSettings.RegisterMergeDriver(mergeDriver);
GlobalSettings.DeregisterMergeDriver(secondRegistration);
}

[Fact]
public void CanRegisterAndDeregisterAfterGarbageCollection()
{
var registration = GlobalSettings.RegisterMergeDriver(new EmptyMergeDriver(MergeDriverName));

GC.Collect();

GlobalSettings.DeregisterMergeDriver(registration);
}

[Fact]
public void SameMergeDriverIsEqual()
{
var mergeDriver = new EmptyMergeDriver(MergeDriverName);
Assert.Equal(mergeDriver, mergeDriver);
}

[Fact]
public void InitCallbackNotMadeWhenMergeDriverNeverUsed()
{
bool called = false;
void initializeCallback()
{
called = true;
}

var driver = new FakeMergeDriver(MergeDriverName, initializeCallback);
var registration = GlobalSettings.RegisterMergeDriver(driver);

try
{
Assert.False(called);
}
finally
{
GlobalSettings.DeregisterMergeDriver(registration);
}
}

[Fact]
public void WhenMergingApplyIsCalledWhenThereIsAConflict()
{
string repoPath = InitNewRepository();
bool called = false;

MergeDriverResult apply(MergeDriverSource source)
{
called = true;
return new MergeDriverResult { Status = MergeStatus.Conflicts };
}

var mergeDriver = new FakeMergeDriver(MergeDriverName, applyCallback: apply);
var registration = GlobalSettings.RegisterMergeDriver(mergeDriver);

try
{
using (var repo = CreateTestRepository(repoPath))
{
string newFilePath = Touch(repo.Info.WorkingDirectory, Guid.NewGuid() + ".txt", "file1");
var stageNewFile = new FileInfo(newFilePath);
Commands.Stage(repo, newFilePath);
repo.Commit("Commit", Constants.Signature, Constants.Signature);

var branch = repo.CreateBranch("second");

var id = Guid.NewGuid() + ".txt";
newFilePath = Touch(repo.Info.WorkingDirectory, id, "file2");
stageNewFile = new FileInfo(newFilePath);
Commands.Stage(repo, newFilePath);
repo.Commit("Commit in master", Constants.Signature, Constants.Signature);

Commands.Checkout(repo, branch.FriendlyName);

newFilePath = Touch(repo.Info.WorkingDirectory, id, "file3");
stageNewFile = new FileInfo(newFilePath);
Commands.Stage(repo, newFilePath);
repo.Commit("Commit in second branch", Constants.Signature, Constants.Signature);

var result = repo.Merge("master", Constants.Signature, new MergeOptions { CommitOnSuccess = false });

Assert.True(called);
}
}
finally
{
GlobalSettings.DeregisterMergeDriver(registration);
}
}

[Fact]
public void MergeDriverCanFetchFileContents()
{
string repoPath = InitNewRepository();

MergeDriverResult apply(MergeDriverSource source)
{
var repos = source.Repository;
var blob = repos.Lookup<Blob>(source.Theirs.Id);
var content = blob.GetContentStream();
return new MergeDriverResult { Status = MergeStatus.UpToDate, Content = content };
}

var mergeDriver = new FakeMergeDriver(MergeDriverName, applyCallback: apply);
var registration = GlobalSettings.RegisterMergeDriver(mergeDriver);

try
{
using (var repo = CreateTestRepository(repoPath))
{
string newFilePath = Touch(repo.Info.WorkingDirectory, Guid.NewGuid() + ".txt", "file1");
var stageNewFile = new FileInfo(newFilePath);
Commands.Stage(repo, newFilePath);
repo.Commit("Commit", Constants.Signature, Constants.Signature);

var branch = repo.CreateBranch("second");

var id = Guid.NewGuid() + ".txt";
newFilePath = Touch(repo.Info.WorkingDirectory, id, "file2");
stageNewFile = new FileInfo(newFilePath);
Commands.Stage(repo, newFilePath);
repo.Commit("Commit in master", Constants.Signature, Constants.Signature);

Commands.Checkout(repo, branch.FriendlyName);

newFilePath = Touch(repo.Info.WorkingDirectory, id, "file3");
stageNewFile = new FileInfo(newFilePath);
Commands.Stage(repo, newFilePath);
repo.Commit("Commit in second branch", Constants.Signature, Constants.Signature);

var result = repo.Merge("master", Constants.Signature, new MergeOptions { CommitOnSuccess = false });
}
}
finally
{
GlobalSettings.DeregisterMergeDriver(registration);
}
}

[Fact]
public void DoubleRegistrationFailsButDoubleDeregistrationDoesNot()
{
Assert.Empty(GlobalSettings.GetRegisteredMergeDrivers());

var mergeDriver = new EmptyMergeDriver(MergeDriverName);
var registration = GlobalSettings.RegisterMergeDriver(mergeDriver);

Assert.Throws<EntryExistsException>(() => { GlobalSettings.RegisterMergeDriver(mergeDriver); });
Assert.Single(GlobalSettings.GetRegisteredMergeDrivers());

Assert.True(registration.IsValid, "MergeDriverRegistration.IsValid should be true.");

GlobalSettings.DeregisterMergeDriver(registration);
Assert.Empty(GlobalSettings.GetRegisteredMergeDrivers());

Assert.False(registration.IsValid, "MergeDriverRegistration.IsValid should be false.");

GlobalSettings.DeregisterMergeDriver(registration);
Assert.Empty(GlobalSettings.GetRegisteredMergeDrivers());

Assert.False(registration.IsValid, "MergeDriverRegistration.IsValid should be false.");
}

private static FileInfo CommitFileOnBranch(Repository repo, string branchName, String content)
{
var branch = repo.CreateBranch(branchName);
Commands.Checkout(repo, branch.FriendlyName);

FileInfo expectedPath = StageNewFile(repo, content);
repo.Commit("Commit", Constants.Signature, Constants.Signature);
return expectedPath;
}

private static FileInfo StageNewFile(IRepository repo, string contents = "null")
{
string newFilePath = Touch(repo.Info.WorkingDirectory, Guid.NewGuid() + ".txt", contents);
var stageNewFile = new FileInfo(newFilePath);
Commands.Stage(repo, newFilePath);
return stageNewFile;
}

private Repository CreateTestRepository(string path)
{
var repository = new Repository(path);
CreateConfigurationWithDummyUser(repository, Constants.Identity);
CreateAttributesFile(repository, "* merge=the-merge-driver");
return repository;
}

class EmptyMergeDriver : MergeDriver
{
public EmptyMergeDriver(string name)
: base(name)
{ }

protected override MergeDriverResult Apply(MergeDriverSource source)
{
throw new NotImplementedException();
}

protected override void Initialize()
{
throw new NotImplementedException();
}
}

class FakeMergeDriver : MergeDriver
{
private readonly Action initCallback;
private readonly Func<MergeDriverSource, MergeDriverResult> applyCallback;

public FakeMergeDriver(string name, Action initCallback = null, Func<MergeDriverSource, MergeDriverResult> applyCallback = null)
: base(name)
{
this.initCallback = initCallback;
this.applyCallback = applyCallback;
}

protected override void Initialize()
{
initCallback?.Invoke();
}

protected override MergeDriverResult Apply(MergeDriverSource source)
{
if (applyCallback != null)
return applyCallback(source);
return new MergeDriverResult { Status = MergeStatus.UpToDate };
}
}
}
}
8 changes: 8 additions & 0 deletions LibGit2Sharp/Core/GitBuf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@ public void Dispose()
Proxy.git_buf_dispose(this);
}
}

[StructLayout(LayoutKind.Sequential)]
unsafe struct git_buf
{
public IntPtr ptr;
public UIntPtr asize;
public UIntPtr size;
}
}
3 changes: 2 additions & 1 deletion LibGit2Sharp/Core/GitErrorCategory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ internal enum GitErrorCategory
Filesystem,
Patch,
Worktree,
Sha1
Sha1,
MergeDriver
}
}
57 changes: 57 additions & 0 deletions LibGit2Sharp/Core/GitMergeDriver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Runtime.InteropServices;

namespace LibGit2Sharp.Core
{
[StructLayout(LayoutKind.Sequential)]
internal struct GitMergeDriver
{
/** The `version` should be set to `GIT_MERGE_DRIVER_VERSION`. */
public uint version;

/** Called when the merge driver is first used for any file. */
[MarshalAs(UnmanagedType.FunctionPtr)]
public git_merge_driver_init_fn initialize;

/** Called when the merge driver is unregistered from the system. */
[MarshalAs(UnmanagedType.FunctionPtr)]
public git_merge_driver_shutdown_fn shutdown;

/**
* Called to merge the contents of a conflict. If this function
* returns `GIT_PASSTHROUGH` then the default (`text`) merge driver
* will instead be invoked. If this function returns
* `GIT_EMERGECONFLICT` then the file will remain conflicted.
*/
[MarshalAs(UnmanagedType.FunctionPtr)]
public git_merge_driver_apply_fn apply;

internal delegate int git_merge_driver_init_fn(IntPtr merge_driver);
internal delegate void git_merge_driver_shutdown_fn(IntPtr merge_driver);

/** Called when the merge driver is invoked due to a file level merge conflict. */
internal delegate int git_merge_driver_apply_fn(
IntPtr merge_driver,
IntPtr path_out,
UIntPtr mode_out,
IntPtr merged_out,
IntPtr driver_name,
IntPtr merge_driver_source
);
}

/// <summary>
/// The file source being merged
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct git_merge_driver_source
{
public git_repository* repository;
readonly char *default_driver;
readonly IntPtr file_opts;

public git_index_entry* ancestor;
public git_index_entry* ours;
public git_index_entry* theirs;
}
}
13 changes: 13 additions & 0 deletions LibGit2Sharp/Core/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,9 @@ internal static extern unsafe int git_branch_upstream_name(
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
internal static extern void git_buf_dispose(GitBuf buf);

[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
internal static extern int git_buf_grow(IntPtr buf, uint target_size);

[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
internal static extern unsafe int git_checkout_tree(
git_repository* repo,
Expand Down Expand Up @@ -977,6 +980,16 @@ internal static extern unsafe int git_merge(
ref GitMergeOpts merge_opts,
ref GitCheckoutOpts checkout_opts);

[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
internal static extern int git_merge_driver_register(
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name,
IntPtr gitMergeDriver);

[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
internal static extern int git_merge_driver_unregister(
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))]string name);


[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
internal static extern unsafe int git_merge_commits(
out git_index* index,
Expand Down
Loading