Skip to content
Open
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
@@ -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
@@ -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
@@ -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,
@@ -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,
24 changes: 24 additions & 0 deletions LibGit2Sharp/Core/Proxy.cs
Original file line number Diff line number Diff line change
@@ -215,6 +215,11 @@ public static void git_buf_dispose(GitBuf buf)
NativeMethods.git_buf_dispose(buf);
}

public static int git_buf_grow(IntPtr buf, uint target_size)
{
return NativeMethods.git_buf_grow(buf, target_size);
}

#endregion

#region git_checkout_
@@ -856,6 +861,25 @@ public static unsafe int git_diff_num_deltas(DiffHandle diff)

#endregion

#region git_merge_driver_
public static void git_merge_driver_register(string name, IntPtr mergeDriverPtr)
{
int res = NativeMethods.git_merge_driver_register(name, mergeDriverPtr);
if (res == (int)GitErrorCode.Exists)
{
throw new EntryExistsException("A merge driver with the name '{0}' is already registered", name);
}
Ensure.ZeroResult(res);
}

public static void git_merge_driver_unregister(string name)
{
int res = NativeMethods.git_merge_driver_unregister(name);
Ensure.ZeroResult(res);
}

#endregion

#region git_error_

public static int git_error_set_str(GitErrorCategory error_class, Exception exception)
77 changes: 77 additions & 0 deletions LibGit2Sharp/GlobalSettings.cs
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ public static class GlobalSettings
{
private static readonly Lazy<Version> version = new Lazy<Version>(Version.Build);
private static readonly Dictionary<Filter, FilterRegistration> registeredFilters;
private static readonly Dictionary<MergeDriver, MergeDriverRegistration> registeredMergeDrivers;
private static readonly bool nativeLibraryPathAllowed;

private static LogConfiguration logConfiguration = LogConfiguration.None;
@@ -38,6 +39,7 @@ static GlobalSettings()
#endif

registeredFilters = new Dictionary<Filter, FilterRegistration>();
registeredMergeDrivers = new Dictionary<MergeDriver, MergeDriverRegistration>();
}

