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:
Diffstat (limited to 'cmd/gitaly-git2go/merge.go')
-rw-r--r--cmd/gitaly-git2go/merge.go251
1 files changed, 251 insertions, 0 deletions
diff --git a/cmd/gitaly-git2go/merge.go b/cmd/gitaly-git2go/merge.go
new file mode 100644
index 000000000..4ab3ef9fd
--- /dev/null
+++ b/cmd/gitaly-git2go/merge.go
@@ -0,0 +1,251 @@
+//go:build static && system_libgit2
+
+package main
+
+import (
+ "context"
+ "encoding/gob"
+ "errors"
+ "flag"
+ "fmt"
+ "time"
+
+ git "github.com/libgit2/git2go/v33"
+ "gitlab.com/gitlab-org/gitaly/v15/cmd/gitaly-git2go/git2goutil"
+ "gitlab.com/gitlab-org/gitaly/v15/internal/git2go"
+)
+
+type mergeSubcommand struct{}
+
+func (cmd *mergeSubcommand) Flags() *flag.FlagSet {
+ flags := flag.NewFlagSet("merge", flag.ExitOnError)
+ return flags
+}
+
+func (cmd *mergeSubcommand) Run(_ context.Context, decoder *gob.Decoder, encoder *gob.Encoder) error {
+ var request git2go.MergeCommand
+ if err := decoder.Decode(&request); err != nil {
+ return err
+ }
+
+ if request.AuthorDate.IsZero() {
+ request.AuthorDate = time.Now()
+ }
+
+ commitID, err := merge(request)
+
+ return encoder.Encode(git2go.Result{
+ CommitID: commitID,
+ Err: git2go.SerializableError(err),
+ })
+}
+
+func merge(request git2go.MergeCommand) (string, error) {
+ repo, err := git2goutil.OpenRepository(request.Repository)
+ if err != nil {
+ return "", fmt.Errorf("could not open repository: %w", err)
+ }
+ defer repo.Free()
+
+ ours, err := lookupCommit(repo, request.Ours)
+ if err != nil {
+ return "", fmt.Errorf("ours commit lookup: %w", err)
+ }
+
+ theirs, err := lookupCommit(repo, request.Theirs)
+ if err != nil {
+ return "", fmt.Errorf("theirs commit lookup: %w", err)
+ }
+
+ mergeOpts, err := git.DefaultMergeOptions()
+ if err != nil {
+ return "", fmt.Errorf("could not create merge options: %w", err)
+ }
+ mergeOpts.RecursionLimit = git2go.MergeRecursionLimit
+
+ index, err := repo.MergeCommits(ours, theirs, &mergeOpts)
+ if err != nil {
+ return "", fmt.Errorf("could not merge commits: %w", err)
+ }
+ defer index.Free()
+
+ if index.HasConflicts() {
+ if !request.AllowConflicts {
+ conflictingFiles, err := getConflictingFiles(index)
+ if err != nil {
+ return "", fmt.Errorf("getting conflicting files: %w", err)
+ }
+
+ return "", git2go.ConflictingFilesError{
+ ConflictingFiles: conflictingFiles,
+ }
+ }
+
+ if err := resolveConflicts(repo, index); err != nil {
+ return "", fmt.Errorf("could not resolve conflicts: %w", err)
+ }
+ }
+
+ tree, err := index.WriteTreeTo(repo)
+ if err != nil {
+ return "", fmt.Errorf("could not write tree: %w", err)
+ }
+
+ author := git.Signature(git2go.NewSignature(request.AuthorName, request.AuthorMail, request.AuthorDate))
+ committer := author
+ if request.CommitterMail != "" {
+ committer = git.Signature(git2go.NewSignature(request.CommitterName, request.CommitterMail, request.CommitterDate))
+ }
+
+ var parents []*git.Oid
+ if request.Squash {
+ parents = []*git.Oid{ours.Id()}
+ } else {
+ parents = []*git.Oid{ours.Id(), theirs.Id()}
+ }
+ commit, err := repo.CreateCommitFromIds("", &author, &committer, request.Message, tree, parents...)
+ if err != nil {
+ return "", fmt.Errorf("could not create merge commit: %w", err)
+ }
+
+ return commit.String(), nil
+}
+
+func resolveConflicts(repo *git.Repository, index *git.Index) error {
+ // We need to get all conflicts up front as resolving conflicts as we
+ // iterate breaks the iterator.
+ indexConflicts, err := getConflicts(index)
+ if err != nil {
+ return err
+ }
+
+ for _, conflict := range indexConflicts {
+ if isConflictMergeable(conflict) {
+ merge, err := Merge(repo, conflict)
+ if err != nil {
+ return err
+ }
+
+ mergedBlob, err := repo.CreateBlobFromBuffer(merge.Contents)
+ if err != nil {
+ return err
+ }
+
+ mergedIndexEntry := git.IndexEntry{
+ Path: merge.Path,
+ Mode: git.Filemode(merge.Mode),
+ Id: mergedBlob,
+ }
+
+ if err := index.Add(&mergedIndexEntry); err != nil {
+ return err
+ }
+
+ if err := index.RemoveConflict(merge.Path); err != nil {
+ return err
+ }
+ } else {
+ if conflict.Their != nil {
+ // If a conflict has `Their` present, we add it back to the index
+ // as we want those changes to be part of the merge.
+ if err := index.Add(conflict.Their); err != nil {
+ return err
+ }
+
+ if err := index.RemoveConflict(conflict.Their.Path); err != nil {
+ return err
+ }
+ } else if conflict.Our != nil {
+ // If a conflict has `Our` present, remove its conflict as we
+ // don't want to include those changes.
+ if err := index.RemoveConflict(conflict.Our.Path); err != nil {
+ return err
+ }
+ } else {
+ // If conflict has no `Their` and `Our`, remove the conflict to
+ // mark it as resolved.
+ if err := index.RemoveConflict(conflict.Ancestor.Path); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ if index.HasConflicts() {
+ conflictingFiles, err := getConflictingFiles(index)
+ if err != nil {
+ return fmt.Errorf("getting conflicting files: %w", err)
+ }
+
+ return git2go.ConflictingFilesError{
+ ConflictingFiles: conflictingFiles,
+ }
+ }
+
+ return nil
+}
+
+func getConflictingFiles(index *git.Index) ([]string, error) {
+ conflicts, err := getConflicts(index)
+ if err != nil {
+ return nil, fmt.Errorf("getting conflicts: %w", err)
+ }
+
+ conflictingFiles := make([]string, 0, len(conflicts))
+ for _, conflict := range conflicts {
+ switch {
+ case conflict.Our != nil:
+ conflictingFiles = append(conflictingFiles, conflict.Our.Path)
+ case conflict.Ancestor != nil:
+ conflictingFiles = append(conflictingFiles, conflict.Ancestor.Path)
+ case conflict.Their != nil:
+ conflictingFiles = append(conflictingFiles, conflict.Their.Path)
+ default:
+ return nil, errors.New("invalid conflict")
+ }
+ }
+
+ return conflictingFiles, nil
+}
+
+func isConflictMergeable(conflict git.IndexConflict) bool {
+ conflictIndexEntriesCount := 0
+
+ if conflict.Their != nil {
+ conflictIndexEntriesCount++
+ }
+
+ if conflict.Our != nil {
+ conflictIndexEntriesCount++
+ }
+
+ if conflict.Ancestor != nil {
+ conflictIndexEntriesCount++
+ }
+
+ return conflictIndexEntriesCount >= 2
+}
+
+func getConflicts(index *git.Index) ([]git.IndexConflict, error) {
+ var conflicts []git.IndexConflict
+
+ iterator, err := index.ConflictIterator()
+ if err != nil {
+ return nil, err
+ }
+ defer iterator.Free()
+
+ for {
+ conflict, err := iterator.Next()
+ if err != nil {
+ if git.IsErrorCode(err, git.ErrorCodeIterOver) {
+ break
+ }
+ return nil, err
+ }
+
+ conflicts = append(conflicts, conflict)
+ }
+
+ return conflicts, nil
+}