Repository: libgit2/git2go
Pull request #657 (2020-10-20 18:30:38)

apply: Add bindings for git_apply_to_tree
Adds bindings to the git_apply_to_tree function that allows applying
a diff directly to a tree.

Should this get merged, when do you anticipate a new release that includes these changes?


Patch:
diff --git a/diff.go b/diff.go
index f65a2dd..76838e7 100644
--- a/diff.go
+++ b/diff.go
@@ -991,6 +991,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/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 (


Test patch:
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{