diff options
author | Sami Hiltunen <shiltunen@gitlab.com> | 2020-10-20 10:47:32 +0300 |
---|---|---|
committer | Sami Hiltunen <shiltunen@gitlab.com> | 2020-11-25 20:07:10 +0300 |
commit | 83fc926987b86b692a0acacc2797b210fceb0c93 (patch) | |
tree | 23ffccac6695faf5ea21a0af3a351cfe3b35a5b4 /cmd | |
parent | d11228c283478b250edcf4a52e6a284bc488a169 (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')
-rw-r--r-- | cmd/gitaly-git2go/apply.go | 224 | ||||
-rw-r--r-- | cmd/gitaly-git2go/commit/commit.go | 5 | ||||
-rw-r--r-- | cmd/gitaly-git2go/main.go | 1 |
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(¶ms); 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{}, |