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 +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;