Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSami Hiltunen <shiltunen@gitlab.com>2020-10-20 10:47:32 +0300
committerSami Hiltunen <shiltunen@gitlab.com>2020-11-25 20:07:10 +0300
commit83fc926987b86b692a0acacc2797b210fceb0c93 (patch)
tree23ffccac6695faf5ea21a0af3a351cfe3b35a5b4 /cmd/gitaly-git2go
parentd11228c283478b250edcf4a52e6a284bc488a169 (diff)
gitaly-git2go apply subcommand
Implements apply subcommand for gitaly-git2go that allows for applying patches from diffs. Similar to 'git am', three-way merge is attempted as a fallback if the patch does not apply cleanly.
Diffstat (limited to 'cmd/gitaly-git2go')
-rw-r--r--cmd/gitaly-git2go/apply.go224
-rw-r--r--cmd/gitaly-git2go/commit/commit.go5
-rw-r--r--cmd/gitaly-git2go/main.go1
3 files changed, 228 insertions, 2 deletions
diff --git a/cmd/gitaly-git2go/apply.go b/cmd/gitaly-git2go/apply.go
new file mode 100644
index 000000000..c4c6cd1d1
--- /dev/null
+++ b/cmd/gitaly-git2go/apply.go
@@ -0,0 +1,224 @@
+// +build static,system_libgit2
+
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/gob"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+
+ git "github.com/libgit2/git2go/v30"
+ "gitlab.com/gitlab-org/gitaly/internal/git2go"
+)
+
+type patchIterator struct {
+ value git2go.Patch
+ decoder *gob.Decoder
+ error error
+}
+
+func (iter *patchIterator) Next() bool {
+ if err := iter.decoder.Decode(&iter.value); err != nil {
+ if !errors.Is(err, io.EOF) {
+ iter.error = fmt.Errorf("decode patch: %w", err)
+ }
+
+ return false
+ }
+
+ return true
+}
+
+func (iter *patchIterator) Value() git2go.Patch { return iter.value }
+
+func (iter *patchIterator) Err() error { return iter.error }
+
+type applySubcommand struct {
+ gitBinaryPath string
+}
+
+func (cmd *applySubcommand) Flags() *flag.FlagSet {
+ fs := flag.NewFlagSet("apply", flag.ExitOnError)
+ fs.StringVar(&cmd.gitBinaryPath, "git-binary-path", "", "Path to the Git binary.")
+ return fs
+}
+
+// Run runs the subcommand.
+func (cmd *applySubcommand) Run(ctx context.Context, stdin io.Reader, stdout io.Writer) error {
+ decoder := gob.NewDecoder(stdin)
+
+ var params git2go.ApplyParams
+ if err := decoder.Decode(&params); err != nil {
+ return fmt.Errorf("decode params: %w", err)
+ }
+
+ params.Patches = &patchIterator{decoder: decoder}
+ commitID, err := cmd.apply(ctx, params)
+ return gob.NewEncoder(stdout).Encode(git2go.Result{
+ CommitID: commitID,
+ Error: git2go.SerializableError(err),
+ })
+}
+
+func (cmd *applySubcommand) apply(ctx context.Context, params git2go.ApplyParams) (string, error) {
+ repo, err := git.OpenRepository(params.Repository)
+ if err != nil {
+ return "", fmt.Errorf("open repository: %w", err)
+ }
+
+ commitOID, err := git.NewOid(params.ParentCommit)
+ if err != nil {
+ return "", fmt.Errorf("parse parent commit oid: %w", err)
+ }
+
+ committer := git.Signature(params.Committer)
+ for i := 0; params.Patches.Next(); i++ {
+ commitOID, err = cmd.applyPatch(ctx, repo, &committer, commitOID, params.Patches.Value())
+ if err != nil {
+ return "", fmt.Errorf("apply patch [%d]: %w", i+1, err)
+ }
+ }
+
+ if err := params.Patches.Err(); err != nil {
+ return "", fmt.Errorf("reading patches: %w", err)
+ }
+
+ return commitOID.String(), nil
+}
+
+func (cmd *applySubcommand) applyPatch(
+ ctx context.Context,
+ repo *git.Repository,
+ committer *git.Signature,
+ parentCommitOID *git.Oid,
+ patch git2go.Patch,
+) (*git.Oid, error) {
+ parentCommit, err := repo.LookupCommit(parentCommitOID)
+ if err != nil {
+ return nil, fmt.Errorf("lookup commit: %w", err)
+ }
+
+ parentTree, err := parentCommit.Tree()
+ if err != nil {
+ return nil, fmt.Errorf("lookup tree: %w", err)
+ }
+
+ diff, err := git.DiffFromBuffer(patch.Diff, repo)
+ if err != nil {
+ return nil, fmt.Errorf("diff from buffer: %w", err)
+ }
+
+ patchedIndex, err := repo.ApplyToTree(diff, parentTree, nil)
+ if err != nil {
+ if !git.IsErrorCode(err, git.ErrApplyFail) {
+ return nil, fmt.Errorf("apply to tree: %w", err)
+ }
+
+ patchedIndex, err = cmd.threeWayMerge(ctx, repo, parentTree, diff, patch.Diff)
+ if err != nil {
+ return nil, fmt.Errorf("three way merge: %w", err)
+ }
+ }
+
+ patchedTree, err := patchedIndex.WriteTreeTo(repo)
+ if err != nil {
+ return nil, fmt.Errorf("write patched tree: %w", err)
+ }
+
+ author := git.Signature(patch.Author)
+ patchedCommitOID, err := repo.CreateCommitFromIds("", &author, committer, patch.Message, patchedTree, parentCommitOID)
+ if err != nil {
+ return nil, fmt.Errorf("create commit: %w", err)
+ }
+
+ return patchedCommitOID, nil
+}
+
+// threeWayMerge attempts a three-way merge as a fallback if applying the patch fails.
+// Fallback three-way merge is only possible if the patch records the pre-image blobs
+// and the repository contains them. It works as follows:
+//
+// 1. An index that contains only the pre-image blobs of the patch is built. This is done
+// by calling `git apply --build-fake-ancestor`. The tree of the index is the fake
+// ancestor tree.
+// 2. The fake ancestor tree is patched to produce the post-image tree of the patch.
+// 3. Three-way merge is performed with fake ancestor tree as the common ancestor, the
+// base commit's tree as our tree and the patched fake ancestor tree as their tree.
+func (cmd *applySubcommand) threeWayMerge(
+ ctx context.Context,
+ repo *git.Repository,
+ our *git.Tree,
+ diff *git.Diff,
+ rawDiff []byte,
+) (*git.Index, error) {
+ ancestorTree, err := cmd.buildFakeAncestor(ctx, repo, rawDiff)
+ if err != nil {
+ return nil, fmt.Errorf("build fake ancestor: %w", err)
+ }
+
+ patchedAncestorIndex, err := repo.ApplyToTree(diff, ancestorTree, nil)
+ if err != nil {
+ return nil, fmt.Errorf("patch fake ancestor: %w", err)
+ }
+
+ patchedAncestorTreeOID, err := patchedAncestorIndex.WriteTreeTo(repo)
+ if err != nil {
+ return nil, fmt.Errorf("write patched fake ancestor: %w", err)
+ }
+
+ patchedTree, err := repo.LookupTree(patchedAncestorTreeOID)
+ if err != nil {
+ return nil, fmt.Errorf("lookup patched tree: %w", err)
+ }
+
+ patchedIndex, err := repo.MergeTrees(ancestorTree, our, patchedTree, nil)
+ if err != nil {
+ return nil, fmt.Errorf("merge trees: %w", err)
+ }
+
+ if patchedIndex.HasConflicts() {
+ return nil, git2go.ErrMergeConflict
+ }
+
+ return patchedIndex, nil
+}
+
+func (cmd *applySubcommand) buildFakeAncestor(ctx context.Context, repo *git.Repository, diff []byte) (*git.Tree, error) {
+ dir, err := ioutil.TempDir("", "gitaly-git2go")
+ if err != nil {
+ return nil, fmt.Errorf("create temporary directory: %w", err)
+ }
+ defer os.RemoveAll(dir)
+
+ file := filepath.Join(dir, "patch-merge-index")
+ gitCmd := exec.CommandContext(ctx, cmd.gitBinaryPath, "--git-dir", repo.Path(), "apply", "--build-fake-ancestor", file)
+ gitCmd.Stdin = bytes.NewReader(diff)
+ if _, err := gitCmd.Output(); err != nil {
+ var exitError *exec.ExitError
+ if errors.As(err, &exitError) {
+ err = fmt.Errorf("%w, stderr: %q", err, exitError.Stderr)
+ }
+
+ return nil, fmt.Errorf("git: %w", err)
+ }
+
+ fakeAncestor, err := git.OpenIndex(file)
+ if err != nil {
+ return nil, fmt.Errorf("open fake ancestor index: %w", err)
+ }
+
+ ancestorTreeOID, err := fakeAncestor.WriteTreeTo(repo)
+ if err != nil {
+ return nil, fmt.Errorf("write fake ancestor tree: %w", err)
+ }
+
+ return repo.LookupTree(ancestorTreeOID)
+}
diff --git a/cmd/gitaly-git2go/commit/commit.go b/cmd/gitaly-git2go/commit/commit.go
index 90862e1a7..c8051c7a2 100644
--- a/cmd/gitaly-git2go/commit/commit.go
+++ b/cmd/gitaly-git2go/commit/commit.go
@@ -73,8 +73,9 @@ func commit(ctx context.Context, params git2go.CommitParams) (string, error) {
return "", fmt.Errorf("write tree: %w", err)
}
- signature := git.Signature(params.Author)
- commitID, err := repo.CreateCommitFromIds("", &signature, &signature, params.Message, treeOID, parents...)
+ author := git.Signature(params.Author)
+ committer := git.Signature(params.Committer)
+ commitID, err := repo.CreateCommitFromIds("", &author, &committer, params.Message, treeOID, parents...)
if err != nil {
if git.IsErrorClass(err, git.ErrClassInvalid) {
return "", git2go.InvalidArgumentError(err.Error())
diff --git a/cmd/gitaly-git2go/main.go b/cmd/gitaly-git2go/main.go
index 04eab4dff..82dad85a9 100644
--- a/cmd/gitaly-git2go/main.go
+++ b/cmd/gitaly-git2go/main.go
@@ -18,6 +18,7 @@ type subcmd interface {
}
var subcommands = map[string]subcmd{
+ "apply": &applySubcommand{},
"commit": commitSubcommand{},
"conflicts": &conflicts.Subcommand{},
"merge": &mergeSubcommand{},