Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow reading patch from diff file and applying it to a repository #1986

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 37 additions & 6 deletions LibGit2Sharp.Tests/PatchEntryChangesFixture.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using LibGit2Sharp.Tests.TestHelpers;
using LibGit2Sharp.Tests.TestHelpers;
using Xunit;
using Xunit.Extensions;

namespace LibGit2Sharp.Tests
{
Expand Down Expand Up @@ -40,5 +35,41 @@ public void PatchEntryBasics()
}
}
}

[Fact]
public void ReadPatch()
{
var path = SandboxStandardTestRepoGitDir();
string file = "numbers.txt";

// The repo
using (var repo = new Repository(path))
{
Tree rootCommitTree = repo.Lookup<Commit>("f8d44d7").Tree;
Tree commitTreeWithUpdatedFile = repo.Lookup<Commit>("ec9e401").Tree;

// Create patch by diffing
using (var patch = repo.Diff.Compare<Patch>(rootCommitTree, commitTreeWithUpdatedFile))
{
string patchContent = patch.Content;

var parsedPatch = Patch.FromPatchContent(patchContent);

Assert.Equal(patch.Content, parsedPatch.Content);

PatchEntryChanges entryChanges = parsedPatch[file];
Assert.Equal(2, entryChanges.LinesAdded);
Assert.Equal(1, entryChanges.LinesDeleted);
Assert.Equal(187, entryChanges.Patch.Length);
// Smoke test
Assert.Equal(Mode.NonExecutableFile, entryChanges.Mode);
Assert.Equal(new ObjectId("4625a36000000000000000000000000000000000"), entryChanges.Oid);
Assert.Equal(ChangeKind.Modified, entryChanges.Status);
Assert.Equal(file, entryChanges.OldPath);
Assert.Equal(Mode.NonExecutableFile, entryChanges.OldMode);
Assert.Equal(new ObjectId("7909961000000000000000000000000000000000"), entryChanges.OldOid);
}
}
}
}
}
21 changes: 21 additions & 0 deletions LibGit2Sharp/Core/GitDiffApplyOpts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Runtime.InteropServices;

namespace LibGit2Sharp.Core
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int git_apply_delta_cb(IntPtr delta, IntPtr payload);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int git_apply_hunk_cb(IntPtr hunk, IntPtr payload);

[StructLayout(LayoutKind.Sequential)]
internal class GitDiffApplyOpts
{
public uint Version = 1;
public git_apply_delta_cb DeltaCallback;
public git_apply_hunk_cb HunkCallback;
public IntPtr Payload;
public uint Flags = 0;
}
}
13 changes: 13 additions & 0 deletions LibGit2Sharp/Core/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ private sealed class NativeShutdownObject : CriticalFinalizerObject
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
internal static extern void git_error_set_oom();

[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
internal static extern unsafe int git_apply(
git_repository* repo,
git_diff* diff,
PatchApplyLocation location,
GitDiffApplyOpts options);

[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
internal static extern unsafe UInt32 git_blame_get_hunk_count(git_blame* blame);

Expand Down Expand Up @@ -710,6 +717,12 @@ private sealed class NativeShutdownObject : CriticalFinalizerObject
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
internal static extern unsafe git_diff_delta* git_diff_get_delta(git_diff* diff, UIntPtr idx);

[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
internal static extern unsafe int git_diff_from_buffer(out git_diff* diff,
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))]
string content,
UIntPtr len);

