Skip to content

Commit 2870fab

Browse files
apply: Add bindings for git_apply_to_tree (#657) (#665)
Adds bindings to the git_apply_to_tree function that allows applying a diff directly to a tree. (cherry picked from commit 10d5ebf) Co-authored-by: Sami Hiltunen <github@hiltunen.io>
1 parent 6badd3d commit 2870fab

File tree

3 files changed

+157
-0
lines changed

3 files changed

+157
-0
lines changed

diff.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,24 @@ func (v *Repository) ApplyDiff(diff *Diff, location ApplyLocation, opts *ApplyOp
980980
return nil
981981
}
982982

983+
// ApplyToTree applies a Diff to a Tree and returns the resulting image as an Index.
984+
func (v *Repository) ApplyToTree(diff *Diff, tree *Tree, opts *ApplyOptions) (*Index, error) {
985+
runtime.LockOSThread()
986+
defer runtime.UnlockOSThread()
987+
988+
var indexPtr *C.git_index
989+
cOpts := opts.toC()
990+
ecode := C.git_apply_to_tree(&indexPtr, v.ptr, tree.cast_ptr, diff.ptr, cOpts)
991+
runtime.KeepAlive(diff)
992+
runtime.KeepAlive(tree)
993+
runtime.KeepAlive(cOpts)
994+
if ecode != 0 {
995+
return nil, MakeGitError(ecode)
996+
}
997+
998+
return newIndexFromC(indexPtr, v), nil
999+
}
1000+
9831001
// DiffFromBuffer reads the contents of a git patch file into a Diff object.
9841002
//
9851003
// The diff object produced is similar to the one that would be produced if you

diff_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"io/ioutil"
77
"path"
8+
"reflect"
89
"strings"
910
"testing"
1011
)
@@ -463,6 +464,141 @@ func TestApplyDiffAddfile(t *testing.T) {
463464
})
464465
}
465466

