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:
authorAhmad Sherif <ahmad.m.sherif@gmail.com>2017-12-20 23:29:15 +0300
committerAhmad Sherif <ahmad.m.sherif@gmail.com>2017-12-20 23:29:15 +0300
commitae6920282ce5fa11580d403a7d3c1fe13dc27c26 (patch)
tree14d81421729cfaca41f339c323305befdeb9c7af
parent3e18525956d4cc6b205ac1dbbd7519b87fb96a62 (diff)
parent88ea5bc21f5b161a424d3b31d530f415bfdbac4e (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.md4
-rw-r--r--internal/service/conflicts/list_conflict_files.go57
-rw-r--r--internal/service/conflicts/list_conflict_files_test.go191
-rw-r--r--internal/service/conflicts/resolve_conflicts.go91
-rw-r--r--internal/service/conflicts/resolve_conflicts_test.go322
-rw-r--r--internal/service/conflicts/resolver.go14
-rw-r--r--internal/service/conflicts/testhelper_test.go72
-rw-r--r--ruby/lib/gitaly_server/conflicts_service.rb87
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