[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
internal static extern int git_filter_register(
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name,
Expand Down
22 changes: 22 additions & 0 deletions LibGit2Sharp/Core/Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ internal class Proxy
internal static readonly bool isOSXArm64 = RuntimeInformation.ProcessArchitecture == Architecture.Arm64
&& RuntimeInformation.IsOSPlatform(OSPlatform.OSX);

#region git_apply_

public static unsafe void git_apply(
RepositoryHandle repo,
DiffHandle diff,
PatchApplyLocation location,
GitDiffApplyOpts options)
{
int res = NativeMethods.git_apply(repo, diff, location, options ?? new GitDiffApplyOpts());

Ensure.ZeroResult(res);
}

#endregion

#region git_blame_

public static unsafe BlameHandle git_blame_file(
Expand Down Expand Up @@ -854,6 +869,13 @@ public static unsafe int git_diff_num_deltas(DiffHandle diff)
return NativeMethods.git_diff_get_delta(diff, (UIntPtr)idx);
}

public static unsafe DiffHandle git_diff_from_buffer(string content, UIntPtr len)
{
int res = NativeMethods.git_diff_from_buffer(out var diff, content, len);
Ensure.ZeroResult(res);
return new DiffHandle(diff, true);
}

#endregion

#region git_error_
Expand Down
23 changes: 23 additions & 0 deletions LibGit2Sharp/DeltaApplyStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace LibGit2Sharp
{
/// <summary>
/// What to do with a delta.
/// </summary>
public enum DeltaApplyStatus
{
/// <summary>
/// Apply the delta.
/// </summary>
Apply,

/// <summary>
/// Do not apply the delta and abort.
/// </summary>
Abort,

/// <summary>
/// Do not apply the delta and continue.
/// </summary>
Skip
}
}
10 changes: 10 additions & 0 deletions LibGit2Sharp/Patch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ internal unsafe Patch(DiffHandle diff)
}
}

/// <summary>
/// Instantiate a patch from its content.
/// </summary>
/// <param name="content">The patch content</param>
/// <returns>The Patch instance</returns>
public static Patch FromPatchContent(string content)
{
return new Patch(Proxy.git_diff_from_buffer(content, (UIntPtr)content.Length));
}

private unsafe void AddFileChange(git_diff_delta* delta)
{
var treeEntryChanges = new TreeEntryChanges(delta);
Expand Down
37 changes: 37 additions & 0 deletions LibGit2Sharp/PatchApplyOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace LibGit2Sharp
{
/// <summary>
/// The options to be used for patch application.
/// </summary>
public sealed class PatchApplyOptions
{
/// <summary>
/// The location to apply (workdir, index or both)
/// </summary>
public PatchApplyLocation Location { get; set; }
}

/// <summary>
/// Possible application locations for applying a patch.
/// </summary>
public enum PatchApplyLocation
{
/// <summary>
/// Apply the patch to the workdir, leaving the index untouched.
/// This is the equivalent of `git apply` with no location argument.
/// </summary>
Workdir = 0,

/// <summary>
/// Apply the patch to the index, leaving the working directory
/// untouched. This is the equivalent of `git apply --cached`.
/// </summary>
Index = 1,

/// <summary>
/// Apply the patch to both the working directory and the index.
/// This is the equivalent of `git apply --index`.
/// </summary>
Both = 2
}
}
33 changes: 33 additions & 0 deletions LibGit2Sharp/RepositoryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using LibGit2Sharp.Core;
using LibGit2Sharp.Core.Handles;

namespace LibGit2Sharp
{
Expand Down Expand Up @@ -470,5 +471,37 @@ public static string Describe(this IRepository repository, Commit commit)
{
return repository.Describe(commit, new DescribeOptions());
}

/// <summary>
/// Applies a patch to a repository.
/// </summary>
/// <param name="repository">The <see cref="IRepository"/> being worked with.</param>
/// <param name="patchContent">The diff to be applied.</param>
/// <param name="applyOptions">The options to use.</param>
public static void ApplyPatch(this Repository repository, string patchContent, PatchApplyOptions applyOptions = null)
{
if (applyOptions == null)
{
applyOptions = new PatchApplyOptions();
}

using (var diff = Proxy.git_diff_from_buffer(patchContent, (UIntPtr) patchContent.Length))
{
Proxy.git_apply(repository.Handle, diff, applyOptions.Location, null);
}
}

/// <summary>
/// Applies a patch file to a repository.
/// </summary>
/// <param name="repository">The <see cref="IRepository"/> being worked with.</param>
/// <param name="patchFilePath">The path to the patch file</param>
/// <param name="options">The options to use.</param>
public static void ApplyPatchFile(this Repository repository, string patchFilePath, PatchApplyOptions options = null)
{
string content = File.ReadAllText(patchFilePath);

ApplyPatch(repository, content, options);
}
}
}