#if NETFRAMEWORK
@@ -337,6 +339,81 @@ public static void SetConfigSearchPaths(ConfigurationLevel level, params string[
Proxy.git_libgit2_opts_set_search_path(level, pathString);
}

/// <summary>
/// Takes a snapshot of the currently registered merge drivers.
/// </summary>
/// <returns>An array of <see cref="MergeDriverRegistration"/>.</returns>
public static IEnumerable<MergeDriverRegistration> GetRegisteredMergeDrivers()
{
lock (registeredMergeDrivers)
{
MergeDriverRegistration[] array = new MergeDriverRegistration[registeredMergeDrivers.Count];
registeredMergeDrivers.Values.CopyTo(array, 0);
return array;
}
}

/// <summary>
/// Registers a <see cref="MergeDriver"/> to be invoked when <see cref="MergeDriver.Name"/> matches .gitattributes 'merge=name'
/// </summary>
/// <param name="mergeDriver">The merge driver to be invoked at run time.</param>
/// <returns>A <see cref="MergeDriverRegistration"/> object used to manage the lifetime of the registration.</returns>
public static MergeDriverRegistration RegisterMergeDriver(MergeDriver mergeDriver)
{
Ensure.ArgumentNotNull(mergeDriver, "merge-driver");

lock (registeredMergeDrivers)
{
// if the merge driver has already been registered
if (registeredMergeDrivers.ContainsKey(mergeDriver))
{
throw new EntryExistsException("The merge driver has already been registered.", GitErrorCode.Exists, GitErrorCategory.MergeDriver);
}

// allocate the registration object
var registration = new MergeDriverRegistration(mergeDriver);
// add the merge driver and registration object to the global tracking list
registeredMergeDrivers.Add(mergeDriver, registration);
return registration;
}
}

/// <summary>
/// Unregisters the associated merge driver.
/// </summary>
/// <param name="registration">Registration object with an associated merge driver.</param>
public static void DeregisterMergeDriver(MergeDriverRegistration registration)
{
Ensure.ArgumentNotNull(registration, "registration");

lock (registeredMergeDrivers)
{
var driver = registration.MergeDriver;

// do nothing if the merge driver isn't registered
if (registeredMergeDrivers.ContainsKey(driver))
{
// remove the register from the global tracking list
registeredMergeDrivers.Remove(driver);
// clean up native allocations
registration.Free();
}
}
}

internal static void DeregisterMergeDriver(MergeDriver driver)
{
System.Diagnostics.Debug.Assert(driver != null);

// do nothing if the merge driver isn't registered
if (registeredMergeDrivers.ContainsKey(driver))
{
var registration = registeredMergeDrivers[driver];
// unregister the merge driver
DeregisterMergeDriver(registration);
}
}

/// <summary>
/// Enable or disable strict hash verification.
/// </summary>
273 changes: 273 additions & 0 deletions LibGit2Sharp/MergeDriver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
using LibGit2Sharp.Core;
using LibGit2Sharp.Core.Handles;
using System;
using System.IO;

namespace LibGit2Sharp
{
/// <summary>
/// The result produced when applying a merge driver to conflicting commits
/// </summary>
public class MergeDriverResult
{
/// <summary>
/// The status of what happened as a result of a merge.
/// </summary>
public MergeStatus Status;

/// <summary>
/// The resulting stream of data of the merge.
/// <para>This will return <code>null</code> if the merge has been unsuccessful due to non-mergeable conflicts.</para>
/// </summary>
public Stream Content;
}

/// <summary>
/// A merge driver can contain logic to do custom merge logic.
/// E.g. proper merging of an html-file requires knowledge of the hierarcical structure of the document.
/// </summary>
public abstract class MergeDriver : IEquatable<MergeDriver>
{
private static readonly LambdaEqualityHelper<MergeDriver> equalityHelper =
new(x => x.Name);
// 64K is optimal buffer size per https://technet.microsoft.com/en-us/library/cc938632.aspx
private const int BufferSize = 64 * 1024;

/// <summary>
/// Initializes a new instance of the <see cref="MergeDriver"/> class.
/// And allocates the merge driver natively.
/// <param name="name">The unique name with which this merge driver is registered with</param>
/// </summary>
protected MergeDriver(string name)
{
Ensure.ArgumentNotNullOrEmptyString(name, "name");

Name = name;
GitMergeDriver = new GitMergeDriver
{
initialize = InitializeCallback,
apply = ApplyMergeCallback
};
}

/// <summary>
/// Finalizer called by the <see cref="GC"/>, deregisters and frees native memory associated with the registered merge driver in libgit2.
/// </summary>
~MergeDriver()
{
GlobalSettings.DeregisterMergeDriver(this);
}

/// <summary>
/// Initialize callback on merge driver
///
/// Specified as `driver.initialize`, this is an optional callback invoked
/// before a merge driver is first used. It will be called once at most
/// per library lifetime.
///
/// If non-NULL, the merge driver's `initialize` callback will be invoked
/// right before the first use of the driver, so you can defer expensive
/// initialization operations (in case libgit2 is being used in a way that
/// doesn't need the merge driver).
/// </summary>
protected abstract void Initialize();

/// <summary>
/// Callback to perform the merge.
///
/// Specified as `driver.apply`, this is the callback that actually does the
/// merge. If it can successfully perform a merge, it should populate
/// `path_out` with a pointer to the filename to accept, `mode_out` with
/// the resultant mode, and `merged_out` with the buffer of the merged file
/// and then return 0. If the driver returns `GIT_PASSTHROUGH`, then the
/// default merge driver should instead be run. It can also return
/// `GIT_EMERGECONFLICT` if the driver is not able to produce a merge result,
/// and the file will remain conflicted. Any other errors will fail and
/// return to the caller.
///
/// The `driver_name` contains the name of the merge driver that was invoked, as
/// specified by the file's attributes.
///
/// The `src` contains the data about the file to be merged.
/// </summary>
protected abstract MergeDriverResult Apply(MergeDriverSource source);

/// <summary>
/// Determines whether the specified <see cref="Object"/> is equal to the current <see cref="MergeDriver"/>.
/// </summary>
/// <param name="obj">The <see cref="Object"/> to compare with the current <see cref="MergeDriver"/>.</param>
/// <returns>True if the specified <see cref="Object"/> is equal to the current <see cref="MergeDriver"/>; otherwise, false.</returns>
public override bool Equals(object obj)
{
return Equals(obj as MergeDriver);
}

/// <summary>
/// Determines whether the specified <see cref="MergeDriver"/> is equal to the current <see cref="MergeDriver"/>.
/// </summary>
/// <param name="other">The <see cref="MergeDriver"/> to compare with the current <see cref="MergeDriver"/>.</param>
/// <returns>True if the specified <see cref="MergeDriver"/> is equal to the current <see cref="MergeDriver"/>; otherwise, false.</returns>
public bool Equals(MergeDriver other)
{
return equalityHelper.Equals(this, other);
}

/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer hash code.</returns>
public override int GetHashCode()
{
return equalityHelper.GetHashCode(this);
}

/// <summary>
/// Tests if two <see cref="MergeDriver"/> are equal.
/// </summary>
/// <param name="left">First <see cref="MergeDriver"/> to compare.</param>
/// <param name="right">Second <see cref="MergeDriver"/> to compare.</param>
/// <returns>True if the two objects are equal; false otherwise.</returns>
public static bool operator ==(MergeDriver left, MergeDriver right)
{
return Equals(left, right);
}

/// <summary>
/// Tests if two <see cref="MergeDriver"/> are different.
/// </summary>
/// <param name="left">First <see cref="MergeDriver"/> to compare.</param>
/// <param name="right">Second <see cref="MergeDriver"/> to compare.</param>
/// <returns>True if the two objects are different; false otherwise.</returns>
public static bool operator !=(MergeDriver left, MergeDriver right)
{
return !Equals(left, right);
}

/// <summary>
/// Initialize callback on merge
///
/// Specified as `driver.initialize`, this is an optional callback invoked
/// before a merge driver is first used. It will be called once at most.
///
/// If non-NULL, the merge driver's `initialize` callback will be invoked right
/// before the first use of the merge driver, so you can defer expensive
/// initialization operations (in case libgit2 is being used in a way that doesn't need the merge driver).
/// </summary>
int InitializeCallback(IntPtr mergeDriverPointer)
{
int result = 0;
try
{
Initialize();
}
catch (Exception exception)
{
Log.Write(LogLevel.Error, "MergeDriver.InitializeCallback exception");
Log.Write(LogLevel.Error, exception.ToString());
Proxy.git_error_set_str(GitErrorCategory.MergeDriver, exception);
result = (int)GitErrorCode.Error;
}
return result;
}

unsafe int ApplyMergeCallback(IntPtr merge_driver, IntPtr path_out, UIntPtr mode_out, IntPtr merged_out, IntPtr driver_name, IntPtr merge_driver_source)
{
MergeDriverSource mergeDriverSource;
try
{
mergeDriverSource = MergeDriverSource.FromNativePtr(merge_driver_source);
var result = Apply(mergeDriverSource);

if (result.Status == MergeStatus.Conflicts)
{
merged_out = IntPtr.Zero;
return (int)GitErrorCode.MergeConflict;
}

var len = result.Content.Length;
Proxy.git_buf_grow(merged_out, (uint)len);
var buffer = (git_buf*)merged_out.ToPointer();
using (var unsafeStream = new UnmanagedMemoryStream((byte*)buffer->ptr.ToPointer(), len, len, FileAccess.Write))
result.Content.CopyTo(unsafeStream);
buffer->size = (UIntPtr)len;

// Decide which source to use for path_out
var driver_source = (git_merge_driver_source*)merge_driver_source.ToPointer();
var ancestorPath = mergeDriverSource.Ancestor?.Path;
var oursPath = mergeDriverSource.Ours?.Path;
var theirsPath = mergeDriverSource.Theirs?.Path;
var best = BestPath(ancestorPath, oursPath, theirsPath);
// Since there is no memory management of the returned character array 'path_out',
// we can only set it to one of the incoming argument strings
if (best == null)
*(char**)path_out.ToPointer() = null;
if (best == ancestorPath)
*(char**)path_out.ToPointer() = driver_source->ancestor->path;
else if (best == oursPath)
*(char**)path_out.ToPointer() = driver_source->ours->path;
else if (best == theirsPath)
*(char**)path_out.ToPointer() = driver_source->theirs->path;

// Decide which source to use for mode_out
var ancestorMode = mergeDriverSource.Ancestor?.Mode ?? Mode.Nonexistent;
var oursMode = mergeDriverSource.Ours?.Mode ?? Mode.Nonexistent;
var theirsMode = mergeDriverSource.Theirs?.Mode ?? Mode.Nonexistent;
*(uint*)mode_out.ToPointer() = (uint)BestMode(ancestorMode, oursMode, theirsMode);

return 0;
}
catch (Exception)
{
merged_out = IntPtr.Zero;
return (int)GitErrorCode.Invalid;
}
}

private static string BestPath(string ancestor, string ours, string theirs)
{
if (ancestor == null)
{
if (ours != null && theirs != null && ours == theirs)
return ours;
return null;
}

if (ours != null && ancestor == ours)
return theirs;
if (theirs != null && ancestor == theirs)
return ours;

return null;
}

private static Mode BestMode(Mode ancestor, Mode ours, Mode theirs)
{
if (ancestor == Mode.Nonexistent)
{
if (ours == Mode.ExecutableFile ||
theirs == Mode.ExecutableFile)
return Mode.ExecutableFile;

return Mode.NonExecutableFile;
}
else if (ours != Mode.Nonexistent && theirs != Mode.Nonexistent)
{
if (ancestor == ours)
return theirs;
return ours;
}

return Mode.Nonexistent;
}

/// <summary>
/// The marshalled merge driver
/// </summary>
internal GitMergeDriver GitMergeDriver { get; private set; }

/// <summary>
/// The name that this merge driver was registered with
/// </summary>
public string Name { get; private set; }
}
}
70 changes: 70 additions & 0 deletions LibGit2Sharp/MergeDriverRegistration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using LibGit2Sharp.Core;
using System.Runtime.InteropServices;

namespace LibGit2Sharp
{
/// <summary>
/// An object representing the registration of a MergeDriver type with libgit2
/// </summary>
public sealed class MergeDriverRegistration
{
/// <summary>
///
/// </summary>
/// <param name="driver"></param>
internal MergeDriverRegistration(MergeDriver driver)
{
System.Diagnostics.Debug.Assert(driver != null);

MergeDriver = driver;

// marshal the git_merge_driver structure into native memory
MergeDriverPointer = Marshal.AllocHGlobal(Marshal.SizeOf(driver.GitMergeDriver));
Marshal.StructureToPtr(driver.GitMergeDriver, MergeDriverPointer, false);

// register the merge driver with the native libary
Proxy.git_merge_driver_register(driver.Name, MergeDriverPointer);
}
/// <summary>
/// Finalizer called by the <see cref="GC"/>, deregisters and frees native memory associated with the registered merge driver in libgit2.
/// </summary>
~MergeDriverRegistration()
{
// deregister the merge driver
GlobalSettings.DeregisterMergeDriver(this);
// clean up native allocations
Free();
}

/// <summary>
/// Gets if the registration and underlying merge driver are valid.
/// </summary>
public bool IsValid { get { return !freed; } }
/// <summary>
/// The registerd merge drivers
/// </summary>
public readonly MergeDriver MergeDriver;
/// <summary>
/// The name of the driver in the libgit2 registry
/// </summary>
public string Name { get { return MergeDriver.Name; } }

private readonly IntPtr MergeDriverPointer;

private bool freed;

internal void Free()
{
if (!freed)
{
// unregister the merge driver with the native libary
Proxy.git_merge_driver_unregister(MergeDriver.Name);
// release native memory
Marshal.FreeHGlobal(MergeDriverPointer);
// remember to not do this twice
freed = true;
}
}
}
}
70 changes: 70 additions & 0 deletions LibGit2Sharp/MergeDriverSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using LibGit2Sharp.Core;
using LibGit2Sharp.Core.Handles;

namespace LibGit2Sharp
{
/// <summary>
/// A merge driver source - describes the repository and file being merged.
/// The MergeDriverSource is what is sent to the Merge Driver callback.
/// </summary>
public class MergeDriverSource
{
/// <summary>
/// The repository containing the file to be merged
/// </summary>
public Repository Repository;
/// <summary>
/// The original content
/// </summary>
public IndexEntry Ancestor;
/// <summary>
/// The content to be merged (based on the Ancestor)
/// </summary>
public IndexEntry Ours;
/// <summary>
/// The already updated content (based on the Ancestor)
/// </summary>
public IndexEntry Theirs;

/// <summary>
/// Needed for mocking purposes
/// </summary>
protected MergeDriverSource() { }

private MergeDriverSource(Repository repos, IndexEntry ancestor, IndexEntry ours, IndexEntry theirs)
{
Repository = repos;
Ancestor = ancestor;
Ours = ours;
Theirs = theirs;
}

/// <summary>
/// Take an unmanaged pointer and convert it to a merge driver source callback paramater
/// </summary>
/// <param name="ptr"></param>
/// <returns></returns>
internal static unsafe MergeDriverSource FromNativePtr(IntPtr ptr)
{
return FromNativePtr((git_merge_driver_source*)ptr.ToPointer());
}

/// <summary>
/// Take an unmanaged pointer and convert it to a merge driver source callback paramater
/// </summary>
/// <param name="ptr"></param>
/// <returns></returns>
internal static unsafe MergeDriverSource FromNativePtr(git_merge_driver_source* ptr)
{
if (ptr == null)
throw new ArgumentNullException(nameof(ptr), "The merge driver source must be defined");

return new MergeDriverSource(
new Repository(new RepositoryHandle(ptr->repository, false)),
IndexEntry.BuildFromPtr(ptr->ancestor),
IndexEntry.BuildFromPtr(ptr->ours),
IndexEntry.BuildFromPtr(ptr->theirs));
}
}
}
6 changes: 6 additions & 0 deletions LibGit2Sharp/Repository.cs
Original file line number Diff line number Diff line change
@@ -70,6 +70,12 @@ public Repository(string path)
: this(path, null, RepositoryRequiredParameter.Path)
{ }

internal unsafe Repository(RepositoryHandle repoHandle)
: this(null, null, RepositoryRequiredParameter.None)
{
handle = repoHandle;
}

/// <summary>
/// Initializes a new instance of the <see cref="Repository"/> class,
/// providing optional behavioral overrides through the