diff --git a/LibGit2Sharp.Tests/ConfigBackendFixture.cs b/LibGit2Sharp.Tests/ConfigBackendFixture.cs new file mode 100644 index 000000000..90e62c4b0 --- /dev/null +++ b/LibGit2Sharp.Tests/ConfigBackendFixture.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; + +namespace LibGit2Sharp.Tests +{ + public class ConfigBackendFixture : BaseFixture + { + [Fact] + public void CanGeneratePredictableObjectShasWithAProvidedBackend() + { + string repoPath = InitNewRepository(); + + using (var repo = new Repository(repoPath)) + { + repo.Config.AddBackend(new MockConfigBackend(), ConfigurationLevel.Local, true); + + var str = repo.Config.Get("test.value"); + } + } + + [Fact] + public void ADisposableOdbBackendGetsDisposedUponRepositoryDisposal() + { + string path = InitNewRepository(); + + int numDisposeCalls = 0; + + using (var repo = new Repository(path)) + { + var mockConfigBackend = new MockConfigBackend(() => { numDisposeCalls++; }); + + Assert.IsAssignableFrom(mockConfigBackend); + + repo.Config.AddBackend(mockConfigBackend, ConfigurationLevel.Local, false); + + Assert.Equal(0, numDisposeCalls); + } + + Assert.Equal(1, numDisposeCalls); + } + + #region MockConfigBackend + + private class MockConfigBackend : ConfigBackend, IDisposable + { + public MockConfigBackend(Action disposer = null) + { + this.disposer = disposer; + } + + public void Dispose() + { + if (disposer == null) + { + return; + } + + disposer(); + + disposer = null; + } + + public override int Open(ConfigurationLevel level) + { + return 0; + } + + public override int Get(string name, out ConfigurationEntry entry) + { + entry = new ConfigurationEntry(name, "valuehere", ConfigurationLevel.Local); + return 0; + } + + public override int Set(string name, string value) + { + throw new NotImplementedException(); + } + + public override int Snapshot(out ConfigBackend snapshot) + { + snapshot = this; + return 0; + } + + private Action disposer; + } + + #endregion + } +} diff --git a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj index 0fadfeb04..76d74c273 100644 --- a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj +++ b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj @@ -64,6 +64,7 @@ + diff --git a/LibGit2Sharp/ConfigBackend.cs b/LibGit2Sharp/ConfigBackend.cs new file mode 100644 index 000000000..a14e1f384 --- /dev/null +++ b/LibGit2Sharp/ConfigBackend.cs @@ -0,0 +1,292 @@ +using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Handles; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace LibGit2Sharp +{ + [StructLayout(LayoutKind.Sequential)] + internal struct GitConfigBackend + { + static GitConfigBackend() + { + GCHandleOffset = Marshal.OffsetOf(typeof(GitConfigBackend), "GCHandle").ToInt32(); + } + + public uint Version; + +#pragma warning disable 169 + + /// + /// This field is populated by libgit2 at backend addition time, and exists for its + /// use only. From this side of the interop, it is unreferenced. + /// + public int Readonly; + + /// + /// This field is populated by libgit2 at backend addition time, and exists for its + /// use only. From this side of the interop, it is unreferenced. + /// + private IntPtr Cfg; + +#pragma warning restore 169 + + public open_callback Open; + public get_callback Get; + public set_callback Set; + public set_multivar_callback SetMultivar; + public del_callback Del; + public del_multivar_callback DelMultivar; + public iterator_callback Iterator; + public snapshot_callback Snapshot; + public lock_callback Lock; + public unlock_callback Unlock; + public free_callback Free; + + /* The libgit2 structure definition ends here. Subsequent fields are for libgit2sharp bookkeeping. */ + + public IntPtr GCHandle; + + /* The following static fields are not part of the structure definition. */ + + public static int GCHandleOffset; + + public delegate int open_callback(IntPtr backend, ConfigurationLevel level); + + public delegate int get_callback(IntPtr backend, IntPtr name, out IntPtr entry_out); + + public delegate int set_callback(IntPtr backend, IntPtr name, IntPtr value); + + public delegate int set_multivar_callback(IntPtr backend, IntPtr name, IntPtr regexp, IntPtr value); + + public delegate int del_callback(IntPtr backend, IntPtr name); + + public delegate int del_multivar_callback(IntPtr backend, IntPtr name, IntPtr regexp); + + public delegate int iterator_callback(out IntPtr iterator_out, IntPtr backend); + + public delegate int snapshot_callback(out IntPtr backend_out, IntPtr backend); + + public delegate int lock_callback(IntPtr backend); + + public delegate int unlock_callback(IntPtr backend, int success); + + public delegate void free_callback(IntPtr backend); + } + + /// + /// Base class for all custom managed backends for libgit2 configs. + /// + /// TODO: + /// If the derived backend implements , the + /// method will be honored and invoked upon the disposal of the repository. + /// + /// + public abstract class ConfigBackend + { + /// + /// Invoked by libgit2 when this backend is no longer needed. + /// + internal void Free() + { + if (nativeBackendPointer == IntPtr.Zero) + { + return; + } + + GCHandle.FromIntPtr(Marshal.ReadIntPtr(nativeBackendPointer, GitConfigBackend.GCHandleOffset)).Free(); + Marshal.FreeHGlobal(nativeBackendPointer); + nativeBackendPointer = IntPtr.Zero; + } + + public abstract int Open(ConfigurationLevel level); + public abstract int Get(string name, out ConfigurationEntry entry); + public abstract int Set(string name, string value); + public abstract int Snapshot(out ConfigBackend snapshot); + + private IntPtr nativeBackendPointer; + + internal IntPtr ConfigBackendPointer + { + get + { + if (IntPtr.Zero == nativeBackendPointer) + { + var nativeBackend = new GitConfigBackend(); + nativeBackend.Version = 1; + + // The "free" entry point is always provided. + nativeBackend.Free = BackendEntryPoints.FreeCallback; + nativeBackend.Open = BackendEntryPoints.OpenCallback; + nativeBackend.Get = BackendEntryPoints.GetCallback; + nativeBackend.Set = BackendEntryPoints.SetCallback; + nativeBackend.Snapshot = BackendEntryPoints.SnapshotCallback; + + nativeBackend.GCHandle = GCHandle.ToIntPtr(GCHandle.Alloc(this)); + nativeBackendPointer = Marshal.AllocHGlobal(Marshal.SizeOf(nativeBackend)); + Marshal.StructureToPtr(nativeBackend, nativeBackendPointer, false); + } + + return nativeBackendPointer; + } + } + + private static class BackendEntryPoints + { + // Because our GitConfigBackend structure exists on the managed heap only for a short time (to be marshaled + // to native memory with StructureToPtr), we need to bind to static delegates. If at construction time + // we were to bind to the methods directly, that's the same as newing up a fresh delegate every time. + // Those delegates won't be rooted in the object graph and can be collected as soon as StructureToPtr finishes. + public static readonly GitConfigBackend.open_callback OpenCallback = Open; + public static readonly GitConfigBackend.get_callback GetCallback = Get; + public static readonly GitConfigBackend.set_callback SetCallback = Set; + //public static readonly GitConfigBackend.set_multivar_callback SetMultivarCallback = SetMultivar; + //public static readonly GitConfigBackend.del_callback DelCallback = Del; + //public static readonly GitConfigBackend.del_multivar_callback DelMultivarCallback = DelMultivar; + //public static readonly GitConfigBackend.iterator_callback IteratorCallback = Iterator; + public static readonly GitConfigBackend.snapshot_callback SnapshotCallback = Snapshot; + public static readonly GitConfigBackend.free_callback FreeCallback = Free; + + private static ConfigBackend MarshalConfigBackend(IntPtr backendPtr) + { + var intPtr = Marshal.ReadIntPtr(backendPtr, GitConfigBackend.GCHandleOffset); + var configBackend = GCHandle.FromIntPtr(intPtr).Target as ConfigBackend; + + if (configBackend == null) + { + Proxy.giterr_set_str(GitErrorCategory.Reference, "Cannot retrieve the managed ConfigBackend."); + return null; + } + + return configBackend; + } + + private unsafe static int Open(IntPtr backend, ConfigurationLevel level) + { + var configBackend = MarshalConfigBackend(backend); + if (configBackend == null) + { + return (int)GitErrorCode.Error; + } + + try + { + return configBackend.Open(level); + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Config, ex); + return (int)GitErrorCode.Error; + } + } + + private unsafe static int Get(IntPtr backend, IntPtr name, out IntPtr entry_out) + { + entry_out = IntPtr.Zero; + + var configBackend = MarshalConfigBackend(backend); + if (configBackend == null) + { + return (int)GitErrorCode.Error; + } + + try + { + var str = LaxUtf8Marshaler.FromNative(name); + ConfigurationEntry entry; + int toReturn = configBackend.Get(str, out entry); + + //nativeBackend.GCHandle = GCHandle.ToIntPtr(GCHandle.Alloc(this)); + var ptr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ConfigurationEntry))); + Marshal.StructureToPtr(entry, ptr, true); + + return toReturn; + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Config, ex); + return (int)GitErrorCode.Error; + } + } + + private unsafe static int Set(IntPtr backend, IntPtr name, IntPtr value) + { + var configBackend = MarshalConfigBackend(backend); + if (configBackend == null) + { + return (int)GitErrorCode.Error; + } + + try + { + string nameStr = LaxUtf8Marshaler.FromNative(name); + string valueStr = LaxUtf8Marshaler.FromNative(value); + + return configBackend.Set(nameStr, valueStr); + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Config, ex); + return (int)GitErrorCode.Error; + } + } + + private unsafe static int Snapshot(out IntPtr backend_out, IntPtr backend) + { + backend_out = IntPtr.Zero; + + var configBackend = MarshalConfigBackend(backend); + if (configBackend == null) + { + return (int)GitErrorCode.Error; + } + + try + { + ConfigBackend snapshot; + int toReturn = configBackend.Snapshot(out snapshot); + backend_out = snapshot.ConfigBackendPointer; + + return toReturn; + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Config, ex); + return (int)GitErrorCode.Error; + } + } + + private static void Free(IntPtr backend) + { + var configBackend = MarshalConfigBackend(backend); + if (configBackend == null) + { + return; + } + + try + { + configBackend.Free(); + + var disposable = configBackend as IDisposable; + + if (disposable == null) + { + return; + } + + disposable.Dispose(); + } + catch (Exception ex) + { + Proxy.giterr_set_str(GitErrorCategory.Config, ex); + } + } + + } + } +} diff --git a/LibGit2Sharp/Configuration.cs b/LibGit2Sharp/Configuration.cs index 3cce3610d..970509709 100644 --- a/LibGit2Sharp/Configuration.cs +++ b/LibGit2Sharp/Configuration.cs @@ -784,5 +784,12 @@ private ConfigurationSafeHandle Snapshot() { return Proxy.git_config_snapshot(configHandle); } + + public virtual void AddBackend(ConfigBackend backend, ConfigurationLevel level, bool force) + { + Ensure.ArgumentNotNull(backend, "backend"); + + Proxy.git_config_add_backend(configHandle, backend.ConfigBackendPointer, level, force); + } } } diff --git a/LibGit2Sharp/ConfigurationEntry.cs b/LibGit2Sharp/ConfigurationEntry.cs index 13c153a2a..dd6730f25 100644 --- a/LibGit2Sharp/ConfigurationEntry.cs +++ b/LibGit2Sharp/ConfigurationEntry.cs @@ -37,7 +37,7 @@ protected ConfigurationEntry() /// The option name /// The option value /// The origin store - protected internal ConfigurationEntry(string key, T value, ConfigurationLevel level) + public ConfigurationEntry(string key, T value, ConfigurationLevel level) { Key = key; Value = value; diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index a1b5b5cf1..604e37549 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -438,6 +438,13 @@ internal static extern int git_config_next( [DllImport(libgit2)] internal static extern int git_config_snapshot(out ConfigurationSafeHandle @out, ConfigurationSafeHandle config); + [DllImport(libgit2)] + internal static extern int git_config_add_backend( + ConfigurationSafeHandle config, + IntPtr backend, + uint level, + [MarshalAs(UnmanagedType.Bool)]bool force); + // Ordinarily we would decorate the `url` parameter with the StrictUtf8Marshaler like we do everywhere // else, but apparently doing a native->managed callback with the 64-bit version of CLR 2.0 can // sometimes vomit when using a custom IMarshaler. So yeah, don't do that. If you need the url, diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index 11b3ca209..27c42a3e6 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -425,6 +425,12 @@ public static void git_config_add_file_ondisk(ConfigurationSafeHandle config, Fi Ensure.ZeroResult(res); } + public static void git_config_add_backend(ConfigurationSafeHandle config, IntPtr backend, ConfigurationLevel level, bool force) + { + int res = NativeMethods.git_config_add_backend(config, backend, (uint)level, true); + Ensure.ZeroResult(res); + } + public static bool git_config_delete(ConfigurationSafeHandle config, string name) { int res = NativeMethods.git_config_delete_entry(config, name); diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 561ad7676..cd4008829 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -69,6 +69,7 @@ +