diff --git a/LibGit2Sharp.Tests/MergeDriverFixture.cs b/LibGit2Sharp.Tests/MergeDriverFixture.cs new file mode 100644 index 000000000..49e2933ae --- /dev/null +++ b/LibGit2Sharp.Tests/MergeDriverFixture.cs @@ -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(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(() => { 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 applyCallback; + + public FakeMergeDriver(string name, Action initCallback = null, Func 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 }; + } + } + } +} diff --git a/LibGit2Sharp/Core/GitBuf.cs b/LibGit2Sharp/Core/GitBuf.cs index 19b1328b9..0ff58cf2a 100644 --- a/LibGit2Sharp/Core/GitBuf.cs +++ b/LibGit2Sharp/Core/GitBuf.cs @@ -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; + } } diff --git a/LibGit2Sharp/Core/GitErrorCategory.cs b/LibGit2Sharp/Core/GitErrorCategory.cs index 5fc4c7d57..3c02a7ec9 100644 --- a/LibGit2Sharp/Core/GitErrorCategory.cs +++ b/LibGit2Sharp/Core/GitErrorCategory.cs @@ -36,6 +36,7 @@ internal enum GitErrorCategory Filesystem, Patch, Worktree, - Sha1 + Sha1, + MergeDriver } } diff --git a/LibGit2Sharp/Core/GitMergeDriver.cs b/LibGit2Sharp/Core/GitMergeDriver.cs new file mode 100644 index 000000000..22ace9459 --- /dev/null +++ b/LibGit2Sharp/Core/GitMergeDriver.cs @@ -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 + ); + } + + /// + /// The file source being merged + /// + [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; + } +} diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index e8e59843e..b46a481a4 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -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, diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index 18e952e68..f05a2dff5 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -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) diff --git a/LibGit2Sharp/GlobalSettings.cs b/LibGit2Sharp/GlobalSettings.cs index bf7875f96..2d9016f93 100644 --- a/LibGit2Sharp/GlobalSettings.cs +++ b/LibGit2Sharp/GlobalSettings.cs @@ -14,6 +14,7 @@ public static class GlobalSettings { private static readonly Lazy version = new Lazy(Version.Build); private static readonly Dictionary registeredFilters; + private static readonly Dictionary registeredMergeDrivers; private static readonly bool nativeLibraryPathAllowed; private static LogConfiguration logConfiguration = LogConfiguration.None; @@ -38,6 +39,7 @@ static GlobalSettings() #endif registeredFilters = new Dictionary(); + registeredMergeDrivers = new Dictionary(); } #if NETFRAMEWORK @@ -337,6 +339,81 @@ public static void SetConfigSearchPaths(ConfigurationLevel level, params string[ Proxy.git_libgit2_opts_set_search_path(level, pathString); } + /// + /// Takes a snapshot of the currently registered merge drivers. + /// + /// An array of . + public static IEnumerable GetRegisteredMergeDrivers() + { + lock (registeredMergeDrivers) + { + MergeDriverRegistration[] array = new MergeDriverRegistration[registeredMergeDrivers.Count]; + registeredMergeDrivers.Values.CopyTo(array, 0); + return array; + } + } + + /// + /// Registers a to be invoked when matches .gitattributes 'merge=name' + /// + /// The merge driver to be invoked at run time. + /// A object used to manage the lifetime of the registration. + 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; + } + } + + /// + /// Unregisters the associated merge driver. + /// + /// Registration object with an associated merge driver. + 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); + } + } + /// /// Enable or disable strict hash verification. /// diff --git a/LibGit2Sharp/MergeDriver.cs b/LibGit2Sharp/MergeDriver.cs new file mode 100644 index 000000000..528c2f588 --- /dev/null +++ b/LibGit2Sharp/MergeDriver.cs @@ -0,0 +1,273 @@ +using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Handles; +using System; +using System.IO; + +namespace LibGit2Sharp +{ + /// + /// The result produced when applying a merge driver to conflicting commits + /// + public class MergeDriverResult + { + /// + /// The status of what happened as a result of a merge. + /// + public MergeStatus Status; + + /// + /// The resulting stream of data of the merge. + /// This will return null if the merge has been unsuccessful due to non-mergeable conflicts. + /// + public Stream Content; + } + + /// + /// 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. + /// + public abstract class MergeDriver : IEquatable + { + private static readonly LambdaEqualityHelper 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; + + /// + /// Initializes a new instance of the class. + /// And allocates the merge driver natively. + /// The unique name with which this merge driver is registered with + /// + protected MergeDriver(string name) + { + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + + Name = name; + GitMergeDriver = new GitMergeDriver + { + initialize = InitializeCallback, + apply = ApplyMergeCallback + }; + } + + /// + /// Finalizer called by the , deregisters and frees native memory associated with the registered merge driver in libgit2. + /// + ~MergeDriver() + { + GlobalSettings.DeregisterMergeDriver(this); + } + + /// + /// 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). + /// + protected abstract void Initialize(); + + /// + /// 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. + /// + protected abstract MergeDriverResult Apply(MergeDriverSource source); + + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// True if the specified is equal to the current ; otherwise, false. + public override bool Equals(object obj) + { + return Equals(obj as MergeDriver); + } + + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// True if the specified is equal to the current ; otherwise, false. + public bool Equals(MergeDriver other) + { + return equalityHelper.Equals(this, other); + } + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() + { + return equalityHelper.GetHashCode(this); + } + + /// + /// Tests if two are equal. + /// + /// First to compare. + /// Second to compare. + /// True if the two objects are equal; false otherwise. + public static bool operator ==(MergeDriver left, MergeDriver right) + { + return Equals(left, right); + } + + /// + /// Tests if two are different. + /// + /// First to compare. + /// Second to compare. + /// True if the two objects are different; false otherwise. + public static bool operator !=(MergeDriver left, MergeDriver right) + { + return !Equals(left, right); + } + + /// + /// 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). + /// + 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; + } + + /// + /// The marshalled merge driver + /// + internal GitMergeDriver GitMergeDriver { get; private set; } + + /// + /// The name that this merge driver was registered with + /// + public string Name { get; private set; } + } +} diff --git a/LibGit2Sharp/MergeDriverRegistration.cs b/LibGit2Sharp/MergeDriverRegistration.cs new file mode 100644 index 000000000..dab699456 --- /dev/null +++ b/LibGit2Sharp/MergeDriverRegistration.cs @@ -0,0 +1,70 @@ +using System; +using LibGit2Sharp.Core; +using System.Runtime.InteropServices; + +namespace LibGit2Sharp +{ + /// + /// An object representing the registration of a MergeDriver type with libgit2 + /// + public sealed class MergeDriverRegistration + { + /// + /// + /// + /// + 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); + } + /// + /// Finalizer called by the , deregisters and frees native memory associated with the registered merge driver in libgit2. + /// + ~MergeDriverRegistration() + { + // deregister the merge driver + GlobalSettings.DeregisterMergeDriver(this); + // clean up native allocations + Free(); + } + + /// + /// Gets if the registration and underlying merge driver are valid. + /// + public bool IsValid { get { return !freed; } } + /// + /// The registerd merge drivers + /// + public readonly MergeDriver MergeDriver; + /// + /// The name of the driver in the libgit2 registry + /// + 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; + } + } + } +} diff --git a/LibGit2Sharp/MergeDriverSource.cs b/LibGit2Sharp/MergeDriverSource.cs new file mode 100644 index 000000000..835c10f5b --- /dev/null +++ b/LibGit2Sharp/MergeDriverSource.cs @@ -0,0 +1,70 @@ +using System; +using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Handles; + +namespace LibGit2Sharp +{ + /// + /// A merge driver source - describes the repository and file being merged. + /// The MergeDriverSource is what is sent to the Merge Driver callback. + /// + public class MergeDriverSource + { + /// + /// The repository containing the file to be merged + /// + public Repository Repository; + /// + /// The original content + /// + public IndexEntry Ancestor; + /// + /// The content to be merged (based on the Ancestor) + /// + public IndexEntry Ours; + /// + /// The already updated content (based on the Ancestor) + /// + public IndexEntry Theirs; + + /// + /// Needed for mocking purposes + /// + protected MergeDriverSource() { } + + private MergeDriverSource(Repository repos, IndexEntry ancestor, IndexEntry ours, IndexEntry theirs) + { + Repository = repos; + Ancestor = ancestor; + Ours = ours; + Theirs = theirs; + } + + /// + /// Take an unmanaged pointer and convert it to a merge driver source callback paramater + /// + /// + /// + internal static unsafe MergeDriverSource FromNativePtr(IntPtr ptr) + { + return FromNativePtr((git_merge_driver_source*)ptr.ToPointer()); + } + + /// + /// Take an unmanaged pointer and convert it to a merge driver source callback paramater + /// + /// + /// + 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)); + } + } +} diff --git a/LibGit2Sharp/Repository.cs b/LibGit2Sharp/Repository.cs index e23c9cd3b..5aab4a2eb 100644 --- a/LibGit2Sharp/Repository.cs +++ b/LibGit2Sharp/Repository.cs @@ -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; + } + /// /// Initializes a new instance of the class, /// providing optional behavioral overrides through the pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy