diff --git a/diff.go b/diff.go index 9b18f96..0c06dfc 100644 --- a/diff.go +++ b/diff.go @@ -980,6 +980,24 @@ func (v *Repository) ApplyDiff(diff *Diff, location ApplyLocation, opts *ApplyOp return nil } +// ApplyToTree applies a Diff to a Tree and returns the resulting image as an Index. +func (v *Repository) ApplyToTree(diff *Diff, tree *Tree, opts *ApplyOptions) (*Index, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + var indexPtr *C.git_index + cOpts := opts.toC() + ecode := C.git_apply_to_tree(&indexPtr, v.ptr, tree.cast_ptr, diff.ptr, cOpts) + runtime.KeepAlive(diff) + runtime.KeepAlive(tree) + runtime.KeepAlive(cOpts) + if ecode != 0 { + return nil, MakeGitError(ecode) + } + + return newIndexFromC(indexPtr, v), 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 diff --git a/diff_test.go b/diff_test.go index e440206..e2f810b 100644 --- a/diff_test.go +++ b/diff_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "path" + "reflect" "strings" "testing" ) @@ -463,6 +464,141 @@ func TestApplyDiffAddfile(t *testing.T) { }) } +func TestApplyToTree(t *testing.T) { + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + seedTestRepo(t, repo) + + commitA, treeA := addAndGetTree(t, repo, "file", "a") + defer commitA.Free() + defer treeA.Free() + commitB, treeB := addAndGetTree(t, repo, "file", "b") + defer commitB.Free() + defer treeB.Free() + commitC, treeC := addAndGetTree(t, repo, "file", "c") + defer commitC.Free() + defer treeC.Free() + + diffAB, err := repo.DiffTreeToTree(treeA, treeB, nil) + checkFatal(t, err) + + diffAC, err := repo.DiffTreeToTree(treeA, treeC, nil) + checkFatal(t, err) + + for _, tc := range []struct { + name string + tree *Tree + diff *Diff + applyHunkCallback ApplyHunkCallback + applyDeltaCallback ApplyDeltaCallback + error error + expectedDiff *Diff + }{ + { + name: "applying patch produces the same diff", + tree: treeA, + diff: diffAB, + expectedDiff: diffAB, + }, + { + name: "applying a conflicting patch errors", + tree: treeB, + diff: diffAC, + error: &GitError{ + Message: "hunk at line 1 did not apply", + Code: ErrApplyFail, + Class: ErrClassPatch, + }, + }, + { + name: "callbacks succeeding apply the diff", + tree: treeA, + diff: diffAB, + applyHunkCallback: func(*DiffHunk) (bool, error) { return true, nil }, + applyDeltaCallback: func(*DiffDelta) (bool, error) { return true, nil }, + expectedDiff: diffAB, + }, + { + name: "hunk callback returning false does not apply", + tree: treeA, + diff: diffAB, + applyHunkCallback: func(*DiffHunk) (bool, error) { return false, nil }, + }, + { + name: "hunk callback erroring fails the call", + tree: treeA, + diff: diffAB, + applyHunkCallback: func(*DiffHunk) (bool, error) { return true, errors.New("message dropped") }, + error: &GitError{ + Code: ErrGeneric, + Class: ErrClassInvalid, + }, + }, + { + name: "delta callback returning false does not apply", + tree: treeA, + diff: diffAB, + applyDeltaCallback: func(*DiffDelta) (bool, error) { return false, nil }, + }, + { + name: "delta callback erroring fails the call", + tree: treeA, + diff: diffAB, + applyDeltaCallback: func(*DiffDelta) (bool, error) { return true, errors.New("message dropped") }, + error: &GitError{ + Code: ErrGeneric, + Class: ErrClassInvalid, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + opts, err := DefaultApplyOptions() + checkFatal(t, err) + + opts.ApplyHunkCallback = tc.applyHunkCallback + opts.ApplyDeltaCallback = tc.applyDeltaCallback + + index, err := repo.ApplyToTree(tc.diff, tc.tree, opts) + if tc.error != nil { + if !reflect.DeepEqual(err, tc.error) { + t.Fatalf("expected error %q but got %q", tc.error, err) + } + + return + } + checkFatal(t, err) + + patchedTreeOID, err := index.WriteTreeTo(repo) + checkFatal(t, err) + + patchedTree, err := repo.LookupTree(patchedTreeOID) + checkFatal(t, err) + + patchedDiff, err := repo.DiffTreeToTree(tc.tree, patchedTree, nil) + checkFatal(t, err) + + appliedRaw, err := patchedDiff.ToBuf(DiffFormatPatch) + checkFatal(t, err) + + if tc.expectedDiff == nil { + if len(appliedRaw) > 0 { + t.Fatalf("expected no diff but got: %s", appliedRaw) + } + + return + } + + expectedDiff, err := tc.expectedDiff.ToBuf(DiffFormatPatch) + checkFatal(t, err) + + if string(expectedDiff) != string(appliedRaw) { + t.Fatalf("diffs do not match:\nexpected: %s\n\nactual: %s", expectedDiff, appliedRaw) + } + }) + } +} + // checkSecondFileStaged checks that there is a single file called "file2" uncommitted in the repo func checkSecondFileStaged(t *testing.T, repo *Repository) { opts := StatusOptions{ diff --git a/git.go b/git.go index 5acff0d..d3def5e 100644 --- a/git.go +++ b/git.go @@ -45,6 +45,7 @@ const ( ErrClassRevert ErrorClass = C.GITERR_REVERT ErrClassCallback ErrorClass = C.GITERR_CALLBACK ErrClassRebase ErrorClass = C.GITERR_REBASE + ErrClassPatch ErrorClass = C.GITERR_PATCH ) type ErrorCode int @@ -109,6 +110,8 @@ const ( ErrPassthrough ErrorCode = C.GIT_PASSTHROUGH // Signals end of iteration with iterator ErrIterOver ErrorCode = C.GIT_ITEROVER + // Patch application failed + ErrApplyFail ErrorCode = C.GIT_EAPPLYFAIL ) var (