diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..b69744f78 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +FROM mcr.microsoft.com/devcontainers/base:debian + +# Install .net 6 and .net 7 +# https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian +RUN wget https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb -O packages-microsoft-prod.deb && \ + dpkg -i packages-microsoft-prod.deb && \ + rm packages-microsoft-prod.deb +RUN apt-get update -yq && \ + apt-get install -yq --no-install-recommends \ + dotnet-sdk-6.0 dotnet-sdk-7.0 + +# Install mono framework +# https://www.mono-project.com/download/stable/#download-lin-debian +RUN apt-get install -yq --no-install-recommends \ + dirmngr ca-certificates gnupg +RUN gpg --keyserver keyserver.ubuntu.com --recv 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \ + gpg --export 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF | tee /usr/share/keyrings/mono.gpg >/dev/null && \ + gpg --batch --yes --delete-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \ + echo "deb [signed-by=/usr/share/keyrings/mono.gpg] https://download.mono-project.com/repo/debian stable-buster main" | tee /etc/apt/sources.list.d/mono-official-stable.list +RUN apt-get update -yq && \ + apt-get install -yq --no-install-recommends \ + mono-devel diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..08fea6e70 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet +{ + "name": "C# (.NET)", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "dockerFile": "Dockerfile", + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [5000, 5001], + // "portsAttributes": { + // "5001": { + // "protocol": "https" + // } + // } + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "dotnet restore", + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csharp" + ] + } + } + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} \ No newline at end of file diff --git a/LibGit2Sharp.Tests/PushFixture.cs b/LibGit2Sharp.Tests/PushFixture.cs index d8cf2befe..fc6c0c42d 100644 --- a/LibGit2Sharp.Tests/PushFixture.cs +++ b/LibGit2Sharp.Tests/PushFixture.cs @@ -196,6 +196,33 @@ public void CanForcePush() } } + [Fact] + public void CanPushWithCustomHeaders() + { + const string knownHeader = "X-Hello: mygit-201"; + var options = new PushOptions { CustomHeaders = new string[] { knownHeader } }; + AssertPush(repo => + repo.Network.Push(repo.Network.Remotes["origin"], "HEAD", @"refs/heads/master", options)); + } + + [Fact] + public void CannotPushWithForbiddenCustomHeaders() + { + const string knownHeader = "User-Agent: mygit-201"; + var options = new PushOptions { CustomHeaders = new string[] { knownHeader } }; + Assert.Throws( + () => AssertPush(repo => repo.Network.Push(repo.Network.Remotes["origin"], "HEAD", @"refs/heads/master", options))); + } + + [Fact] + public void CannotPushWithMalformedCustomHeaders() + { + const string knownHeader = "Hello world"; + var options = new PushOptions { CustomHeaders = new string[] { knownHeader } }; + Assert.Throws( + () => AssertPush(repo => repo.Network.Push(repo.Network.Remotes["origin"], "HEAD", @"refs/heads/master", options))); + } + private static void AssertRemoteHeadTipEquals(IRepository localRepo, string sha) { var remoteReferences = localRepo.Network.ListReferences(localRepo.Network.Remotes.Single()); diff --git a/LibGit2Sharp/Core/GitPushOptionsWrapper.cs b/LibGit2Sharp/Core/GitPushOptionsWrapper.cs new file mode 100644 index 000000000..ffac5a220 --- /dev/null +++ b/LibGit2Sharp/Core/GitPushOptionsWrapper.cs @@ -0,0 +1,36 @@ +using System; + +namespace LibGit2Sharp.Core +{ + /// + /// Git push options wrapper. Disposable wrapper for . + /// + internal class GitPushOptionsWrapper : IDisposable + { + public GitPushOptionsWrapper() : this(new GitPushOptions()) { } + + public GitPushOptionsWrapper(GitPushOptions pushOptions) + { + this.Options = pushOptions; + } + + public GitPushOptions Options { get; private set; } + + #region IDisposable + private bool disposedValue = false; // To detect redundant calls + protected virtual void Dispose(bool disposing) + { + if (disposedValue) + return; + + this.Options.CustomHeaders.Dispose(); + disposedValue = true; + } + + public void Dispose() + { + Dispose(true); + } + #endregion + } +} diff --git a/LibGit2Sharp/Network.cs b/LibGit2Sharp/Network.cs index d5f032058..825f3c886 100644 --- a/LibGit2Sharp/Network.cs +++ b/LibGit2Sharp/Network.cs @@ -365,18 +365,27 @@ public virtual void Push(Remote remote, IEnumerable pushRefSpecs, PushOp // Load the remote. using (RemoteHandle remoteHandle = Proxy.git_remote_lookup(repository.Handle, remote.Name, true)) + + // Create a git options wrapper so managed strings are disposed. + using (var pushOptionsWrapper = new GitPushOptionsWrapper()) { var callbacks = new RemoteCallbacks(pushOptions); GitRemoteCallbacks gitCallbacks = callbacks.GenerateCallbacks(); + var gitPushOptions = pushOptionsWrapper.Options; + gitPushOptions.PackbuilderDegreeOfParallelism = pushOptions.PackbuilderDegreeOfParallelism; + gitPushOptions.RemoteCallbacks = gitCallbacks; + gitPushOptions.ProxyOptions = new GitProxyOptions { Version = 1 }; + + // If there are custom headers, create a managed string array. + if (pushOptions.CustomHeaders != null && pushOptions.CustomHeaders.Length > 0) + { + gitPushOptions.CustomHeaders = GitStrArrayManaged.BuildFrom(pushOptions.CustomHeaders); + } + Proxy.git_remote_push(remoteHandle, pushRefSpecs, - new GitPushOptions() - { - PackbuilderDegreeOfParallelism = pushOptions.PackbuilderDegreeOfParallelism, - RemoteCallbacks = gitCallbacks, - ProxyOptions = new GitProxyOptions { Version = 1 }, - }); + gitPushOptions); } } diff --git a/LibGit2Sharp/PushOptions.cs b/LibGit2Sharp/PushOptions.cs index 99c65dd8b..f7d37eba9 100644 --- a/LibGit2Sharp/PushOptions.cs +++ b/LibGit2Sharp/PushOptions.cs @@ -51,5 +51,25 @@ public sealed class PushOptions /// information about what updates will be performed. /// public PrePushHandler OnNegotiationCompletedBeforePush { get; set; } + + /// + /// Get/Set the custom headers. + /// + /// This allows you to set custom headers (e.g. X-Forwarded-For, + /// X-Request-Id, etc), + /// + /// + /// + /// Libgit2 sets some headers for HTTP requests (User-Agent, Host, + /// Accept, Content-Type, Transfer-Encoding, Content-Length, Accept) that + /// cannot be overriden. + /// + /// + /// var pushOptions - new PushOptions() { + /// CustomHeaders = new String[] {"X-Request-Id: 12345"} + /// }; + /// + /// The custom headers string array + public string[] CustomHeaders { get; set; } } }