diff --git a/LibGit2Sharp.Tests/PushFixture.cs b/LibGit2Sharp.Tests/PushFixture.cs index 10261d0b3..0c1b05d9d 100644 --- a/LibGit2Sharp.Tests/PushFixture.cs +++ b/LibGit2Sharp.Tests/PushFixture.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using LibGit2Sharp.Handlers; @@ -78,6 +79,63 @@ public void CanPushABranchTrackingAnUpstreamBranch() Assert.True(packBuilderCalled); } + [Fact] + public void CanInvokePrePushCallbackAndSucceed() + { + bool packBuilderCalled = false; + bool prePushHandlerCalled = false; + PackBuilderProgressHandler packBuilderCb = (x, y, z) => { packBuilderCalled = true; return true; }; + PrePushHandler prePushHook = (IEnumerable updates) => + { + Assert.True(updates.Count() == 1, "Expected 1 update, received " + updates.Count()); + prePushHandlerCalled = true; + return true; + }; + + AssertPush(repo => repo.Network.Push(repo.Head)); + AssertPush(repo => repo.Network.Push(repo.Branches["master"])); + + PushOptions options = new PushOptions() + { + OnPushStatusError = OnPushStatusError, + OnPackBuilderProgress = packBuilderCb, + OnNegotiationCompletedBeforePush = prePushHook, + }; + + AssertPush(repo => repo.Network.Push(repo.Network.Remotes["origin"], "HEAD", @"refs/heads/master", options)); + Assert.True(packBuilderCalled); + Assert.True(prePushHandlerCalled); + } + + [Fact] + public void CanInvokePrePushCallbackAndFail() + { + bool packBuilderCalled = false; + bool prePushHandlerCalled = false; + PackBuilderProgressHandler packBuilderCb = (x, y, z) => { packBuilderCalled = true; return true; }; + PrePushHandler prePushHook = (IEnumerable updates) => + { + Assert.True(updates.Count() == 1, "Expected 1 update, received " + updates.Count()); + prePushHandlerCalled = true; + return false; + }; + + AssertPush(repo => repo.Network.Push(repo.Head)); + AssertPush(repo => repo.Network.Push(repo.Branches["master"])); + + PushOptions options = new PushOptions() + { + OnPushStatusError = OnPushStatusError, + OnPackBuilderProgress = packBuilderCb, + OnNegotiationCompletedBeforePush = prePushHook + }; + + Assert.Throws(() => { AssertPush(repo => repo.Network.Push(repo.Network.Remotes["origin"], "HEAD", @"refs/heads/master", options)); }); + + Assert.False(packBuilderCalled); + Assert.True(prePushHandlerCalled); + } + [Fact] public void PushingABranchThatDoesNotTrackAnUpstreamBranchThrows() { diff --git a/LibGit2Sharp/Core/GitPushUpdate.cs b/LibGit2Sharp/Core/GitPushUpdate.cs index f38697a42..5e5246622 100644 --- a/LibGit2Sharp/Core/GitPushUpdate.cs +++ b/LibGit2Sharp/Core/GitPushUpdate.cs @@ -4,11 +4,11 @@ namespace LibGit2Sharp.Core { [StructLayout(LayoutKind.Sequential)] - internal class GitPushUpdate + internal struct GitPushUpdate { - IntPtr src_refname; - IntPtr dst_refname; - GitOid src; - GitOid dst; + public IntPtr src_refname; + public IntPtr dst_refname; + public GitOid src; + public GitOid dst; } } diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index 9b4e818f3..53c43078d 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -1141,10 +1141,9 @@ internal delegate int remote_update_tips_callback( IntPtr data); internal delegate int push_negotiation_callback( - IntPtr updates, // GitPushUpdate? + IntPtr updates, UIntPtr len, - IntPtr payload - ); + IntPtr payload); internal delegate int push_update_reference_callback( IntPtr refName, diff --git a/LibGit2Sharp/Handlers.cs b/LibGit2Sharp/Handlers.cs index 196b438fd..2e1c9b6dc 100644 --- a/LibGit2Sharp/Handlers.cs +++ b/LibGit2Sharp/Handlers.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; + namespace LibGit2Sharp.Handlers { /// @@ -71,6 +73,13 @@ namespace LibGit2Sharp.Handlers /// True to continue, false to cancel. public delegate bool PackBuilderProgressHandler(PackBuilderStage stage, int current, int total); + /// + /// Provides information about what updates will be performed before a push occurs + /// + /// List of updates about to be performed via push + /// True to continue, false to cancel. + public delegate bool PrePushHandler(IEnumerable updates); + /// /// Delegate definition to handle reporting errors when updating references on the remote. /// diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index c20b0933b..d17d6101b 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -121,6 +121,7 @@ + diff --git a/LibGit2Sharp/PushOptions.cs b/LibGit2Sharp/PushOptions.cs index 15e6af691..f4874643e 100644 --- a/LibGit2Sharp/PushOptions.cs +++ b/LibGit2Sharp/PushOptions.cs @@ -39,5 +39,11 @@ public sealed class PushOptions /// be more than once every 0.5 seconds (in general). /// public PackBuilderProgressHandler OnPackBuilderProgress { get; set; } + + /// + /// Called once between the negotiation step and the upload. It provides + /// information about what updates will be performed. + /// + public PrePushHandler OnNegotiationCompletedBeforePush { get; set; } } } diff --git a/LibGit2Sharp/PushUpdate.cs b/LibGit2Sharp/PushUpdate.cs new file mode 100644 index 000000000..d948408c9 --- /dev/null +++ b/LibGit2Sharp/PushUpdate.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.InteropServices; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// Represents an update which will be performed on the remote during push + /// + public class PushUpdate + { + internal PushUpdate(string srcRefName, ObjectId srcOid, string dstRefName, ObjectId dstOid) + { + DestinationObjectId = dstOid; + DestinationRefName = dstRefName; + SourceObjectId = srcOid; + SourceRefName = srcRefName; + } + internal PushUpdate(GitPushUpdate update) + { + DestinationObjectId = update.dst; + DestinationRefName = LaxUtf8Marshaler.FromNative(update.dst_refname); + SourceObjectId = update.src; + SourceRefName = LaxUtf8Marshaler.FromNative(update.src_refname); + } + /// + /// Empty constructor to support test suites + /// + protected PushUpdate() + { + DestinationObjectId = ObjectId.Zero; + DestinationRefName = String.Empty; + SourceObjectId = ObjectId.Zero; + SourceRefName = String.Empty; + } + + /// + /// The source name of the reference + /// + public readonly string SourceRefName; + /// + /// The name of the reference to update on the server + /// + public readonly string DestinationRefName; + /// + /// The current target of the reference + /// + public readonly ObjectId SourceObjectId; + /// + /// The new target for the reference + /// + public readonly ObjectId DestinationObjectId; + } +} diff --git a/LibGit2Sharp/RemoteCallbacks.cs b/LibGit2Sharp/RemoteCallbacks.cs index 55be945d2..8d8a06a99 100644 --- a/LibGit2Sharp/RemoteCallbacks.cs +++ b/LibGit2Sharp/RemoteCallbacks.cs @@ -28,6 +28,7 @@ internal RemoteCallbacks(PushOptions pushOptions) PackBuilderProgress = pushOptions.OnPackBuilderProgress; CredentialsProvider = pushOptions.CredentialsProvider; PushStatusError = pushOptions.OnPushStatusError; + PrePushCallback = pushOptions.OnNegotiationCompletedBeforePush; } internal RemoteCallbacks(FetchOptionsBase fetchOptions) @@ -77,6 +78,11 @@ internal RemoteCallbacks(FetchOptionsBase fetchOptions) /// private readonly PackBuilderProgressHandler PackBuilderProgress; + /// + /// Called during remote push operation after negotiation, before upload + /// + private readonly PrePushHandler PrePushCallback; + #endregion /// @@ -86,7 +92,7 @@ internal RemoteCallbacks(FetchOptionsBase fetchOptions) internal GitRemoteCallbacks GenerateCallbacks() { - var callbacks = new GitRemoteCallbacks {version = 1}; + var callbacks = new GitRemoteCallbacks { version = 1 }; if (Progress != null) { @@ -123,6 +129,11 @@ internal GitRemoteCallbacks GenerateCallbacks() callbacks.pack_progress = GitPackbuilderProgressHandler; } + if (PrePushCallback != null) + { + callbacks.push_negotiation = GitPushNegotiationHandler; + } + return callbacks; } @@ -185,7 +196,7 @@ private int GitUpdateTipsHandler(IntPtr str, ref GitOid oldId, ref GitOid newId, /// 0 on success; a negative value to abort the process. private int GitPushUpdateReference(IntPtr str, IntPtr status, IntPtr data) { - PushStatusErrorHandler onPushError = PushStatusError; + PushStatusErrorHandler onPushError = PushStatusError; if (onPushError != null) { @@ -262,6 +273,49 @@ private int GitCredentialHandler(out IntPtr ptr, IntPtr cUrl, IntPtr usernameFro return cred.GitCredentialHandler(out ptr); } + private int GitPushNegotiationHandler(IntPtr updates, UIntPtr len, IntPtr payload) + { + if (updates == IntPtr.Zero) + { + return (int)GitErrorCode.Error; + } + + bool result = false; + try + { + + int length = len.ConvertToInt(); + PushUpdate[] pushUpdates = new PushUpdate[length]; + + unsafe + { + IntPtr* ptr = (IntPtr*)updates.ToPointer(); + + for (int i = 0; i < length; i++) + { + if (ptr[i] == IntPtr.Zero) + { + throw new NullReferenceException("Unexpected null git_push_update pointer was encountered"); + } + + GitPushUpdate gitPushUpdate = ptr[i].MarshalAs(); + PushUpdate pushUpdate = new PushUpdate(gitPushUpdate); + pushUpdates[i] = pushUpdate; + } + + result = PrePushCallback(pushUpdates); + } + } + catch (Exception exception) + { + Log.Write(LogLevel.Error, exception.ToString()); + Proxy.giterr_set_str(GitErrorCategory.Callback, exception); + result = false; + } + + return Proxy.ConvertResultToCancelFlag(result); + } + #endregion } }