467+
func TestApplyToTree(t *testing.T) {
468+
repo := createTestRepo(t)
469+
defer cleanupTestRepo(t, repo)
470+
471+
seedTestRepo(t, repo)
472+
473+
commitA, treeA := addAndGetTree(t, repo, "file", "a")
474+
defer commitA.Free()
475+
defer treeA.Free()
476+
commitB, treeB := addAndGetTree(t, repo, "file", "b")
477+
defer commitB.Free()
478+
defer treeB.Free()
479+
commitC, treeC := addAndGetTree(t, repo, "file", "c")
480+
defer commitC.Free()
481+
defer treeC.Free()
482+
483+
diffAB, err := repo.DiffTreeToTree(treeA, treeB, nil)
484+
checkFatal(t, err)
485+
486+
diffAC, err := repo.DiffTreeToTree(treeA, treeC, nil)
487+
checkFatal(t, err)
488+
489+
for _, tc := range []struct {
490+
name string
491+
tree *Tree
492+
diff *Diff
493+
applyHunkCallback ApplyHunkCallback
494+
applyDeltaCallback ApplyDeltaCallback
495+
error error
496+
expectedDiff *Diff
497+
}{
498+
{
499+
name: "applying patch produces the same diff",
500+
tree: treeA,
501+
diff: diffAB,
502+
expectedDiff: diffAB,
503+
},
504+
{
505+
name: "applying a conflicting patch errors",
506+
tree: treeB,
507+
diff: diffAC,
508+
error: &GitError{
509+
Message: "hunk at line 1 did not apply",
510+
Code: ErrApplyFail,
511+
Class: ErrClassPatch,
512+
},
513+
},
514+
{
515+
name: "callbacks succeeding apply the diff",
516+
tree: treeA,
517+
diff: diffAB,
518+
applyHunkCallback: func(*DiffHunk) (bool, error) { return true, nil },
519+
applyDeltaCallback: func(*DiffDelta) (bool, error) { return true, nil },
520+
expectedDiff: diffAB,
521+
},
522+
{
523+
name: "hunk callback returning false does not apply",
524+
tree: treeA,
525+
diff: diffAB,
526+
applyHunkCallback: func(*DiffHunk) (bool, error) { return false, nil },
527+
},
528+
{
529+
name: "hunk callback erroring fails the call",
530+
tree: treeA,
531+
diff: diffAB,
532+
applyHunkCallback: func(*DiffHunk) (bool, error) { return true, errors.New("message dropped") },
533+
error: &GitError{
534+
Code: ErrGeneric,
535+
Class: ErrClassInvalid,
536+
},
537+
},
538+
{
539+
name: "delta callback returning false does not apply",
540+
tree: treeA,
541+
diff: diffAB,
542+
applyDeltaCallback: func(*DiffDelta) (bool, error) { return false, nil },
543+
},
544+
{
545+
name: "delta callback erroring fails the call",
546+
tree: treeA,
547+
diff: diffAB,
548+
applyDeltaCallback: func(*DiffDelta) (bool, error) { return true, errors.New("message dropped") },
549+
error: &GitError{
550+
Code: ErrGeneric,
551+
Class: ErrClassInvalid,
552+
},
553+
},
554+
} {
555+
t.Run(tc.name, func(t *testing.T) {
556+
opts, err := DefaultApplyOptions()
557+
checkFatal(t, err)
558+
559+
opts.ApplyHunkCallback = tc.applyHunkCallback
560+
opts.ApplyDeltaCallback = tc.applyDeltaCallback
561+
562+
index, err := repo.ApplyToTree(tc.diff, tc.tree, opts)
563+
if tc.error != nil {
564+
if !reflect.DeepEqual(err, tc.error) {
565+
t.Fatalf("expected error %q but got %q", tc.error, err)
566+
}
567+
568+
return
569+
}
570+
checkFatal(t, err)
571+
572+
patchedTreeOID, err := index.WriteTreeTo(repo)
573+
checkFatal(t, err)
574+
575+
patchedTree, err := repo.LookupTree(patchedTreeOID)
576+
checkFatal(t, err)
577+
578+
patchedDiff, err := repo.DiffTreeToTree(tc.tree, patchedTree, nil)
579+
checkFatal(t, err)
580+
581+
appliedRaw, err := patchedDiff.ToBuf(DiffFormatPatch)
582+
checkFatal(t, err)
583+
584+
if tc.expectedDiff == nil {
585+
if len(appliedRaw) > 0 {
586+
t.Fatalf("expected no diff but got: %s", appliedRaw)
587+
}
588+
589+
return
590+
}
591+
592+
expectedDiff, err := tc.expectedDiff.ToBuf(DiffFormatPatch)
593+
checkFatal(t, err)
594+
595+
if string(expectedDiff) != string(appliedRaw) {
596+
t.Fatalf("diffs do not match:\nexpected: %s\n\nactual: %s", expectedDiff, appliedRaw)
597+
}
598+
})
599+
}
600+
}
601+
466602
// checkSecondFileStaged checks that there is a single file called "file2" uncommitted in the repo
467603
func checkSecondFileStaged(t *testing.T, repo *Repository) {
468604
opts := StatusOptions{

git.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const (
4545
ErrClassRevert ErrorClass = C.GITERR_REVERT
4646
ErrClassCallback ErrorClass = C.GITERR_CALLBACK
4747
ErrClassRebase ErrorClass = C.GITERR_REBASE
48+
ErrClassPatch ErrorClass = C.GITERR_PATCH
4849
)
4950

5051
type ErrorCode int
@@ -109,6 +110,8 @@ const (
109110
ErrPassthrough ErrorCode = C.GIT_PASSTHROUGH
110111
// Signals end of iteration with iterator
111112
ErrIterOver ErrorCode = C.GIT_ITEROVER
113+
// Patch application failed
114+
ErrApplyFail ErrorCode = C.GIT_EAPPLYFAIL
112115
)
113116

114117
var (

0 commit comments

Comments
 (0)