diff options
author | Ahmad Sherif <ahmad.m.sherif@gmail.com> | 2017-12-20 23:29:15 +0300 |
---|---|---|
committer | Ahmad Sherif <ahmad.m.sherif@gmail.com> | 2017-12-20 23:29:15 +0300 |
commit | ae6920282ce5fa11580d403a7d3c1fe13dc27c26 (patch) | |
tree | 14d81421729cfaca41f339c323305befdeb9c7af | |
parent | 3e18525956d4cc6b205ac1dbbd7519b87fb96a62 (diff) | |
parent | 88ea5bc21f5b161a424d3b31d530f415bfdbac4e (diff) |
Merge branch '788-server-implementation-conflict-resolver' into 'master'
Resolve "Server Implementation: Conflict Resolver"
Closes #788
See merge request gitlab-org/gitaly!470
-rw-r--r-- | CHANGELOG.md | 4 | ||||
-rw-r--r-- | internal/service/conflicts/list_conflict_files.go | 57 | ||||
-rw-r--r-- | internal/service/conflicts/list_conflict_files_test.go | 191 | ||||
-rw-r--r-- | internal/service/conflicts/resolve_conflicts.go | 91 | ||||
-rw-r--r-- | internal/service/conflicts/resolve_conflicts_test.go | 322 | ||||
-rw-r--r-- | internal/service/conflicts/resolver.go | 14 | ||||
-rw-r--r-- | internal/service/conflicts/testhelper_test.go | 72 | ||||
-rw-r--r-- | ruby/lib/gitaly_server/conflicts_service.rb | 87 |
8 files changed, 824 insertions, 14 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 44bdda0a1..fb0926ad9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ UNRELEASED +- Implement ConflictsService.ResolveConflicts RPC + https://gitlab.com/gitlab-org/gitaly/merge_requests/470 +- Implement ConflictsService.ListConflictFiles RPC + https://gitlab.com/gitlab-org/gitaly/merge_requests/470 - Implement RemoteService.RemoveRemote RPC https://gitlab.com/gitlab-org/gitaly/merge_requests/490 - Implement RemoteService.AddRemote RPC diff --git a/internal/service/conflicts/list_conflict_files.go b/internal/service/conflicts/list_conflict_files.go new file mode 100644 index 000000000..29ac0d7fc --- /dev/null +++ b/internal/service/conflicts/list_conflict_files.go @@ -0,0 +1,57 @@ +package conflicts + +import ( + "fmt" + + pb "gitlab.com/gitlab-org/gitaly-proto/go" + "gitlab.com/gitlab-org/gitaly/internal/rubyserver" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" +) + +func (s *server) ListConflictFiles(in *pb.ListConflictFilesRequest, stream pb.ConflictsService_ListConflictFilesServer) error { + ctx := stream.Context() + + if err := validateListConflictFilesRequest(in); err != nil { + return grpc.Errorf(codes.InvalidArgument, "ListConflictFiles: %v", err) + } + + client, err := s.ConflictsServiceClient(ctx) + if err != nil { + return err + } + + clientCtx, err := rubyserver.SetHeaders(ctx, in.GetRepository()) + if err != nil { + return err + } + + rubyStream, err := client.ListConflictFiles(clientCtx, in) + if err != nil { + return err + } + + return rubyserver.Proxy(func() error { + resp, err := rubyStream.Recv() + if err != nil { + md := rubyStream.Trailer() + stream.SetTrailer(md) + return err + } + return stream.Send(resp) + }) +} + +func validateListConflictFilesRequest(in *pb.ListConflictFilesRequest) error { + if in.GetRepository() == nil { + return fmt.Errorf("empty Repository") + } + if in.GetOurCommitOid() == "" { + return fmt.Errorf("empty OurCommitOid") + } + if in.GetTheirCommitOid() == "" { + return fmt.Errorf("empty TheirCommitOid") + } + + return nil +} diff --git a/internal/service/conflicts/list_conflict_files_test.go b/internal/service/conflicts/list_conflict_files_test.go new file mode 100644 index 000000000..184cc3312 --- /dev/null +++ b/internal/service/conflicts/list_conflict_files_test.go @@ -0,0 +1,191 @@ +package conflicts + +import ( + "io" + "testing" + + "google.golang.org/grpc/codes" + + "github.com/stretchr/testify/require" + + pb "gitlab.com/gitlab-org/gitaly-proto/go" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" +) + +type conflictFile struct { + header *pb.ConflictFileHeader + content []byte +} + +func TestSuccessfulListConflictFilesRequest(t *testing.T) { + server, serverSocketPath := runConflictsServer(t) + defer server.Stop() + + client, conn := NewConflictsClient(t, serverSocketPath) + defer conn.Close() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + ourCommitOid := "0b4bc9a49b562e85de7cc9e834518ea6828729b9" + theirCommitOid := "bb5206fee213d983da88c47f9cf4cc6caf9c66dc" + conflictContent := `<<<<<<< files/ruby/feature.rb +class Feature + def foo + puts 'bar' + end +======= +# This file was changed in feature branch +# We put different code here to make merge conflict +class Conflict +>>>>>>> files/ruby/feature.rb +end +` + + ctx, cancel := testhelper.Context() + defer cancel() + + request := &pb.ListConflictFilesRequest{ + Repository: testRepo, + OurCommitOid: ourCommitOid, + TheirCommitOid: theirCommitOid, + } + + c, err := client.ListConflictFiles(ctx, request) + if err != nil { + t.Fatal(err) + } + + files := getConflictFiles(t, c) + require.Len(t, files, 1) + + file := files[0] + require.Equal(t, ourCommitOid, file.header.CommitOid) + require.Equal(t, int32(0100644), file.header.OurMode) + require.Equal(t, "files/ruby/feature.rb", string(file.header.OurPath)) + require.Equal(t, "files/ruby/feature.rb", string(file.header.TheirPath)) + require.Equal(t, conflictContent, string(file.content)) +} + +func TestFailedListConflictFilesRequestDueToConflictSideMissing(t *testing.T) { + server, serverSocketPath := runConflictsServer(t) + defer server.Stop() + + client, conn := NewConflictsClient(t, serverSocketPath) + defer conn.Close() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + ourCommitOid := "eb227b3e214624708c474bdab7bde7afc17cefcc" // conflict-missing-side + theirCommitOid := "824be604a34828eb682305f0d963056cfac87b2d" + + ctx, cancel := testhelper.Context() + defer cancel() + + request := &pb.ListConflictFilesRequest{ + Repository: testRepo, + OurCommitOid: ourCommitOid, + TheirCommitOid: theirCommitOid, + } + + c, _ := client.ListConflictFiles(ctx, request) + testhelper.AssertGrpcError(t, drainListConflictFilesResponse(c), codes.FailedPrecondition, "") +} + +func TestFailedListConflictFilesRequestDueToValidation(t *testing.T) { + server, serverSocketPath := runConflictsServer(t) + defer server.Stop() + + client, conn := NewConflictsClient(t, serverSocketPath) + defer conn.Close() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + ourCommitOid := "0b4bc9a49b562e85de7cc9e834518ea6828729b9" + theirCommitOid := "bb5206fee213d983da88c47f9cf4cc6caf9c66dc" + + testCases := []struct { + desc string + request *pb.ListConflictFilesRequest + code codes.Code + }{ + { + desc: "empty repo", + request: &pb.ListConflictFilesRequest{ + Repository: nil, + OurCommitOid: ourCommitOid, + TheirCommitOid: theirCommitOid, + }, + code: codes.InvalidArgument, + }, + { + desc: "empty OurCommitId repo", + request: &pb.ListConflictFilesRequest{ + Repository: testRepo, + OurCommitOid: "", + TheirCommitOid: theirCommitOid, + }, + code: codes.InvalidArgument, + }, + { + desc: "empty TheirCommitId repo", + request: &pb.ListConflictFilesRequest{ + Repository: testRepo, + OurCommitOid: ourCommitOid, + TheirCommitOid: "", + }, + code: codes.InvalidArgument, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.desc, func(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + c, _ := client.ListConflictFiles(ctx, testCase.request) + testhelper.AssertGrpcError(t, drainListConflictFilesResponse(c), testCase.code, "") + }) + } +} + +func getConflictFiles(t *testing.T, c pb.ConflictsService_ListConflictFilesClient) []conflictFile { + files := []conflictFile{} + currentFile := conflictFile{} + for { + r, err := c.Recv() + if err == io.EOF { + break + } else if err != nil { + t.Fatal(err) + } + for _, file := range r.GetFiles() { + // If there's a header this is the beginning of a new file + if header := file.GetHeader(); header != nil { + // Save previous file, except on the first iteration + if len(files) > 0 { + files = append(files, currentFile) + } + + currentFile = conflictFile{header: header} + } else { + // Append to current file's content + currentFile.content = append(currentFile.content, file.GetContent()...) + } + } + } + // Append leftover file + files = append(files, currentFile) + + return files +} + +func drainListConflictFilesResponse(c pb.ConflictsService_ListConflictFilesClient) error { + var err error + for err == nil { + _, err = c.Recv() + } + return err +} diff --git a/internal/service/conflicts/resolve_conflicts.go b/internal/service/conflicts/resolve_conflicts.go new file mode 100644 index 000000000..7f3cf28d4 --- /dev/null +++ b/internal/service/conflicts/resolve_conflicts.go @@ -0,0 +1,91 @@ +package conflicts + +import ( + "fmt" + + pb "gitlab.com/gitlab-org/gitaly-proto/go" + "gitlab.com/gitlab-org/gitaly/internal/rubyserver" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" +) + +func (s *server) ResolveConflicts(stream pb.ConflictsService_ResolveConflictsServer) error { + firstRequest, err := stream.Recv() + if err != nil { + return err + } + + header := firstRequest.GetHeader() + if header == nil { + return grpc.Errorf(codes.InvalidArgument, "ListConflictFiles: empty ResolveConflictsRequestHeader") + } + + if err = validateResolveConflictsHeader(header); err != nil { + return grpc.Errorf(codes.InvalidArgument, "ListConflictFiles: %v", err) + } + + ctx := stream.Context() + client, err := s.ConflictsServiceClient(ctx) + if err != nil { + return err + } + + clientCtx, err := rubyserver.SetHeaders(ctx, header.GetRepository()) + if err != nil { + return err + } + + rubyStream, err := client.ResolveConflicts(clientCtx) + if err != nil { + return err + } + + if err := rubyStream.Send(firstRequest); err != nil { + return err + } + + err = rubyserver.Proxy(func() error { + request, err := stream.Recv() + if err != nil { + return err + } + return rubyStream.Send(request) + }) + + if err != nil { + return err + } + + response, err := rubyStream.CloseAndRecv() + if err != nil { + return err + } + + return stream.SendAndClose(response) +} + +func validateResolveConflictsHeader(header *pb.ResolveConflictsRequestHeader) error { + if header.GetOurCommitOid() == "" { + return fmt.Errorf("empty OurCommitOid") + } + if header.GetTargetRepository() == nil { + return fmt.Errorf("empty TargetRepository") + } + if header.GetTheirCommitOid() == "" { + return fmt.Errorf("empty TheirCommitOid") + } + if header.GetSourceBranch() == nil { + return fmt.Errorf("empty SourceBranch") + } + if header.GetTargetBranch() == nil { + return fmt.Errorf("empty TargetBranch") + } + if header.GetCommitMessage() == nil { + return fmt.Errorf("empty CommitMessage") + } + if header.GetUser() == nil { + return fmt.Errorf("empty User") + } + + return nil +} diff --git a/internal/service/conflicts/resolve_conflicts_test.go b/internal/service/conflicts/resolve_conflicts_test.go new file mode 100644 index 000000000..775695a6c --- /dev/null +++ b/internal/service/conflicts/resolve_conflicts_test.go @@ -0,0 +1,322 @@ +package conflicts_test + +import ( + "encoding/json" + "net" + "testing" + + "github.com/stretchr/testify/require" + pb "gitlab.com/gitlab-org/gitaly-proto/go" + "gitlab.com/gitlab-org/gitaly/internal/git/log" + serverPkg "gitlab.com/gitlab-org/gitaly/internal/server" + "gitlab.com/gitlab-org/gitaly/internal/service/conflicts" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" +) + +var ( + user = &pb.User{ + Name: []byte("John Doe"), + Email: []byte("johndoe@gitlab.com"), + GlId: "user-1", + } + conflictResolutionCommitMessage = "Solve conflicts" +) + +func TestSuccessfulResolveConflictsRequest(t *testing.T) { + server, serverSocketPath := runFullServer(t) + defer server.Stop() + + client, conn := conflicts.NewConflictsClient(t, serverSocketPath) + defer conn.Close() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + ctxOuter, cancel := testhelper.Context() + defer cancel() + + md := testhelper.GitalyServersMetadata(t, serverSocketPath) + ctx := metadata.NewOutgoingContext(ctxOuter, md) + + files := []map[string]interface{}{ + { + "old_path": "files/ruby/popen.rb", + "new_path": "files/ruby/popen.rb", + "sections": map[string]string{ + "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14": "head", + }, + }, + { + "old_path": "files/ruby/regex.rb", + "new_path": "files/ruby/regex.rb", + "sections": map[string]string{ + "6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9": "head", + "6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21": "origin", + "6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49": "origin", + }, + }, + } + filesJSON, err := json.Marshal(files) + require.NoError(t, err) + + sourceBranch := "conflict-resolvable" + headerRequest := &pb.ResolveConflictsRequest{ + ResolveConflictsRequestPayload: &pb.ResolveConflictsRequest_Header{ + Header: &pb.ResolveConflictsRequestHeader{ + Repository: testRepo, + TargetRepository: testRepo, + CommitMessage: []byte(conflictResolutionCommitMessage), + OurCommitOid: "1450cd639e0bc6721eb02800169e464f212cde06", + TheirCommitOid: "824be604a34828eb682305f0d963056cfac87b2d", + SourceBranch: []byte(sourceBranch), + TargetBranch: []byte("conflict-start"), + User: user, + }, + }, + } + filesRequest1 := &pb.ResolveConflictsRequest{ + ResolveConflictsRequestPayload: &pb.ResolveConflictsRequest_FilesJson{ + FilesJson: filesJSON[:50], + }, + } + filesRequest2 := &pb.ResolveConflictsRequest{ + ResolveConflictsRequestPayload: &pb.ResolveConflictsRequest_FilesJson{ + FilesJson: filesJSON[50:], + }, + } + + stream, err := client.ResolveConflicts(ctx) + require.NoError(t, err) + require.NoError(t, stream.Send(headerRequest)) + require.NoError(t, stream.Send(filesRequest1)) + require.NoError(t, stream.Send(filesRequest2)) + + r, err := stream.CloseAndRecv() + require.NoError(t, err) + require.Empty(t, r.GetResolutionError()) + + headCommit, err := log.GetCommit(ctxOuter, testRepo, sourceBranch, "") + require.NoError(t, err) + require.Contains(t, headCommit.ParentIds, "1450cd639e0bc6721eb02800169e464f212cde06") + require.Contains(t, headCommit.ParentIds, "824be604a34828eb682305f0d963056cfac87b2d") + require.Equal(t, string(headCommit.Author.Email), "johndoe@gitlab.com") + require.Equal(t, string(headCommit.Committer.Email), "johndoe@gitlab.com") + require.Equal(t, string(headCommit.Subject), conflictResolutionCommitMessage) +} + +func TestFailedResolveConflictsRequestDueToResolutionError(t *testing.T) { + server, serverSocketPath := runFullServer(t) + defer server.Stop() + + client, conn := conflicts.NewConflictsClient(t, serverSocketPath) + defer conn.Close() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + ctxOuter, cancel := testhelper.Context() + defer cancel() + + md := testhelper.GitalyServersMetadata(t, serverSocketPath) + ctx := metadata.NewOutgoingContext(ctxOuter, md) + + files := []map[string]interface{}{ + { + "old_path": "files/ruby/popen.rb", + "new_path": "files/ruby/popen.rb", + "content": "", + }, + { + "old_path": "files/ruby/regex.rb", + "new_path": "files/ruby/regex.rb", + "sections": map[string]string{ + "6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9": "head", + }, + }, + } + filesJSON, err := json.Marshal(files) + require.NoError(t, err) + + headerRequest := &pb.ResolveConflictsRequest{ + ResolveConflictsRequestPayload: &pb.ResolveConflictsRequest_Header{ + Header: &pb.ResolveConflictsRequestHeader{ + Repository: testRepo, + TargetRepository: testRepo, + CommitMessage: []byte(conflictResolutionCommitMessage), + OurCommitOid: "1450cd639e0bc6721eb02800169e464f212cde06", + TheirCommitOid: "824be604a34828eb682305f0d963056cfac87b2d", + SourceBranch: []byte("conflict-resolvable"), + TargetBranch: []byte("conflict-start"), + User: user, + }, + }, + } + filesRequest := &pb.ResolveConflictsRequest{ + ResolveConflictsRequestPayload: &pb.ResolveConflictsRequest_FilesJson{ + FilesJson: filesJSON, + }, + } + + stream, err := client.ResolveConflicts(ctx) + require.NoError(t, err) + require.NoError(t, stream.Send(headerRequest)) + require.NoError(t, stream.Send(filesRequest)) + + r, err := stream.CloseAndRecv() + require.NoError(t, err) + require.Equal(t, r.GetResolutionError(), "Missing resolution for section ID: 6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21") +} + +func TestFailedResolveConflictsRequestDueToValidation(t *testing.T) { + server, serverSocketPath := runFullServer(t) + defer server.Stop() + + client, conn := conflicts.NewConflictsClient(t, serverSocketPath) + defer conn.Close() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + md := testhelper.GitalyServersMetadata(t, serverSocketPath) + ourCommitOid := "1450cd639e0bc6721eb02800169e464f212cde06" + theirCommitOid := "824be604a34828eb682305f0d963056cfac87b2d" + commitMsg := []byte(conflictResolutionCommitMessage) + sourceBranch := []byte("conflict-resolvable") + targetBranch := []byte("conflict-start") + + testCases := []struct { + desc string + header *pb.ResolveConflictsRequestHeader + code codes.Code + }{ + { + desc: "empty repo", + header: &pb.ResolveConflictsRequestHeader{ + Repository: nil, + OurCommitOid: ourCommitOid, + TargetRepository: testRepo, + TheirCommitOid: theirCommitOid, + CommitMessage: commitMsg, + SourceBranch: sourceBranch, + TargetBranch: targetBranch, + }, + code: codes.InvalidArgument, + }, + { + desc: "empty target repo", + header: &pb.ResolveConflictsRequestHeader{ + Repository: testRepo, + OurCommitOid: ourCommitOid, + TargetRepository: nil, + TheirCommitOid: theirCommitOid, + CommitMessage: commitMsg, + SourceBranch: sourceBranch, + TargetBranch: targetBranch, + }, + code: codes.InvalidArgument, + }, + { + desc: "empty OurCommitId repo", + header: &pb.ResolveConflictsRequestHeader{ + Repository: testRepo, + OurCommitOid: "", + TargetRepository: testRepo, + TheirCommitOid: theirCommitOid, + CommitMessage: commitMsg, + SourceBranch: sourceBranch, + TargetBranch: targetBranch, + }, + code: codes.InvalidArgument, + }, + { + desc: "empty TheirCommitId repo", + header: &pb.ResolveConflictsRequestHeader{ + Repository: testRepo, + OurCommitOid: ourCommitOid, + TargetRepository: testRepo, + TheirCommitOid: "", + CommitMessage: commitMsg, + SourceBranch: sourceBranch, + TargetBranch: targetBranch, + }, + code: codes.InvalidArgument, + }, + { + desc: "empty CommitMessage repo", + header: &pb.ResolveConflictsRequestHeader{ + Repository: testRepo, + OurCommitOid: ourCommitOid, + TargetRepository: testRepo, + TheirCommitOid: theirCommitOid, + CommitMessage: nil, + SourceBranch: sourceBranch, + TargetBranch: targetBranch, + }, + code: codes.InvalidArgument, + }, + { + desc: "empty SourceBranch repo", + header: &pb.ResolveConflictsRequestHeader{ + Repository: testRepo, + OurCommitOid: ourCommitOid, + TargetRepository: testRepo, + TheirCommitOid: theirCommitOid, + CommitMessage: commitMsg, + SourceBranch: nil, + TargetBranch: targetBranch, + }, + code: codes.InvalidArgument, + }, + { + desc: "empty TargetBranch repo", + header: &pb.ResolveConflictsRequestHeader{ + Repository: testRepo, + OurCommitOid: ourCommitOid, + TargetRepository: testRepo, + TheirCommitOid: theirCommitOid, + CommitMessage: commitMsg, + SourceBranch: sourceBranch, + TargetBranch: nil, + }, + code: codes.InvalidArgument, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.desc, func(t *testing.T) { + ctxOuter, cancel := testhelper.Context() + defer cancel() + + ctx := metadata.NewOutgoingContext(ctxOuter, md) + stream, err := client.ResolveConflicts(ctx) + require.NoError(t, err) + + headerRequest := &pb.ResolveConflictsRequest{ + ResolveConflictsRequestPayload: &pb.ResolveConflictsRequest_Header{ + Header: testCase.header, + }, + } + require.NoError(t, stream.Send(headerRequest)) + + _, err = stream.CloseAndRecv() + testhelper.AssertGrpcError(t, err, testCase.code, "") + }) + } +} + +func runFullServer(t *testing.T) (*grpc.Server, string) { + server := serverPkg.New(conflicts.RubyServer) + serverSocketPath := testhelper.GetTemporaryGitalySocketFileName() + + listener, err := net.Listen("unix", serverSocketPath) + if err != nil { + t.Fatal(err) + } + + go server.Serve(listener) + + return server, serverSocketPath +} diff --git a/internal/service/conflicts/resolver.go b/internal/service/conflicts/resolver.go deleted file mode 100644 index 926bf865c..000000000 --- a/internal/service/conflicts/resolver.go +++ /dev/null @@ -1,14 +0,0 @@ -package conflicts - -import ( - pb "gitlab.com/gitlab-org/gitaly-proto/go" - "gitlab.com/gitlab-org/gitaly/internal/helper" -) - -func (s *server) ListConflictFiles(in *pb.ListConflictFilesRequest, stream pb.ConflictsService_ListConflictFilesServer) error { - return helper.Unimplemented -} - -func (s *server) ResolveConflicts(stream pb.ConflictsService_ResolveConflictsServer) error { - return helper.Unimplemented -} diff --git a/internal/service/conflicts/testhelper_test.go b/internal/service/conflicts/testhelper_test.go new file mode 100644 index 000000000..92651f3d6 --- /dev/null +++ b/internal/service/conflicts/testhelper_test.go @@ -0,0 +1,72 @@ +package conflicts + +import ( + "net" + "os" + "testing" + "time" + + log "github.com/sirupsen/logrus" + + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + + pb "gitlab.com/gitlab-org/gitaly-proto/go" + + "gitlab.com/gitlab-org/gitaly/internal/rubyserver" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" +) + +func TestMain(m *testing.M) { + os.Exit(testMain(m)) +} + +var RubyServer *rubyserver.Server + +func testMain(m *testing.M) int { + defer testhelper.MustHaveNoChildProcess() + + var err error + + testhelper.ConfigureRuby() + RubyServer, err = rubyserver.Start() + if err != nil { + log.Fatal(err) + } + defer RubyServer.Stop() + + return m.Run() +} + +func runConflictsServer(t *testing.T) (*grpc.Server, string) { + server := testhelper.NewTestGrpcServer(t, nil, nil) + + serverSocketPath := testhelper.GetTemporaryGitalySocketFileName() + listener, err := net.Listen("unix", serverSocketPath) + if err != nil { + t.Fatal(err) + } + + pb.RegisterConflictsServiceServer(server, NewServer(RubyServer)) + reflection.Register(server) + + go server.Serve(listener) + + return server, serverSocketPath +} + +func NewConflictsClient(t *testing.T, serverSocketPath string) (pb.ConflictsServiceClient, *grpc.ClientConn) { + connOpts := []grpc.DialOption{ + grpc.WithInsecure(), + grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) { + return net.DialTimeout("unix", addr, timeout) + }), + } + + conn, err := grpc.Dial(serverSocketPath, connOpts...) + if err != nil { + t.Fatal(err) + } + + return pb.NewConflictsServiceClient(conn), conn +} diff --git a/ruby/lib/gitaly_server/conflicts_service.rb b/ruby/lib/gitaly_server/conflicts_service.rb index dc16e5b3c..0fbea42e8 100644 --- a/ruby/lib/gitaly_server/conflicts_service.rb +++ b/ruby/lib/gitaly_server/conflicts_service.rb @@ -1,5 +1,92 @@ +require 'active_support/core_ext/hash/indifferent_access' + module GitalyServer class ConflictsService < Gitaly::ConflictsService::Service include Utils + + def list_conflict_files(request, call) + bridge_exceptions do + begin + repo = Gitlab::Git::Repository.from_gitaly(request.repository, call) + resolver = Gitlab::Git::Conflict::Resolver.new(repo, request.our_commit_oid, request.their_commit_oid) + conflicts = resolver.conflicts + files = [] + msg_size = 0 + + Enumerator.new do |y| + conflicts.each do |file| + files << Gitaly::ConflictFile.new(header: conflict_file_header(file)) + + strio = StringIO.new(file.content) + while chunk = strio.read(Gitlab.config.git.write_buffer_size - msg_size) + files << Gitaly::ConflictFile.new(content: chunk) + msg_size += chunk.bytesize + + # We don't send a message for each chunk because the content of + # a file may be smaller than the size limit, which means we can + # keep adding data to the message + next if msg_size < Gitlab.config.git.write_buffer_size + + y.yield(Gitaly::ListConflictFilesResponse.new(files: files)) + + files = [] + msg_size = 0 + end + end + + # Send leftover data, if any + y.yield(Gitaly::ListConflictFilesResponse.new(files: files)) if files.any? + end + rescue Gitlab::Git::Conflict::Resolver::ConflictSideMissing => e + raise GRPC::FailedPrecondition.new(e.message) + end + end + end + + def resolve_conflicts(call) + bridge_exceptions do + header = nil + files_json = "" + + call.each_remote_read.each_with_index do |request, index| + if index.zero? + header = request.header + else + files_json << request.files_json + end + end + + repo = Gitlab::Git::Repository.from_gitaly(header.repository, call) + remote_repo = Gitlab::Git::GitalyRemoteRepository.new(header.target_repository, call) + resolver = Gitlab::Git::Conflict::Resolver.new(remote_repo, header.our_commit_oid, header.their_commit_oid) + user = Gitlab::Git::User.from_gitaly(header.user) + files = JSON.parse(files_json).map(&:with_indifferent_access) + + begin + params = { + source_branch: header.source_branch, + target_branch: header.target_branch, + commit_message: header.commit_message.dup + } + resolver.resolve_conflicts(repo, user, files, params) + + Gitaly::ResolveConflictsResponse.new + rescue Gitlab::Git::Conflict::Resolver::ResolutionError => e + Gitaly::ResolveConflictsResponse.new(resolution_error: e.message) + end + end + end + + private + + def conflict_file_header(file) + Gitaly::ConflictFileHeader.new( + repository: file.repository.gitaly_repository, + commit_oid: file.commit_oid, + their_path: file.their_path, + our_path: file.our_path, + our_mode: file.our_mode + ) + end end end |