summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormichael boulton <[email protected]>2020-08-18 14:14:02 +0100
committerGitHub <[email protected]>2020-08-18 06:14:02 -0700
commit7883ec85de56ee55667481228282fd690fce6246 (patch)
tree8a686eb37f0b79d2a81b05bb50e8cfcc292b796e
parent2ac9f4e69bd57a686d15176d199a3c9cc4a6bb91 (diff)
More diff functionality (#629)
This PR adds - The ability to apply a Diff object to the repo - Support for git_apply_hunk_cb and git_apply_delta_cb callbacks in options for applying the diffs - The ability to import a diff from a raw buffer (for example, one exported by ToBuf) into a Diff object associated with the repo - Tests for the above
-rw-r--r--diff.go174
-rw-r--r--diff_test.go307
-rw-r--r--wrapper.c6
3 files changed, 486 insertions, 1 deletions
diff --git a/diff.go b/diff.go
index e022b47..9d17dd7 100644
--- a/diff.go
+++ b/diff.go
@@ -3,6 +3,7 @@ package git
/*
#include <git2.h>
+extern void _go_git_populate_apply_cb(git_apply_options *options);
extern int _go_git_diff_foreach(git_diff *diff, int eachFile, int eachHunk, int eachLine, void *payload);
extern void _go_git_setup_diff_notify_callbacks(git_diff_options* opts);
extern int _go_git_diff_blobs(git_blob *old, const char *old_path, git_blob *new, const char *new_path, git_diff_options *opts, int eachFile, int eachHunk, int eachLine, void *payload);
@@ -550,7 +551,7 @@ const (
DiffFindRemoveUnmodified DiffFindOptionsFlag = C.GIT_DIFF_FIND_REMOVE_UNMODIFIED
)
-//TODO implement git_diff_similarity_metric
+// TODO implement git_diff_similarity_metric
type DiffFindOptions struct {
Flags DiffFindOptionsFlag
RenameThreshold uint16
@@ -847,3 +848,174 @@ func DiffBlobs(oldBlob *Blob, oldAsPath string, newBlob *Blob, newAsPath string,
return nil
}
+
+// ApplyHunkCallback is a callback that will be made per delta (file) when applying a patch.
+type ApplyHunkCallback func(*DiffHunk) (apply bool, err error)
+
+// ApplyDeltaCallback is a callback that will be made per hunk when applying a patch.
+type ApplyDeltaCallback func(*DiffDelta) (apply bool, err error)
+
+// ApplyOptions has 2 callbacks that are called for hunks or deltas
+// If these functions return an error, abort the apply process immediately.
+// If the first return value is true, the delta/hunk will be applied. If it is false, the delta/hunk will not be applied. In either case, the rest of the apply process will continue.
+type ApplyOptions struct {
+ ApplyHunkCallback ApplyHunkCallback
+ ApplyDeltaCallback ApplyDeltaCallback
+ Flags uint
+}
+
+//export hunkApplyCallback
+func hunkApplyCallback(_hunk *C.git_diff_hunk, _payload unsafe.Pointer) C.int {
+ opts, ok := pointerHandles.Get(_payload).(*ApplyOptions)
+ if !ok {
+ panic("invalid apply options payload")
+ }
+
+ if opts.ApplyHunkCallback == nil {
+ return 0
+ }
+
+ hunk := diffHunkFromC(_hunk)
+
+ apply, err := opts.ApplyHunkCallback(&hunk)
+ if err != nil {
+ if gitError, ok := err.(*GitError); ok {
+ return C.int(gitError.Code)
+ }
+ return -1
+ } else if apply {
+ return 0
+ } else {
+ return 1
+ }
+}
+
+//export deltaApplyCallback
+func deltaApplyCallback(_delta *C.git_diff_delta, _payload unsafe.Pointer) C.int {
+ opts, ok := pointerHandles.Get(_payload).(*ApplyOptions)
+ if !ok {
+ panic("invalid apply options payload")
+ }
+
+ if opts.ApplyDeltaCallback == nil {
+ return 0
+ }
+
+ delta := diffDeltaFromC(_delta)
+
+ apply, err := opts.ApplyDeltaCallback(&delta)
+ if err != nil {
+ if gitError, ok := err.(*GitError); ok {
+ return C.int(gitError.Code)
+ }
+ return -1
+ } else if apply {
+ return 0
+ } else {
+ return 1
+ }
+}
+
+// DefaultApplyOptions returns default options for applying diffs or patches.
+func DefaultApplyOptions() (*ApplyOptions, error) {
+ opts := C.git_apply_options{}
+
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+
+ ecode := C.git_apply_options_init(&opts, C.GIT_APPLY_OPTIONS_VERSION)
+ if int(ecode) != 0 {
+
+ return nil, MakeGitError(ecode)
+ }
+
+ return applyOptionsFromC(&opts), nil
+}
+
+func (a *ApplyOptions) toC() *C.git_apply_options {
+ if a == nil {
+ return nil
+ }
+
+ opts := &C.git_apply_options{
+ version: C.GIT_APPLY_OPTIONS_VERSION,
+ flags: C.uint(a.Flags),
+ }
+
+ if a.ApplyDeltaCallback != nil || a.ApplyHunkCallback != nil {
+ C._go_git_populate_apply_cb(opts)
+ opts.payload = pointerHandles.Track(a)
+ }
+
+ return opts
+}
+
+func applyOptionsFromC(opts *C.git_apply_options) *ApplyOptions {
+ return &ApplyOptions{
+ Flags: uint(opts.flags),
+ }
+}
+
+// ApplyLocation represents the possible application locations for applying
+// diffs.
+type ApplyLocation int
+
+const (
+ // ApplyLocationWorkdir applies the patch to the workdir, leaving the
+ // index untouched. This is the equivalent of `git apply` with no location
+ // argument.
+ ApplyLocationWorkdir ApplyLocation = C.GIT_APPLY_LOCATION_WORKDIR
+ // ApplyLocationIndex applies the patch to the index, leaving the working
+ // directory untouched. This is the equivalent of `git apply --cached`.
+ ApplyLocationIndex ApplyLocation = C.GIT_APPLY_LOCATION_INDEX
+ // ApplyLocationBoth applies the patch to both the working directory and
+ // the index. This is the equivalent of `git apply --index`.
+ ApplyLocationBoth ApplyLocation = C.GIT_APPLY_LOCATION_BOTH
+)
+
+// ApplyDiff appllies a Diff to the given repository, making changes directly
+// in the working directory, the index, or both.
+func (v *Repository) ApplyDiff(diff *Diff, location ApplyLocation, opts *ApplyOptions) error {
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+
+ cOpts := opts.toC()
+ ecode := C.git_apply(v.ptr, diff.ptr, C.git_apply_location_t(location), cOpts)
+ runtime.KeepAlive(v)
+ runtime.KeepAlive(diff)
+ runtime.KeepAlive(cOpts)
+ if ecode < 0 {
+ return MakeGitError(ecode)
+ }
+
+ return nil
+}
+
+// DiffFromBuffer reads the contents of a git patch file into a Diff object.
+//
+// The diff object produced is similar to the one that would be produced if you
+// actually produced it computationally by comparing two trees, however there
+// may be subtle differences. For example, a patch file likely contains
+// abbreviated object IDs, so the object IDs in a git_diff_delta produced by
+// this function will also be abbreviated.
+//
+// This function will only read patch files created by a git implementation, it
+// will not read unified diffs produced by the diff program, nor any other
+// types of patch files.
+func DiffFromBuffer(buffer []byte, repo *Repository) (*Diff, error) {
+ var diff *C.git_diff
+
+ cBuffer := C.CBytes(buffer)
+ defer C.free(unsafe.Pointer(cBuffer))
+
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+
+ ecode := C.git_diff_from_buffer(&diff, (*C.char)(cBuffer), C.size_t(len(buffer)))
+ if ecode < 0 {
+ return nil, MakeGitError(ecode)
+ }
+ runtime.KeepAlive(diff)
+
+ return newDiffFromC(diff, repo), nil
+}
diff --git a/diff_test.go b/diff_test.go
index 6fbad51..394a4c1 100644
--- a/diff_test.go
+++ b/diff_test.go
@@ -2,6 +2,9 @@ package git
import (
"errors"
+ "fmt"
+ "io/ioutil"
+ "path"
"strings"
"testing"
)
@@ -236,3 +239,307 @@ func TestDiffBlobs(t *testing.T) {
t.Fatalf("Bad number of lines iterated")
}
}
+
+func TestApplyDiffAddfile(t *testing.T) {
+ repo := createTestRepo(t)
+ defer cleanupTestRepo(t, repo)
+
+ seedTestRepo(t, repo)
+
+ addFirstFileCommit, addFileTree := addAndGetTree(t, repo, "file1", `hello`)
+ addSecondFileCommit, addSecondFileTree := addAndGetTree(t, repo, "file2", `hello2`)
+
+ diff, err := repo.DiffTreeToTree(addFileTree, addSecondFileTree, nil)
+ checkFatal(t, err)
+
+ t.Run("check does not apply to current tree because file exists", func(t *testing.T) {
+ err = repo.ResetToCommit(addSecondFileCommit, ResetHard, &CheckoutOpts{})
+ checkFatal(t, err)
+
+ err = repo.ApplyDiff(diff, ApplyLocationBoth, nil)
+ if err == nil {
+ t.Error("expecting applying patch to current repo to fail")
+ }
+ })
+
+ t.Run("check apply to correct commit", func(t *testing.T) {
+ err = repo.ResetToCommit(addFirstFileCommit, ResetHard, &CheckoutOpts{})
+ checkFatal(t, err)
+
+ err = repo.ApplyDiff(diff, ApplyLocationBoth, nil)
+ checkFatal(t, err)
+
+ t.Run("Check that diff only changed one file", func(t *testing.T) {
+ checkSecondFileStaged(t, repo)
+
+ index, err := repo.Index()
+ checkFatal(t, err)
+ defer index.Free()
+
+ newTreeOID, err := index.WriteTreeTo(repo)
+ checkFatal(t, err)
+
+ newTree, err := repo.LookupTree(newTreeOID)
+ checkFatal(t, err)
+ defer newTree.Free()
+
+ _, err = repo.CreateCommit("HEAD", signature(), signature(), fmt.Sprintf("patch apply"), newTree, addFirstFileCommit)
+ checkFatal(t, err)
+ })
+
+ t.Run("test applying patch produced the same diff", func(t *testing.T) {
+ head, err := repo.Head()
+ checkFatal(t, err)
+
+ commit, err := repo.LookupCommit(head.Target())
+ checkFatal(t, err)
+
+ tree, err := commit.Tree()
+ checkFatal(t, err)
+
+ newDiff, err := repo.DiffTreeToTree(addFileTree, tree, nil)
+ checkFatal(t, err)
+
+ raw1b, err := diff.ToBuf(DiffFormatPatch)
+ checkFatal(t, err)
+ raw2b, err := newDiff.ToBuf(DiffFormatPatch)
+ checkFatal(t, err)
+
+ raw1 := string(raw1b)
+ raw2 := string(raw2b)
+
+ if raw1 != raw2 {
+ t.Error("diffs should be the same")
+ }
+ })
+ })
+
+ t.Run("check convert to raw buffer and apply", func(t *testing.T) {
+ err = repo.ResetToCommit(addFirstFileCommit, ResetHard, &CheckoutOpts{})
+ checkFatal(t, err)
+
+ raw, err := diff.ToBuf(DiffFormatPatch)
+ checkFatal(t, err)
+
+ if len(raw) == 0 {
+ t.Error("empty diff created")
+ }
+
+ diff2, err := DiffFromBuffer(raw, repo)
+ checkFatal(t, err)
+
+ err = repo.ApplyDiff(diff2, ApplyLocationBoth, nil)
+ checkFatal(t, err)
+ })
+
+ t.Run("check apply callbacks work", func(t *testing.T) {
+ // reset the state and get new default options for test
+ resetAndGetOpts := func(t *testing.T) *ApplyOptions {
+ err = repo.ResetToCommit(addFirstFileCommit, ResetHard, &CheckoutOpts{})
+ checkFatal(t, err)
+
+ opts, err := DefaultApplyOptions()
+ checkFatal(t, err)
+
+ return opts
+ }
+
+ t.Run("Check hunk callback working applies patch", func(t *testing.T) {
+ opts := resetAndGetOpts(t)
+
+ called := false
+ opts.ApplyHunkCallback = func(hunk *DiffHunk) (apply bool, err error) {
+ called = true
+ return true, nil
+ }
+
+ err = repo.ApplyDiff(diff, ApplyLocationBoth, opts)
+ checkFatal(t, err)
+
+ if called == false {
+ t.Error("apply hunk callback was not called")
+ }
+
+ checkSecondFileStaged(t, repo)
+ })
+
+ t.Run("Check delta callback working applies patch", func(t *testing.T) {
+ opts := resetAndGetOpts(t)
+
+ called := false
+ opts.ApplyDeltaCallback = func(hunk *DiffDelta) (apply bool, err error) {
+ if hunk.NewFile.Path != "file2" {
+ t.Error("Unexpected delta in diff application")
+ }
+ called = true
+ return true, nil
+ }
+
+ err = repo.ApplyDiff(diff, ApplyLocationBoth, opts)
+ checkFatal(t, err)
+
+ if called == false {
+ t.Error("apply hunk callback was not called")
+ }
+
+ checkSecondFileStaged(t, repo)
+ })
+
+ t.Run("Check delta callback returning false does not apply patch", func(t *testing.T) {
+ opts := resetAndGetOpts(t)
+
+ called := false
+ opts.ApplyDeltaCallback = func(hunk *DiffDelta) (apply bool, err error) {
+ if hunk.NewFile.Path != "file2" {
+ t.Error("Unexpected hunk in diff application")
+ }
+ called = true
+ return false, nil
+ }
+
+ err = repo.ApplyDiff(diff, ApplyLocationBoth, opts)
+ checkFatal(t, err)
+
+ if called == false {
+ t.Error("apply hunk callback was not called")
+ }
+
+ checkNoFilesStaged(t, repo)
+ })
+
+ t.Run("Check hunk callback returning causes application to fail", func(t *testing.T) {
+ opts := resetAndGetOpts(t)
+
+ called := false
+ opts.ApplyHunkCallback = func(hunk *DiffHunk) (apply bool, err error) {
+ called = true
+ return false, errors.New("something happened")
+ }
+
+ err = repo.ApplyDiff(diff, ApplyLocationBoth, opts)
+ if err == nil {
+ t.Error("expected an error after trying to apply")
+ }
+
+ if called == false {
+ t.Error("apply hunk callback was not called")
+ }
+
+ checkNoFilesStaged(t, repo)
+ })
+
+ t.Run("Check delta callback returning causes application to fail", func(t *testing.T) {
+ opts := resetAndGetOpts(t)
+
+ called := false
+ opts.ApplyDeltaCallback = func(hunk *DiffDelta) (apply bool, err error) {
+ if hunk.NewFile.Path != "file2" {
+ t.Error("Unexpected delta in diff application")
+ }
+ called = true
+ return false, errors.New("something happened")
+ }
+
+ err = repo.ApplyDiff(diff, ApplyLocationBoth, opts)
+ if err == nil {
+ t.Error("expected an error after trying to apply")
+ }
+
+ if called == false {
+ t.Error("apply hunk callback was not called")
+ }
+
+ checkNoFilesStaged(t, repo)
+ })
+ })
+}
+
+// checkSecondFileStaged checks that there is a single file called "file2" uncommitted in the repo
+func checkSecondFileStaged(t *testing.T, repo *Repository) {
+ opts := StatusOptions{
+ Show: StatusShowIndexAndWorkdir,
+ Flags: StatusOptIncludeUntracked,
+ }
+
+ statuses, err := repo.StatusList(&opts)
+ checkFatal(t, err)
+
+ count, err := statuses.EntryCount()
+ checkFatal(t, err)
+
+ if count != 1 {
+ t.Error("diff should affect exactly one file")
+ }
+ if count == 0 {
+ t.Fatal("no statuses, cannot continue test")
+ }
+
+ entry, err := statuses.ByIndex(0)
+ checkFatal(t, err)
+
+ if entry.Status != StatusIndexNew {
+ t.Error("status should be 'new' as file has been added between commits")
+ }
+
+ if entry.HeadToIndex.NewFile.Path != "file2" {
+ t.Error("new file should be 'file2")
+ }
+ return
+}
+
+// checkNoFilesStaged checks that there is a single file called "file2" uncommitted in the repo
+func checkNoFilesStaged(t *testing.T, repo *Repository) {
+ opts := StatusOptions{
+ Show: StatusShowIndexAndWorkdir,
+ Flags: StatusOptIncludeUntracked,
+ }
+
+ statuses, err := repo.StatusList(&opts)
+ checkFatal(t, err)
+
+ count, err := statuses.EntryCount()
+ checkFatal(t, err)
+
+ if count != 0 {
+ t.Error("files changed unexpectedly")
+ }
+}
+
+// addAndGetTree creates a file and commits it, returning the commit and tree
+func addAndGetTree(t *testing.T, repo *Repository, filename string, content string) (*Commit, *Tree) {
+ headCommit, err := headCommit(repo)
+ checkFatal(t, err)
+ defer headCommit.Free()
+
+ p := repo.Path()
+ p = strings.TrimSuffix(p, ".git")
+ p = strings.TrimSuffix(p, ".git/")
+
+ err = ioutil.WriteFile(path.Join(p, filename), []byte((content)), 0777)
+ checkFatal(t, err)
+
+ index, err := repo.Index()
+ checkFatal(t, err)
+ defer index.Free()
+
+ err = index.AddByPath(filename)
+ checkFatal(t, err)
+
+ newTreeOID, err := index.WriteTreeTo(repo)
+ checkFatal(t, err)
+
+ newTree, err := repo.LookupTree(newTreeOID)
+ checkFatal(t, err)
+ defer newTree.Free()
+
+ commitId, err := repo.CreateCommit("HEAD", signature(), signature(), fmt.Sprintf("add %s", filename), newTree, headCommit)
+ checkFatal(t, err)
+
+ commit, err := repo.LookupCommit(commitId)
+ checkFatal(t, err)
+
+ tree, err := commit.Tree()
+ checkFatal(t, err)
+
+ return commit, tree
+}
diff --git a/wrapper.c b/wrapper.c
index c4a5ff0..4308ae4 100644
--- a/wrapper.c
+++ b/wrapper.c
@@ -6,6 +6,12 @@
typedef int (*gogit_submodule_cbk)(git_submodule *sm, const char *name, void *payload);
+void _go_git_populate_apply_cb(git_apply_options *options)
+{
+ options->delta_cb = (git_apply_delta_cb)deltaApplyCallback;
+ options->hunk_cb = (git_apply_hunk_cb)hunkApplyCallback;
+}
+
void _go_git_populate_remote_cb(git_clone_options *opts)
{
opts->remote_cb = (git_remote_create_cb)remoteCreateCallback;