diff options
author | Paul Okstad <pokstad@gitlab.com> | 2020-10-16 09:57:59 +0300 |
---|---|---|
committer | Paul Okstad <pokstad@gitlab.com> | 2020-10-17 00:35:08 +0300 |
commit | 73a4037cb51c1f2febebdd2b7bbf3ae0f59db43a (patch) | |
tree | 1c2cc24749eac71918e3d59ef91eb27a95d9c73f | |
parent | ac6f26f20a651168007f8621111e1b75c3c18243 (diff) |
Subcommand for resolve conflicts
-rw-r--r-- | cmd/gitaly-git2go/resolve_conflicts.go | 194 | ||||
-rw-r--r-- | internal/git2go/resolve_conflicts.go | 52 | ||||
-rw-r--r-- | internal/gitaly/service/conflicts/resolve_conflicts.go | 74 |
3 files changed, 320 insertions, 0 deletions
diff --git a/cmd/gitaly-git2go/resolve_conflicts.go b/cmd/gitaly-git2go/resolve_conflicts.go new file mode 100644 index 000000000..3c8bf6eed --- /dev/null +++ b/cmd/gitaly-git2go/resolve_conflicts.go @@ -0,0 +1,194 @@ +// +build static,system_libgit2 + +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "strings" + "time" + + git "github.com/libgit2/git2go/v30" + "gitlab.com/gitlab-org/gitaly/internal/git/conflict" + "gitlab.com/gitlab-org/gitaly/internal/git2go" +) + +type resolveSubcommand struct { + request string +} + +func (cmd *resolveSubcommand) Flags() *flag.FlagSet { + flags := flag.NewFlagSet("resolve", flag.ExitOnError) + flags.StringVar(&cmd.request, "request", "", "git2go.MergeCommand") + return flags +} + +func (cmd resolveSubcommand) Run() error { + request, err := git2go.ResolveCommandFromSerialized(cmd.request) + if err != nil { + return err + } + + if request.AuthorDate.IsZero() { + request.AuthorDate = time.Now() + } + + repo, err := git.OpenRepository(request.Repository) + if err != nil { + return fmt.Errorf("could not open repository: %w", err) + } + + ours, err := lookupCommit(repo, request.Ours) + if err != nil { + return fmt.Errorf("could not lookup commit %q: %w", request.Ours, err) + } + + theirs, err := lookupCommit(repo, request.Theirs) + if err != nil { + return fmt.Errorf("could not lookup commit %q: %w", request.Theirs, err) + } + + index, err := repo.MergeCommits(ours, theirs, nil) + if err != nil { + return fmt.Errorf("could not merge commits: %w", err) + } + + ci, err := index.ConflictIterator() + if err != nil { + return err + } + + type paths struct { + theirs, ours string + } + conflicts := map[paths]git.IndexConflict{} + + for c, err := ci.Next(); err != nil; c, err = ci.Next() { + if c.Our.Path == "" || c.Their.Path == "" { + return errors.New("conflict side missing") + } + + k := paths{ + theirs: c.Their.Path, + ours: c.Our.Path, + } + conflicts[k] = c + } + + for _, r := range request.Resolutions { + c, ok := conflicts[paths{ + theirs: r.OldPath, + ours: r.NewPath, + }] + if !ok { + continue + } + + odb, err := repo.Odb() + if err != nil { + return err + } + + mfr, err := mergeFileResult(odb, c) + if err != nil { + return err + } + + f, err := conflict.Parse( + bytes.NewReader(mfr.Contents), + c.Our.Path, + c.Their.Path, + c.Ancestor.Path, + ) + if err != nil { + return err + } + + resolvedBlob, err := f.Resolve(r) + if err != nil { + return err + } + + resolvedBlobOID, err := odb.Write(resolvedBlob, git.ObjectBlob) + if err != nil { + return err + } + + ourResolvedEntry := *c.Our // copy by value + ourResolvedEntry.Id = resolvedBlobOID + if err := index.Add(&ourResolvedEntry); err != nil { + return err + } + + // TODO: verify we need to remove conflict + if err := index.RemoveConflict(ourResolvedEntry.Path); err != nil { + return err + } + } + + if index.HasConflicts() { + ci, err := index.ConflictIterator() + if err != nil { + return err + } + + var conflictPaths []string + for { + c, err := ci.Next() + if err != nil { + return err + } + conflictPaths = append(conflictPaths, c.Ancestor.Path) + } + + return fmt.Errorf( + "Missing resolutions for the following files: %s", + strings.Join(conflictPaths, ", "), + ) + } + + return nil +} + +func mergeFileResult(odb *git.Odb, c git.IndexConflict) (*git.MergeFileResult, error) { + ancestorBlob, err := odb.Read(c.Ancestor.Id) + if err != nil { + return nil, err + } + + ourBlob, err := odb.Read(c.Our.Id) + if err != nil { + return nil, err + } + + theirBlob, err := odb.Read(c.Their.Id) + if err != nil { + return nil, err + } + + mfr, err := git.MergeFile( + git.MergeFileInput{ + Path: c.Ancestor.Path, + Mode: uint(c.Ancestor.Mode), + Contents: ancestorBlob.Data(), + }, + git.MergeFileInput{ + Path: c.Our.Path, + Mode: uint(c.Our.Mode), + Contents: ourBlob.Data(), + }, + git.MergeFileInput{ + Path: c.Their.Path, + Mode: uint(c.Their.Mode), + Contents: theirBlob.Data(), + }, + nil, + ) + if err != nil { + return nil, err + } + + return mfr, nil +} diff --git a/internal/git2go/resolve_conflicts.go b/internal/git2go/resolve_conflicts.go new file mode 100644 index 000000000..5eb2535b2 --- /dev/null +++ b/internal/git2go/resolve_conflicts.go @@ -0,0 +1,52 @@ +package git2go + +import ( + "context" + "fmt" + + "gitlab.com/gitlab-org/gitaly/internal/git/conflict" + "gitlab.com/gitlab-org/gitaly/internal/gitaly/config" +) + +type ResolveCommand struct { + MergeCommand `json:"merge_command"` + Resolutions []conflict.Resolution `json:"conflict_files"` +} + +type ResolveResult struct{} + +func ResolveCommandFromSerialized(serialized string) (ResolveCommand, error) { + var request ResolveCommand + if err := deserialize(serialized, &request); err != nil { + return ResolveCommand{}, err + } + + if err := request.verify(); err != nil { + return ResolveCommand{}, fmt.Errorf("resolve: %w: %s", ErrInvalidArgument, err.Error()) + } + + return request, nil +} + +func (r ResolveCommand) Run(ctx context.Context, cfg config.Cfg) (ResolveResult, error) { + if err := r.verify(); err != nil { + return ResolveResult{}, fmt.Errorf("resolve: %w: %s", ErrInvalidArgument, err.Error()) + } + + serialized, err := serialize(r) + if err != nil { + return ResolveResult{}, err + } + + stdout, err := run(ctx, cfg, "resolve", serialized) + if err != nil { + return ResolveResult{}, err + } + + var response ResolveResult + if err := deserialize(stdout, &response); err != nil { + return ResolveResult{}, err + } + + return response, nil +} diff --git a/internal/gitaly/service/conflicts/resolve_conflicts.go b/internal/gitaly/service/conflicts/resolve_conflicts.go index d8734ef8d..9b283f475 100644 --- a/internal/gitaly/service/conflicts/resolve_conflicts.go +++ b/internal/gitaly/service/conflicts/resolve_conflicts.go @@ -1,9 +1,17 @@ package conflicts import ( + "bytes" + "encoding/json" "fmt" + "io" + "gitlab.com/gitlab-org/gitaly/client" + "gitlab.com/gitlab-org/gitaly/internal/git" + "gitlab.com/gitlab-org/gitaly/internal/git/repository" + "gitlab.com/gitlab-org/gitaly/internal/gitaly/config" "gitlab.com/gitlab-org/gitaly/internal/gitaly/rubyserver" + "gitlab.com/gitlab-org/gitaly/internal/metadata/featureflag" "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -24,6 +32,10 @@ func (s *server) ResolveConflicts(stream gitalypb.ConflictsService_ResolveConfli return status.Errorf(codes.InvalidArgument, "ResolveConflicts: %v", err) } + if featureflag.IsEnabled(stream.Context(), featureflag.GoResolveConflicts) { + return s.resolveConflicts(header, stream) + } + ctx := stream.Context() client, err := s.ruby.ConflictsServiceClient(ctx) if err != nil { @@ -89,3 +101,65 @@ func validateResolveConflictsHeader(header *gitalypb.ResolveConflictsRequestHead return nil } + +type conflictFile struct { + OldPath string `json:"old_path"` + NewPath string `json:"new_path"` + Sections map[string]string `json:"sections"` +} + +func cfgContainsRepo(cfg config.Cfg, r repository.GitRepo) bool { + for _, s := range cfg.Storages { + if r.GetStorageName() == s.Name { + return true + } + } + return false +} + +func (s *server) resolveConflicts(header *gitalypb.ResolveConflictsRequestHeader, stream gitalypb.ConflictsService_ResolveConflictsServer) error { + b := bytes.NewBuffer(nil) + for { + req, err := stream.Recv() + switch err { + case io.EOF: + return nil + case nil: + // do nothing, continue + default: + return err + } + + if _, err := b.Write(req.GetFilesJson()); err != nil { + return err + } + } + + var conflicts []map[string]conflictFile + if err := json.NewDecoder(b).Decode(&conflicts); err != nil { + return err + } + + srcRepo := git.NewRepository(header.GetRepository()) + + var dstRepo git.Repository + switch dst := header.GetTargetRepository(); { + case dst == nil: + dstRepo = srcRepo + case cfgContainsRepo(s.cfg, dst): + dstRepo = git.NewRepository(dst) + default: + var err error + dstRepo, err = git.NewRemoteRepository(stream.Context(), dst, client.NewPool()) + if err != nil { + return err + } + } + + targetOID, err := dstRepo.ResolveRefish(ctx, string(header.TargetBranch)) + if err != nil { + return err + } + + return nil +} |