diff options
author | karthik nayak <knayak@gitlab.com> | 2023-06-16 16:57:42 +0300 |
---|---|---|
committer | karthik nayak <knayak@gitlab.com> | 2023-06-16 16:57:42 +0300 |
commit | ceafa16c0204bd3b155299731c8b7da661ce3511 (patch) | |
tree | 0221796091f4d1e65f17b7ae826d16314ac1048d | |
parent | 4a7c0b66c9e8f70a0b24103ab530edaff0f82943 (diff) | |
parent | ec67f0aed83fbbceaa5b0d75c6b7395140342f72 (diff) |
Merge branch '4580-reimplement-resolveconflicts-without-git2go' into 'master'
conflicts: Modernize the tests in `resolve_conflicts_test`
Closes #4580
See merge request https://gitlab.com/gitlab-org/gitaly/-/merge_requests/5924
Merged-by: karthik nayak <knayak@gitlab.com>
Approved-by: Quang-Minh Nguyen <qmnguyen@gitlab.com>
Reviewed-by: Quang-Minh Nguyen <qmnguyen@gitlab.com>
3 files changed, 1068 insertions, 830 deletions
diff --git a/internal/gitaly/service/conflicts/list_conflict_files_test.go b/internal/gitaly/service/conflicts/list_conflict_files_test.go index adc2c932a..e0921cc72 100644 --- a/internal/gitaly/service/conflicts/list_conflict_files_test.go +++ b/internal/gitaly/service/conflicts/list_conflict_files_test.go @@ -45,7 +45,7 @@ func testListConflictFiles(t *testing.T, ctx context.Context) { { "Lists the expected conflict files", func(tb testing.TB, ctx context.Context) setupData { - cfg, client := setupConflictsServiceWithoutRepo(tb, nil) + cfg, client := setupConflictsService(tb, nil) repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( @@ -92,7 +92,7 @@ func testListConflictFiles(t *testing.T, ctx context.Context) { { "Lists the expected conflict files with short OIDs", func(tb testing.TB, ctx context.Context) setupData { - cfg, client := setupConflictsServiceWithoutRepo(tb, nil) + cfg, client := setupConflictsService(tb, nil) repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( @@ -139,7 +139,7 @@ func testListConflictFiles(t *testing.T, ctx context.Context) { { "conflict in submodules commits are not handled", func(tb testing.TB, ctx context.Context) setupData { - cfg, client := setupConflictsServiceWithoutRepo(tb, nil) + cfg, client := setupConflictsService(tb, nil) repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) _, subRepoPath := gittest.CreateRepository(tb, ctx, cfg) @@ -192,7 +192,7 @@ func testListConflictFiles(t *testing.T, ctx context.Context) { { "Lists the expected conflict files with ancestor path", func(tb testing.TB, ctx context.Context) setupData { - cfg, client := setupConflictsServiceWithoutRepo(tb, nil) + cfg, client := setupConflictsService(tb, nil) repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) commonCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( @@ -249,7 +249,7 @@ func testListConflictFiles(t *testing.T, ctx context.Context) { { "Lists the expected conflict files with huge diff", func(tb testing.TB, ctx context.Context) setupData { - cfg, client := setupConflictsServiceWithoutRepo(tb, nil) + cfg, client := setupConflictsService(tb, nil) repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( @@ -302,7 +302,7 @@ func testListConflictFiles(t *testing.T, ctx context.Context) { { "invalid commit id on 'our' side", func(tb testing.TB, ctx context.Context) setupData { - cfg, client := setupConflictsServiceWithoutRepo(tb, nil) + cfg, client := setupConflictsService(tb, nil) repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( @@ -326,7 +326,7 @@ func testListConflictFiles(t *testing.T, ctx context.Context) { { "invalid commit id on 'their' side", func(tb testing.TB, ctx context.Context) setupData { - cfg, client := setupConflictsServiceWithoutRepo(tb, nil) + cfg, client := setupConflictsService(tb, nil) repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( @@ -350,7 +350,7 @@ func testListConflictFiles(t *testing.T, ctx context.Context) { { "conflict side missing", func(tb testing.TB, ctx context.Context) setupData { - cfg, client := setupConflictsServiceWithoutRepo(tb, nil) + cfg, client := setupConflictsService(tb, nil) repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) commonCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( @@ -384,7 +384,7 @@ func testListConflictFiles(t *testing.T, ctx context.Context) { { "allow tree conflicts", func(tb testing.TB, ctx context.Context) setupData { - cfg, client := setupConflictsServiceWithoutRepo(tb, nil) + cfg, client := setupConflictsService(tb, nil) repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) commonCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( @@ -446,7 +446,7 @@ func testListConflictFiles(t *testing.T, ctx context.Context) { { "encoding error", func(tb testing.TB, ctx context.Context) setupData { - cfg, client := setupConflictsServiceWithoutRepo(tb, nil) + cfg, client := setupConflictsService(tb, nil) repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( @@ -472,7 +472,7 @@ func testListConflictFiles(t *testing.T, ctx context.Context) { { "empty repo", func(tb testing.TB, ctx context.Context) setupData { - cfg, client := setupConflictsServiceWithoutRepo(tb, nil) + cfg, client := setupConflictsService(tb, nil) _, repoPath := gittest.CreateRepository(tb, ctx, cfg) ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( @@ -501,7 +501,7 @@ func testListConflictFiles(t *testing.T, ctx context.Context) { { "empty OurCommitId field", func(tb testing.TB, ctx context.Context) setupData { - cfg, client := setupConflictsServiceWithoutRepo(tb, nil) + cfg, client := setupConflictsService(tb, nil) repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( @@ -524,7 +524,7 @@ func testListConflictFiles(t *testing.T, ctx context.Context) { { "empty TheirCommitId field", func(tb testing.TB, ctx context.Context) setupData { - cfg, client := setupConflictsServiceWithoutRepo(tb, nil) + cfg, client := setupConflictsService(tb, nil) repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( diff --git a/internal/gitaly/service/conflicts/resolve_conflicts_test.go b/internal/gitaly/service/conflicts/resolve_conflicts_test.go index 265ed36e7..d501ca540 100644 --- a/internal/gitaly/service/conflicts/resolve_conflicts_test.go +++ b/internal/gitaly/service/conflicts/resolve_conflicts_test.go @@ -3,15 +3,10 @@ package conflicts import ( - "bytes" "context" + "crypto/sha1" "encoding/json" "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "regexp" "strings" "testing" @@ -19,16 +14,13 @@ import ( "gitlab.com/gitlab-org/gitaly/v16/internal/git" "gitlab.com/gitlab-org/gitaly/v16/internal/git/gittest" "gitlab.com/gitlab-org/gitaly/v16/internal/git/localrepo" - "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/hook" - "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage" - "gitlab.com/gitlab-org/gitaly/v16/internal/helper/perm" + "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/config" + "gitlab.com/gitlab-org/gitaly/v16/internal/helper/text" "gitlab.com/gitlab-org/gitaly/v16/internal/structerr" "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper/testcfg" "gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb" - "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -39,891 +31,1149 @@ var ( GlId: "user-1", } conflictResolutionCommitMessage = "Solve conflicts" - - 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", - }, - }, - } ) -func TestSuccessfulResolveConflictsRequestHelper(t *testing.T) { - var verifyFunc func(tb testing.TB, pushOptions []string, stdin io.Reader) - verifyFuncProxy := func(t *testing.T, ctx context.Context, repo *gitalypb.Repository, pushOptions, env []string, stdin io.Reader, stdout, stderr io.Writer) error { - // We use a proxy func here as we need to provide the hookManager dependency while creating the service but we only - // know the commit IDs after the service is created. The proxy allows us to modify the verifyFunc after the service - // is already built. - verifyFunc(t, pushOptions, stdin) - return nil +func TestResolveConflicts(t *testing.T) { + type setupData struct { + cfg config.Cfg + requestHeader *gitalypb.ResolveConflictsRequest_Header + requestsFilesJSON []*gitalypb.ResolveConflictsRequest_FilesJson + client gitalypb.ConflictsServiceClient + repo *gitalypb.Repository + repoPath string + expectedContent map[string]map[string][]byte + expectedResponse *gitalypb.ResolveConflictsResponse + expectedError error + skipCommitCheck bool + additionalChecks func() } - ctx := testhelper.Context(t) - hookManager := hook.NewMockManager(t, verifyFuncProxy, verifyFuncProxy, hook.NopUpdate, hook.NopReferenceTransaction) - cfg, repoProto, repoPath, client := setupConflictsService(t, ctx, hookManager) + for _, tc := range []struct { + desc string + setup func(testing.TB, context.Context) setupData + }{ + { + "single file conflict, pick ours", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + baseCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( + gittest.TreeEntry{Path: "a", Mode: "100644", Content: "apple"}, + )) + + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithParents(baseCommitID), gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "apricot"})) + theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithParents(baseCommitID), gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "acai"})) + + files := []map[string]interface{}{ + { + "old_path": "a", + "new_path": "a", + "sections": map[string]string{ + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 1, 1): "head", + }, + }, + } - repo := localrepo.NewTestRepo(t, cfg, repoProto) + filesJSON, err := json.Marshal(files) + require.NoError(t, err) - missingAncestorPath := "files/missing_ancestor.txt" - files := []map[string]interface{}{ - { - "old_path": "files/ruby/popen.rb", - "new_path": "files/ruby/popen.rb", - "sections": map[string]string{ - "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14": "head", + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON[:50]}, + {FilesJson: filesJSON[50:]}, + }, + expectedResponse: &gitalypb.ResolveConflictsResponse{}, + expectedContent: map[string]map[string][]byte{ + "refs/heads/ours": { + "a": []byte("apricot"), + }, + }, + } }, }, { - "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", + "single file conflict, pick theirs", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + baseCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( + gittest.TreeEntry{Path: "a", Mode: "100644", Content: "apple"}, + )) + + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithParents(baseCommitID), gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "apricot"})) + theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithParents(baseCommitID), gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "acai"})) + + files := []map[string]interface{}{ + { + "old_path": "a", + "new_path": "a", + "sections": map[string]string{ + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 1, 1): "origin", + }, + }, + } + + filesJSON, err := json.Marshal(files) + require.NoError(t, err) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON[:50]}, + {FilesJson: filesJSON[50:]}, + }, + expectedResponse: &gitalypb.ResolveConflictsResponse{}, + expectedContent: map[string]map[string][]byte{ + "refs/heads/ours": { + "a": []byte("acai"), + }, + }, + } }, }, { - "old_path": missingAncestorPath, - "new_path": missingAncestorPath, - "sections": map[string]string{ - "b760bfd3b1b1da380b4276eb30fb3b2b7e4f08e1_1_1": "origin", + "single file conflict without ancestor, pick ours", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "apricot"})) + theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "acai"})) + + files := []map[string]interface{}{ + { + "old_path": "a", + "new_path": "a", + "sections": map[string]string{ + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 1, 1): "head", + }, + }, + } + + filesJSON, err := json.Marshal(files) + require.NoError(t, err) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON[:50]}, + {FilesJson: filesJSON[50:]}, + }, + expectedResponse: &gitalypb.ResolveConflictsResponse{}, + expectedContent: map[string]map[string][]byte{ + "refs/heads/ours": { + "a": []byte("apricot"), + }, + }, + } }, }, - } - - filesJSON, err := json.Marshal(files) - require.NoError(t, err) - - sourceBranch := "conflict-resolvable" - targetBranch := "conflict-start" - ourCommitOID := "1450cd639e0bc6721eb02800169e464f212cde06" // part of branch conflict-resolvable - theirCommitOID := "824be604a34828eb682305f0d963056cfac87b2d" // part of branch conflict-start - ancestorCommitOID := "6907208d755b60ebeacb2e9dfea74c92c3449a1f" - - // introduce a conflict that exists on both branches, but not the - // ancestor - commitConflict := func(parentCommitID, branch, blob string) string { - blobID, err := repo.WriteBlob(ctx, "", strings.NewReader(blob)) - require.NoError(t, err) - gittest.Exec(t, cfg, "-C", repoPath, "read-tree", branch) - gittest.Exec(t, cfg, "-C", repoPath, - "update-index", "--add", "--cacheinfo", "100644", blobID.String(), missingAncestorPath, - ) - treeID := bytes.TrimSpace( - gittest.Exec(t, cfg, "-C", repoPath, "write-tree"), - ) - commitID := bytes.TrimSpace( - gittest.Exec(t, cfg, "-C", repoPath, - "commit-tree", string(treeID), "-p", parentCommitID, - ), - ) - gittest.Exec(t, cfg, "-C", repoPath, "update-ref", "refs/heads/"+branch, string(commitID)) - return string(commitID) - } + { + "single file multi conflict", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + baseCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( + gittest.TreeEntry{Path: "a", Mode: "100644", Content: "apple\n" + strings.Repeat("filler\n", 10) + "banana"}, + )) + + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithParents(baseCommitID), gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{ + Path: "a", Mode: "100644", + Content: "apricot\n" + strings.Repeat("filler\n", 10) + "berries", + })) + theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithParents(baseCommitID), gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{ + Path: "a", Mode: "100644", + Content: "acai\n" + strings.Repeat("filler\n", 10) + "birne", + })) + + files := []map[string]interface{}{ + { + "old_path": "a", + "new_path": "a", + "sections": map[string]string{ + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 1, 1): "head", + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 12, 12): "origin", + }, + }, + } - // sanity check: make sure the conflict file does not exist on the - // common ancestor - cmd := exec.CommandContext(ctx, "git", "cat-file", "-e", ancestorCommitOID+":"+missingAncestorPath) - require.Error(t, cmd.Run()) - - ourCommitOID = commitConflict(ourCommitOID, sourceBranch, "content-1") - theirCommitOID = commitConflict(theirCommitOID, targetBranch, "content-2") - hookCount := 0 - - verifyFunc = func(tb testing.TB, pushOptions []string, stdin io.Reader) { - changes, err := io.ReadAll(stdin) - require.NoError(tb, err) - pattern := fmt.Sprintf("%s .* refs/heads/%s\n", ourCommitOID, sourceBranch) - require.Regexp(tb, regexp.MustCompile(pattern), string(changes)) - require.Empty(tb, pushOptions) - hookCount++ - } + filesJSON, err := json.Marshal(files) + require.NoError(t, err) - mdGS := testcfg.GitalyServersMetadataFromCfg(t, cfg) - mdFF, _ := metadata.FromOutgoingContext(ctx) - ctx = metadata.NewOutgoingContext(ctx, metadata.Join(mdGS, mdFF)) - - headerRequest := &gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_Header{ - Header: &gitalypb.ResolveConflictsRequestHeader{ - Repository: repoProto, - TargetRepository: repoProto, - CommitMessage: []byte(conflictResolutionCommitMessage), - OurCommitOid: ourCommitOID, - TheirCommitOid: theirCommitOID, - SourceBranch: []byte(sourceBranch), - TargetBranch: []byte(targetBranch), - User: user, + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON[:50]}, + {FilesJson: filesJSON[50:]}, + }, + expectedResponse: &gitalypb.ResolveConflictsResponse{}, + expectedContent: map[string]map[string][]byte{ + "refs/heads/ours": { + "a": []byte("apricot\n" + strings.Repeat("filler\n", 10) + "birne"), + }, + }, + } }, }, - } - filesRequest1 := &gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_FilesJson{ - FilesJson: filesJSON[:50], - }, - } - filesRequest2 := &gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_FilesJson{ - FilesJson: filesJSON[50:], - }, - } + { + "multi file multi conflict", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + baseCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithTreeEntries( + gittest.TreeEntry{Path: "a", Mode: "100644", Content: "apple\n" + strings.Repeat("filler\n", 10) + "banana"}, + gittest.TreeEntry{Path: "b", Mode: "100644", Content: "strawberry"}, + )) + + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithParents(baseCommitID), gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{ + Path: "a", Mode: "100644", + Content: "apricot\n" + strings.Repeat("filler\n", 10) + "berries", + }, gittest.TreeEntry{Path: "b", Mode: "100644", Content: "blueberry"})) + theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithParents(baseCommitID), gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{ + Path: "a", Mode: "100644", + Content: "acai\n" + strings.Repeat("filler\n", 10) + "birne", + }, gittest.TreeEntry{Path: "b", Mode: "100644", Content: "raspberry"})) + + files := []map[string]interface{}{ + { + "old_path": "a", + "new_path": "a", + "sections": map[string]string{ + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 1, 1): "head", + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 12, 12): "origin", + }, + }, + { + "old_path": "b", + "new_path": "b", + "sections": map[string]string{ + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("b")), 1, 1): "head", + }, + }, + } - 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 := repo.ReadCommit(ctx, git.Revision(sourceBranch)) - require.NoError(t, err) - require.Contains(t, headCommit.ParentIds, ourCommitOID) - require.Contains(t, headCommit.ParentIds, theirCommitOID) - 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) - - require.Equal(t, 2, hookCount) -} + filesJSON, err := json.Marshal(files) + require.NoError(t, err) -func TestResolveConflictsWithRemoteRepo(t *testing.T) { - t.Parallel() - - ctx := testhelper.Context(t) - hookManager := hook.NewMockManager(t, hook.NopPreReceive, hook.NopPostReceive, hook.NopUpdate, hook.NopReferenceTransaction) - cfg, sourceRepo, sourceRepoPath, client := setupConflictsService(t, ctx, hookManager) - - testcfg.BuildGitalySSH(t, cfg) - testcfg.BuildGitalyHooks(t, cfg) - - sourceBlobOID := gittest.WriteBlob(t, cfg, sourceRepoPath, []byte("contents-1\n")) - sourceCommitOID := gittest.WriteCommit(t, cfg, sourceRepoPath, - gittest.WithTreeEntries(gittest.TreeEntry{ - Path: "file.txt", OID: sourceBlobOID, Mode: "100644", - }), - ) - gittest.Exec(t, cfg, "-C", sourceRepoPath, "update-ref", "refs/heads/source", sourceCommitOID.String()) - - targetRepo, targetRepoPath := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{ - Seed: gittest.SeedGitLabTest, - }) - targetBlobOID := gittest.WriteBlob(t, cfg, targetRepoPath, []byte("contents-2\n")) - targetCommitOID := gittest.WriteCommit(t, cfg, targetRepoPath, - gittest.WithTreeEntries(gittest.TreeEntry{ - OID: targetBlobOID, Path: "file.txt", Mode: "100644", - }), - ) - gittest.Exec(t, cfg, "-C", targetRepoPath, "update-ref", "refs/heads/target", targetCommitOID.String()) - - ctx = testhelper.MergeOutgoingMetadata(ctx, testcfg.GitalyServersMetadataFromCfg(t, cfg)) - - stream, err := client.ResolveConflicts(ctx) - require.NoError(t, err) - - filesJSON, err := json.Marshal([]map[string]interface{}{ - { - "old_path": "file.txt", - "new_path": "file.txt", - "sections": map[string]string{ - "5436437fa01a7d3e41d46741da54b451446774ca_1_1": "origin", + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON[:50]}, + {FilesJson: filesJSON[50:]}, + }, + expectedResponse: &gitalypb.ResolveConflictsResponse{}, + expectedContent: map[string]map[string][]byte{ + "refs/heads/ours": { + "a": []byte("apricot\n" + strings.Repeat("filler\n", 10) + "birne"), + "b": []byte("blueberry"), + }, + }, + } }, }, - }) - require.NoError(t, err) - - require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_Header{ - Header: &gitalypb.ResolveConflictsRequestHeader{ - Repository: sourceRepo, - TargetRepository: targetRepo, - CommitMessage: []byte(conflictResolutionCommitMessage), - OurCommitOid: sourceCommitOID.String(), - TheirCommitOid: targetCommitOID.String(), - SourceBranch: []byte("source"), - TargetBranch: []byte("target"), - User: user, + { + "single file conflict, remote repo", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + testcfg.BuildGitalySSH(t, cfg) + + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + targetRepo, targetRepoPath := gittest.CreateRepository(tb, ctx, cfg) + + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "apricot"})) + theirCommitID := gittest.WriteCommit(tb, cfg, targetRepoPath, gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "acai"})) + + files := []map[string]interface{}{ + { + "old_path": "a", + "new_path": "a", + "sections": map[string]string{ + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 1, 1): "head", + }, + }, + } + + filesJSON, err := json.Marshal(files) + require.NoError(t, err) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: targetRepo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON[:50]}, + {FilesJson: filesJSON[50:]}, + }, + expectedResponse: &gitalypb.ResolveConflictsResponse{}, + expectedContent: map[string]map[string][]byte{ + "refs/heads/ours": { + "a": []byte("apricot"), + }, + }, + } }, }, - })) - require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_FilesJson{ - FilesJson: filesJSON, - }, - })) - - response, err := stream.CloseAndRecv() - require.NoError(t, err) - require.Empty(t, response.GetResolutionError()) + { + "single file with only newline", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) - require.Equal(t, []byte("contents-2\n"), gittest.Exec(t, cfg, "-C", sourceRepoPath, "cat-file", "-p", "refs/heads/source:file.txt")) -} + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "\n"})) + theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "\n"})) -func TestResolveConflictsLineEndings(t *testing.T) { - ctx := testhelper.Context(t) - hookManager := hook.NewMockManager(t, hook.NopPreReceive, hook.NopPostReceive, hook.NopUpdate, hook.NopReferenceTransaction) - cfg, repo, repoPath, client := setupConflictsService(t, ctx, hookManager) + files := []map[string]interface{}{} - ctx = testhelper.MergeOutgoingMetadata(ctx, testcfg.GitalyServersMetadataFromCfg(t, cfg)) + filesJSON, err := json.Marshal(files) + require.NoError(t, err) - for _, tc := range []struct { - desc string - ourContent string - theirContent string - resolutions []map[string]interface{} - expectedContents string - expectedError string - }{ - { - desc: "only newline", - ourContent: "\n", - theirContent: "\n", - resolutions: []map[string]interface{}{}, - expectedContents: "\n", - }, - { - desc: "conflicting newline with embedded character", - ourContent: "\nA\n", - theirContent: "\nB\n", - resolutions: []map[string]interface{}{ - { - "old_path": "file.txt", - "new_path": "file.txt", - "sections": map[string]string{ - "5436437fa01a7d3e41d46741da54b451446774ca_2_2": "head", - }, - }, - }, - expectedContents: "\nA\n", - }, - { - desc: "conflicting carriage-return newlines", - ourContent: "A\r\nB\r\nC\r\nD\r\nE\r\n", - theirContent: "A\r\nB\r\nX\r\nD\r\nE\r\n", - resolutions: []map[string]interface{}{ - { - "old_path": "file.txt", - "new_path": "file.txt", - "sections": map[string]string{ - "5436437fa01a7d3e41d46741da54b451446774ca_3_3": "origin", - }, - }, + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON}, + }, + expectedResponse: &gitalypb.ResolveConflictsResponse{}, + expectedContent: map[string]map[string][]byte{ + "refs/heads/ours": { + "a": []byte("\n"), + }, + }, + } }, - expectedContents: "A\r\nB\r\nX\r\nD\r\nE\r\n", }, { - desc: "conflict with no trailing newline", - ourContent: "A\nB", - theirContent: "X\nB", - resolutions: []map[string]interface{}{ - { - "old_path": "file.txt", - "new_path": "file.txt", - "sections": map[string]string{ - "5436437fa01a7d3e41d46741da54b451446774ca_1_1": "head", - }, - }, + "conflicting newline with embedded character", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "\nA\n"})) + theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "\nB\n"})) + + files := []map[string]interface{}{ + { + "old_path": "a", + "new_path": "a", + "sections": map[string]string{ + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 2, 2): "head", + }, + }, + } + + filesJSON, err := json.Marshal(files) + require.NoError(t, err) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON}, + }, + expectedResponse: &gitalypb.ResolveConflictsResponse{}, + expectedContent: map[string]map[string][]byte{ + "refs/heads/ours": { + "a": []byte("\nA\n"), + }, + }, + } }, - expectedContents: "A\nB", }, { - desc: "conflict with existing conflict markers", - ourContent: "<<<<<<< HEAD\nA\nB\n=======", - theirContent: "X\nB", - resolutions: []map[string]interface{}{ - { - "old_path": "file.txt", - "new_path": "file.txt", - "sections": map[string]string{ - "5436437fa01a7d3e41d46741da54b451446774ca_1_1": "head", - }, - }, - }, - expectedError: `resolve: parse conflict for "file.txt": unexpected conflict delimiter`, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - ourOID := gittest.WriteBlob(t, cfg, repoPath, []byte(tc.ourContent)) - ourCommit := gittest.WriteCommit(t, cfg, repoPath, - gittest.WithTreeEntries(gittest.TreeEntry{ - OID: ourOID, Path: "file.txt", Mode: "100644", - }), - ) - gittest.Exec(t, cfg, "-C", repoPath, "update-ref", "refs/heads/ours", ourCommit.String()) - - theirOID := gittest.WriteBlob(t, cfg, repoPath, []byte(tc.theirContent)) - theirCommit := gittest.WriteCommit(t, cfg, repoPath, - gittest.WithTreeEntries(gittest.TreeEntry{ - OID: theirOID, Path: "file.txt", Mode: "100644", - }), - ) - gittest.Exec(t, cfg, "-C", repoPath, "update-ref", "refs/heads/theirs", theirCommit.String()) - - stream, err := client.ResolveConflicts(ctx) - require.NoError(t, err) - - filesJSON, err := json.Marshal(tc.resolutions) - require.NoError(t, err) + "conflicting carriage-return newlines", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "A\r\nB\r\nC\r\nD\r\nE\r\n"})) + theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "A\r\nB\r\nX\r\nD\r\nE\r\n"})) + + files := []map[string]interface{}{ + { + "old_path": "a", + "new_path": "a", + "sections": map[string]string{ + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 3, 3): "origin", + }, + }, + } - require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_Header{ - Header: &gitalypb.ResolveConflictsRequestHeader{ - Repository: repo, - TargetRepository: repo, - CommitMessage: []byte(conflictResolutionCommitMessage), - OurCommitOid: ourCommit.String(), - TheirCommitOid: theirCommit.String(), - SourceBranch: []byte("ours"), - TargetBranch: []byte("theirs"), - User: user, - }, - }, - })) - require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_FilesJson{ - FilesJson: filesJSON, - }, - })) - - response, err := stream.CloseAndRecv() - - if tc.expectedError == "" { + filesJSON, err := json.Marshal(files) require.NoError(t, err) - require.Empty(t, response.GetResolutionError()) - oursFile := gittest.Exec(t, cfg, "-C", repoPath, "cat-file", "-p", "refs/heads/ours:file.txt") - require.Equal(t, []byte(tc.expectedContents), oursFile) - } else { - require.Equal(t, status.Error(codes.Internal, tc.expectedError), err) - } - }) - } -} - -func TestResolveConflictsNonOIDRequests(t *testing.T) { - ctx := testhelper.Context(t) - hookManager := hook.NewMockManager(t, hook.NopPreReceive, hook.NopPostReceive, hook.NopUpdate, hook.NopReferenceTransaction) - cfg, repoProto, _, client := setupConflictsService(t, ctx, hookManager) - - ctx = testhelper.MergeOutgoingMetadata(ctx, testcfg.GitalyServersMetadataFromCfg(t, cfg)) - - stream, err := client.ResolveConflicts(ctx) - require.NoError(t, err) - - require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_Header{ - Header: &gitalypb.ResolveConflictsRequestHeader{ - Repository: repoProto, - TargetRepository: repoProto, - CommitMessage: []byte(conflictResolutionCommitMessage), - OurCommitOid: "conflict-resolvable", - TheirCommitOid: "conflict-start", - SourceBranch: []byte("conflict-resolvable"), - TargetBranch: []byte("conflict-start"), - User: user, + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON}, + }, + expectedResponse: &gitalypb.ResolveConflictsResponse{}, + expectedContent: map[string]map[string][]byte{ + "refs/heads/ours": { + "a": []byte("A\r\nB\r\nX\r\nD\r\nE\r\n"), + }, + }, + } }, }, - })) - - filesJSON, err := json.Marshal(files) - require.NoError(t, err) - require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_FilesJson{ - FilesJson: filesJSON, - }, - })) - - _, err = stream.CloseAndRecv() - testhelper.RequireGrpcError(t, status.Errorf(codes.Internal, "Rugged::InvalidError: unable to parse OID - contains invalid characters"), err) -} - -func TestResolveConflictsIdenticalContent(t *testing.T) { - ctx := testhelper.Context(t) - - hookManager := hook.NewMockManager(t, hook.NopPreReceive, hook.NopPostReceive, hook.NopUpdate, hook.NopReferenceTransaction) - cfg, repoProto, repoPath, client := setupConflictsService(t, ctx, hookManager) - - repo := localrepo.NewTestRepo(t, cfg, repoProto) - - sourceBranch := "conflict-resolvable" - sourceOID, err := repo.ResolveRevision(ctx, git.Revision(sourceBranch)) - require.NoError(t, err) - - targetBranch := "conflict-start" - targetOID, err := repo.ResolveRevision(ctx, git.Revision(targetBranch)) - require.NoError(t, err) - - tempDir := testhelper.TempDir(t) + { + "conflict with no trailing newline", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "A\nB"})) + theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "X\nB"})) + + files := []map[string]interface{}{ + { + "old_path": "a", + "new_path": "a", + "sections": map[string]string{ + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 1, 1): "head", + }, + }, + } - var conflictingPaths []string - for _, rev := range []string{ - sourceOID.String(), - "6907208d755b60ebeacb2e9dfea74c92c3449a1f", - targetOID.String(), - } { - contents := gittest.Exec(t, cfg, "-C", repoPath, "cat-file", "-p", rev+":files/ruby/popen.rb") - path := filepath.Join(tempDir, rev) - require.NoError(t, os.WriteFile(path, contents, perm.SharedFile)) - conflictingPaths = append(conflictingPaths, path) - } + filesJSON, err := json.Marshal(files) + require.NoError(t, err) - var conflictContents bytes.Buffer - err = repo.ExecAndWait(ctx, git.Command{ - Name: "merge-file", - Flags: []git.Option{ - git.Flag{Name: "--quiet"}, - git.Flag{Name: "--stdout"}, - // We pass `-L` three times for each of the conflicting files. - git.ValueFlag{Name: "-L", Value: "files/ruby/popen.rb"}, - git.ValueFlag{Name: "-L", Value: "files/ruby/popen.rb"}, - git.ValueFlag{Name: "-L", Value: "files/ruby/popen.rb"}, + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON}, + }, + expectedResponse: &gitalypb.ResolveConflictsResponse{}, + expectedContent: map[string]map[string][]byte{ + "refs/heads/ours": { + "a": []byte("A\nB"), + }, + }, + } + }, }, - Args: conflictingPaths, - }, git.WithStdout(&conflictContents)) + { + "conflict with existing conflict markers", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "<<<<<<< HEAD\nA\nB\n======="})) + theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "X\nB"})) + + files := []map[string]interface{}{ + { + "old_path": "a", + "new_path": "a", + "sections": map[string]string{ + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 1, 1): "head", + }, + }, + } - // The merge will result in a merge conflict and thus cause the command to fail. - require.Error(t, err) - require.Contains(t, conflictContents.String(), "<<<<<<") + filesJSON, err := json.Marshal(files) + require.NoError(t, err) - filesJSON, err := json.Marshal([]map[string]interface{}{ - { - "old_path": "files/ruby/popen.rb", - "new_path": "files/ruby/popen.rb", - "content": conflictContents.String(), - }, - { - "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", - }, - }, - }) - require.NoError(t, err) - - ctx = testhelper.MergeOutgoingMetadata(ctx, testcfg.GitalyServersMetadataFromCfg(t, cfg)) - stream, err := client.ResolveConflicts(ctx) - require.NoError(t, err) - - require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_Header{ - Header: &gitalypb.ResolveConflictsRequestHeader{ - Repository: repoProto, - TargetRepository: repoProto, - CommitMessage: []byte(conflictResolutionCommitMessage), - OurCommitOid: sourceOID.String(), - TheirCommitOid: targetOID.String(), - SourceBranch: []byte(sourceBranch), - TargetBranch: []byte(targetBranch), - User: user, + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON}, + }, + expectedError: structerr.NewInternal(`resolve: parse conflict for "a": unexpected conflict delimiter`), + skipCommitCheck: true, + } }, }, - })) - require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_FilesJson{ - FilesJson: filesJSON, - }, - })) - - response, err := stream.CloseAndRecv() - require.NoError(t, err) - testhelper.ProtoEqual(t, &gitalypb.ResolveConflictsResponse{ - ResolutionError: "Resolved content has no changes for file files/ruby/popen.rb", - }, response) -} - -func TestResolveConflictsStableID(t *testing.T) { - ctx := testhelper.Context(t) - - hookManager := hook.NewMockManager(t, hook.NopPreReceive, hook.NopPostReceive, hook.NopUpdate, hook.NopReferenceTransaction) - cfg, repoProto, _, client := setupConflictsService(t, ctx, hookManager) + { + "invalid OID", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) - repo := localrepo.NewTestRepo(t, cfg, repoProto) + gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "apricot"})) + gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "acai"})) - md := testcfg.GitalyServersMetadataFromCfg(t, cfg) - ctx = testhelper.MergeOutgoingMetadata(ctx, md) + files := []map[string]interface{}{} - stream, err := client.ResolveConflicts(ctx) - require.NoError(t, err) + filesJSON, err := json.Marshal(files) + require.NoError(t, err) - require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_Header{ - Header: &gitalypb.ResolveConflictsRequestHeader{ - Repository: repoProto, - TargetRepository: repoProto, - CommitMessage: []byte(conflictResolutionCommitMessage), - OurCommitOid: "1450cd639e0bc6721eb02800169e464f212cde06", - TheirCommitOid: "824be604a34828eb682305f0d963056cfac87b2d", - SourceBranch: []byte("conflict-resolvable"), - TargetBranch: []byte("conflict-start"), - User: user, - Timestamp: ×tamppb.Timestamp{Seconds: 12345}, + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: "conflict-resolvable", + TheirCommitOid: "conflict-start", + SourceBranch: []byte("ours"), + TargetBranch: []byte("theirs"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON}, + }, + expectedError: structerr.NewInternal("Rugged::InvalidError: unable to parse OID - contains invalid characters"), + skipCommitCheck: true, + } }, }, - })) - - filesJSON, err := json.Marshal(files) - require.NoError(t, err) - require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_FilesJson{ - FilesJson: filesJSON, - }, - })) - - response, err := stream.CloseAndRecv() - require.NoError(t, err) - require.Empty(t, response.GetResolutionError()) - - resolvedCommit, err := repo.ReadCommit(ctx, git.Revision("conflict-resolvable")) - require.NoError(t, err) - require.Equal(t, &gitalypb.GitCommit{ - Id: "a5ad028fd739d7a054b07c293e77c5b7aecc2435", - TreeId: "febd97e4a09e71355a513d7e0b0b3808e2dabd28", - ParentIds: []string{ - "1450cd639e0bc6721eb02800169e464f212cde06", - "824be604a34828eb682305f0d963056cfac87b2d", - }, - Subject: []byte(conflictResolutionCommitMessage), - Body: []byte(conflictResolutionCommitMessage), - BodySize: 15, - Author: &gitalypb.CommitAuthor{ - Name: user.Name, - Email: user.Email, - Date: ×tamppb.Timestamp{Seconds: 12345}, - Timezone: []byte("+0000"), - }, - Committer: &gitalypb.CommitAuthor{ - Name: user.Name, - Email: user.Email, - Date: ×tamppb.Timestamp{Seconds: 12345}, - Timezone: []byte("+0000"), - }, - }, resolvedCommit) -} - -func TestFailedResolveConflictsRequestDueToResolutionError(t *testing.T) { - ctx := testhelper.Context(t) - hookManager := hook.NewMockManager(t, hook.NopPreReceive, hook.NopPostReceive, hook.NopUpdate, hook.NopReferenceTransaction) - cfg, repo, _, client := setupConflictsService(t, ctx, hookManager) + { + "resolved content is same as the conflict", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "apricot"})) + theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "acai"})) + + files := []map[string]interface{}{ + { + "old_path": "a", + "new_path": "a", + "content": "<<<<<<< a\napricot\n=======\nacai\n>>>>>>> a\n", + }, + } - mdGS := testcfg.GitalyServersMetadataFromCfg(t, cfg) - mdFF, _ := metadata.FromOutgoingContext(ctx) - ctx = metadata.NewOutgoingContext(ctx, metadata.Join(mdGS, mdFF)) + filesJSON, err := json.Marshal(files) + require.NoError(t, err) - files := []map[string]interface{}{ - { - "old_path": "files/ruby/popen.rb", - "new_path": "files/ruby/popen.rb", - "content": "", + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON}, + }, + expectedResponse: &gitalypb.ResolveConflictsResponse{ResolutionError: "Resolved content has no changes for file a"}, + skipCommitCheck: true, + } + }, }, { - "old_path": "files/ruby/regex.rb", - "new_path": "files/ruby/regex.rb", - "sections": map[string]string{ - "6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9": "head", + "missing resolution for section", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "apricot"})) + theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("theirs"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "acai"})) + + files := []map[string]interface{}{ + { + "old_path": "a", + "new_path": "a", + "sections": map[string]string{ + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 4, 4): "head", + }, + }, + } + + filesJSON, err := json.Marshal(files) + require.NoError(t, err) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON}, + }, + expectedResponse: &gitalypb.ResolveConflictsResponse{ + ResolutionError: fmt.Sprintf("Missing resolution for section ID: %x_%d_%d", sha1.Sum([]byte("a")), 1, 1), + }, + skipCommitCheck: true, + } }, }, - } - filesJSON, err := json.Marshal(files) - require.NoError(t, err) - - headerRequest := &gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_Header{ - Header: &gitalypb.ResolveConflictsRequestHeader{ - Repository: repo, - TargetRepository: repo, - CommitMessage: []byte(conflictResolutionCommitMessage), - OurCommitOid: "1450cd639e0bc6721eb02800169e464f212cde06", - TheirCommitOid: "824be604a34828eb682305f0d963056cfac87b2d", - SourceBranch: []byte("conflict-resolvable"), - TargetBranch: []byte("conflict-start"), - User: user, + { + "empty User", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + TheirCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + Timestamp: ×tamppb.Timestamp{}, + }, + }, + skipCommitCheck: true, + expectedError: structerr.NewInvalidArgument("empty User"), + } }, }, - } - filesRequest := &gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.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) { - ctx := testhelper.Context(t) - hookManager := hook.NewMockManager(t, hook.NopPreReceive, hook.NopPostReceive, hook.NopUpdate, hook.NopReferenceTransaction) - cfg, repo, _, client := setupConflictsService(t, ctx, hookManager) - - mdGS := testcfg.GitalyServersMetadataFromCfg(t, cfg) - ourCommitOid := "1450cd639e0bc6721eb02800169e464f212cde06" - theirCommitOid := "824be604a34828eb682305f0d963056cfac87b2d" - commitMsg := []byte(conflictResolutionCommitMessage) - sourceBranch := []byte("conflict-resolvable") - targetBranch := []byte("conflict-start") - - testCases := []struct { - desc string - header *gitalypb.ResolveConflictsRequestHeader - expectedErr error - }{ { - desc: "empty user", - header: &gitalypb.ResolveConflictsRequestHeader{ - User: nil, - Repository: repo, - OurCommitOid: ourCommitOid, - TargetRepository: repo, - TheirCommitOid: theirCommitOid, - CommitMessage: commitMsg, - SourceBranch: sourceBranch, - TargetBranch: targetBranch, + "empty Repository", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + User: user, + TargetRepository: repo, + OurCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + TheirCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + Timestamp: ×tamppb.Timestamp{}, + }, + }, + skipCommitCheck: true, + expectedError: structerr.NewInvalidArgument(testhelper.GitalyOrPraefect( + "repository not set", + "repo scoped: repository not set", + )), + } }, - expectedErr: status.Error(codes.InvalidArgument, "empty User"), }, { - desc: "empty repo", - header: &gitalypb.ResolveConflictsRequestHeader{ - User: user, - Repository: nil, - OurCommitOid: ourCommitOid, - TargetRepository: repo, - TheirCommitOid: theirCommitOid, - CommitMessage: commitMsg, - SourceBranch: sourceBranch, - TargetBranch: targetBranch, + "empty TargetRepository", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + User: user, + Repository: repo, + OurCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + TheirCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + Timestamp: ×tamppb.Timestamp{}, + }, + }, + skipCommitCheck: true, + expectedError: structerr.NewInvalidArgument("empty TargetRepository"), + } }, - expectedErr: testhelper.GitalyOrPraefect( - structerr.NewInvalidArgument("%w", storage.ErrRepositoryNotSet), - structerr.NewInvalidArgument("repo scoped: %w", storage.ErrRepositoryNotSet), - ), }, { - desc: "empty target repo", - header: &gitalypb.ResolveConflictsRequestHeader{ - User: user, - Repository: repo, - OurCommitOid: ourCommitOid, - TargetRepository: nil, - TheirCommitOid: theirCommitOid, - CommitMessage: commitMsg, - SourceBranch: sourceBranch, - TargetBranch: targetBranch, + "empty OurCommitID", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + User: user, + Repository: repo, + TargetRepository: repo, + TheirCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + Timestamp: ×tamppb.Timestamp{}, + }, + }, + skipCommitCheck: true, + expectedError: structerr.NewInvalidArgument("empty OurCommitOid"), + } }, - expectedErr: status.Error(codes.InvalidArgument, "empty TargetRepository"), }, { - desc: "empty OurCommitId repo", - header: &gitalypb.ResolveConflictsRequestHeader{ - User: user, - Repository: repo, - OurCommitOid: "", - TargetRepository: repo, - TheirCommitOid: theirCommitOid, - CommitMessage: commitMsg, - SourceBranch: sourceBranch, - TargetBranch: targetBranch, + "empty TheirCommitID", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + User: user, + Repository: repo, + TargetRepository: repo, + OurCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + Timestamp: ×tamppb.Timestamp{}, + }, + }, + skipCommitCheck: true, + expectedError: structerr.NewInvalidArgument("empty TheirCommitOid"), + } }, - expectedErr: status.Error(codes.InvalidArgument, "empty OurCommitOid"), }, { - desc: "empty TheirCommitId repo", - header: &gitalypb.ResolveConflictsRequestHeader{ - User: user, - Repository: repo, - OurCommitOid: ourCommitOid, - TargetRepository: repo, - TheirCommitOid: "", - CommitMessage: commitMsg, - SourceBranch: sourceBranch, - TargetBranch: targetBranch, + "empty CommitMessage", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + User: user, + Repository: repo, + TargetRepository: repo, + OurCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + TheirCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + Timestamp: ×tamppb.Timestamp{}, + }, + }, + skipCommitCheck: true, + expectedError: structerr.NewInvalidArgument("empty CommitMessage"), + } }, - expectedErr: status.Error(codes.InvalidArgument, "empty TheirCommitOid"), }, { - desc: "empty CommitMessage repo", - header: &gitalypb.ResolveConflictsRequestHeader{ - User: user, - Repository: repo, - OurCommitOid: ourCommitOid, - TargetRepository: repo, - TheirCommitOid: theirCommitOid, - CommitMessage: nil, - SourceBranch: sourceBranch, - TargetBranch: targetBranch, + "empty SourceBranch", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + User: user, + Repository: repo, + TargetRepository: repo, + OurCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + TheirCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + TargetBranch: []byte("theirs"), + CommitMessage: []byte(conflictResolutionCommitMessage), + Timestamp: ×tamppb.Timestamp{}, + }, + }, + skipCommitCheck: true, + expectedError: structerr.NewInvalidArgument("empty SourceBranch"), + } }, - expectedErr: status.Error(codes.InvalidArgument, "empty CommitMessage"), }, { - desc: "empty SourceBranch repo", - header: &gitalypb.ResolveConflictsRequestHeader{ - User: user, - Repository: repo, - OurCommitOid: ourCommitOid, - TargetRepository: repo, - TheirCommitOid: theirCommitOid, - CommitMessage: commitMsg, - SourceBranch: nil, - TargetBranch: targetBranch, + "empty TargetBranch", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + User: user, + Repository: repo, + TargetRepository: repo, + OurCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + TheirCommitOid: gittest.DefaultObjectHash.EmptyTreeOID.String(), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + Timestamp: ×tamppb.Timestamp{}, + }, + }, + skipCommitCheck: true, + expectedError: structerr.NewInvalidArgument("empty TargetBranch"), + } }, - expectedErr: status.Error(codes.InvalidArgument, "empty SourceBranch"), }, { - desc: "empty TargetBranch repo", - header: &gitalypb.ResolveConflictsRequestHeader{ - User: user, - Repository: repo, - OurCommitOid: ourCommitOid, - TargetRepository: repo, - TheirCommitOid: theirCommitOid, - CommitMessage: commitMsg, - SourceBranch: sourceBranch, - TargetBranch: nil, + "uses quarantine repo", + func(tb testing.TB, ctx context.Context) setupData { + cfg, client := setupConflictsService(tb, nil) + repo, repoPath := gittest.CreateRepository(tb, ctx, cfg) + + testcfg.BuildGitalySSH(t, cfg) + testcfg.BuildGitalyHooks(t, cfg) + + // We set up a custom "pre-receive" hook which simply prints the commits content to stdout and + // then exits with an error. Like this, we can both assert that the hook can see the + // quarantined tag, and it allows us to fail the RPC before we migrate quarantined objects. + gittest.WriteCustomHook(t, repoPath, "pre-receive", []byte( + `#!/bin/sh + read oldval newval ref && + git cat-file -p $newval^{commit}:a && + exit 1 + `)) + + baseCommit := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("ours"), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "apple"})) + ourCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithParents(baseCommit), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "apricot"})) + theirCommitID := gittest.WriteCommit(tb, cfg, repoPath, gittest.WithBranch("theirs"), + gittest.WithParents(baseCommit), + gittest.WithTreeEntries(gittest.TreeEntry{Path: "a", Mode: "100644", Content: "acai"})) + + files := []map[string]interface{}{ + { + "old_path": "a", + "new_path": "a", + "sections": map[string]string{ + fmt.Sprintf("%x_%d_%d", sha1.Sum([]byte("a")), 1, 1): "head", + }, + }, + } + + filesJSON, err := json.Marshal(files) + require.NoError(t, err) + + objectsBefore := len(strings.Split(text.ChompBytes(gittest.Exec(t, cfg, "-C", repoPath, "rev-list", "--objects", "--all")), "\n")) + + return setupData{ + cfg: cfg, + client: client, + repoPath: repoPath, + repo: repo, + requestHeader: &gitalypb.ResolveConflictsRequest_Header{ + Header: &gitalypb.ResolveConflictsRequestHeader{ + Repository: repo, + TargetRepository: repo, + OurCommitOid: ourCommitID.String(), + TheirCommitOid: theirCommitID.String(), + TargetBranch: []byte("theirs"), + SourceBranch: []byte("ours"), + CommitMessage: []byte(conflictResolutionCommitMessage), + User: user, + Timestamp: ×tamppb.Timestamp{}, + }, + }, + requestsFilesJSON: []*gitalypb.ResolveConflictsRequest_FilesJson{ + {FilesJson: filesJSON}, + }, + expectedContent: map[string]map[string][]byte{ + "refs/heads/ours": { + "a": []byte("apple"), + }, + }, + expectedError: structerr.NewInternal("running pre-receive hooks: apricot"), + skipCommitCheck: true, + additionalChecks: func() { + objectsAfter := len(strings.Split(text.ChompBytes(gittest.Exec(t, cfg, "-C", repoPath, "rev-list", "--objects", "--all")), "\n")) + require.Equal(t, objectsBefore, objectsAfter, "No new objets should've been added") + }, + } }, - expectedErr: status.Error(codes.InvalidArgument, "empty TargetBranch"), }, - } + } { + tc := tc - for _, testCase := range testCases { - t.Run(testCase.desc, func(t *testing.T) { + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + ctx := testhelper.Context(t) + setup := tc.setup(t, ctx) + + mdGS := testcfg.GitalyServersMetadataFromCfg(t, setup.cfg) mdFF, _ := metadata.FromOutgoingContext(ctx) ctx = metadata.NewOutgoingContext(ctx, metadata.Join(mdGS, mdFF)) - stream, err := client.ResolveConflicts(ctx) + stream, err := setup.client.ResolveConflicts(ctx) require.NoError(t, err) - headerRequest := &gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_Header{ - Header: testCase.header, - }, + require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ResolveConflictsRequestPayload: setup.requestHeader})) + for _, req := range setup.requestsFilesJSON { + require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ResolveConflictsRequestPayload: req})) + } + + r, err := stream.CloseAndRecv() + testhelper.RequireGrpcError(t, setup.expectedError, err) + testhelper.ProtoEqual(t, setup.expectedResponse, r) + + for branch, pathAndContent := range setup.expectedContent { + for path, content := range pathAndContent { + actual := gittest.Exec(t, setup.cfg, "-C", setup.repoPath, "cat-file", "-p", fmt.Sprintf("%s:%s", branch, path)) + require.Equal(t, content, actual) + } } - require.NoError(t, stream.Send(headerRequest)) - _, err = stream.CloseAndRecv() - testhelper.RequireGrpcError(t, testCase.expectedErr, err) + if setup.requestHeader.Header != nil && !setup.skipCommitCheck { + repo := localrepo.NewTestRepo(t, setup.cfg, setup.repo) + headCommit, err := repo.ReadCommit(ctx, git.Revision(setup.requestHeader.Header.SourceBranch)) + require.NoError(t, err) + require.Contains(t, headCommit.ParentIds, setup.requestHeader.Header.OurCommitOid) + require.Contains(t, headCommit.ParentIds, setup.requestHeader.Header.TheirCommitOid) + require.Equal(t, headCommit.Author.Email, user.Email) + require.Equal(t, headCommit.Committer.Email, user.Email) + require.Equal(t, string(headCommit.Subject), conflictResolutionCommitMessage) + } + + if setup.additionalChecks != nil { + setup.additionalChecks() + } }) } } - -func TestResolveConflictsQuarantine(t *testing.T) { - t.Parallel() - - ctx := testhelper.Context(t) - cfg, sourceRepoProto, sourceRepoPath, client := setupConflictsService(t, ctx, nil) - - testcfg.BuildGitalySSH(t, cfg) - testcfg.BuildGitalyHooks(t, cfg) - - sourceBlobOID := gittest.WriteBlob(t, cfg, sourceRepoPath, []byte("contents-1\n")) - sourceCommitOID := gittest.WriteCommit(t, cfg, sourceRepoPath, - gittest.WithParents("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"), - gittest.WithTreeEntries(gittest.TreeEntry{ - Path: "file.txt", OID: sourceBlobOID, Mode: "100644", - }), - ) - gittest.Exec(t, cfg, "-C", sourceRepoPath, "update-ref", "refs/heads/source", sourceCommitOID.String()) - - // We set up a custom "pre-receive" hook which simply prints the new commit to stdout and - // then exits with an error. Like this, we can both assert that the hook can see the - // quarantined tag, and it allows us to fail the RPC before we migrate quarantined objects. - gittest.WriteCustomHook(t, sourceRepoPath, "pre-receive", []byte( - `#!/bin/sh - read oldval newval ref && - echo $newval && - git cat-file -p $newval^{commit} && - exit 1 - `)) - - targetRepoProto, targetRepoPath := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{ - Seed: gittest.SeedGitLabTest, - }) - targetBlobOID := gittest.WriteBlob(t, cfg, targetRepoPath, []byte("contents-2\n")) - targetCommitOID := gittest.WriteCommit(t, cfg, targetRepoPath, - gittest.WithParents("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"), - gittest.WithTreeEntries(gittest.TreeEntry{ - Path: "file.txt", OID: targetBlobOID, Mode: "100644", - }), - ) - gittest.Exec(t, cfg, "-C", targetRepoPath, "update-ref", "refs/heads/target", targetCommitOID.String()) - - ctx = testhelper.MergeOutgoingMetadata(ctx, testcfg.GitalyServersMetadataFromCfg(t, cfg)) - - stream, err := client.ResolveConflicts(ctx) - require.NoError(t, err) - - filesJSON, err := json.Marshal([]map[string]interface{}{ - { - "old_path": "file.txt", - "new_path": "file.txt", - "sections": map[string]string{ - "5436437fa01a7d3e41d46741da54b451446774ca_1_1": "origin", - }, - }, - }) - require.NoError(t, err) - - require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_Header{ - Header: &gitalypb.ResolveConflictsRequestHeader{ - Repository: sourceRepoProto, - TargetRepository: targetRepoProto, - CommitMessage: []byte(conflictResolutionCommitMessage), - OurCommitOid: sourceCommitOID.String(), - TheirCommitOid: targetCommitOID.String(), - SourceBranch: []byte("source"), - TargetBranch: []byte("target"), - User: user, - Timestamp: ×tamppb.Timestamp{Seconds: 12345}, - }, - }, - })) - require.NoError(t, stream.Send(&gitalypb.ResolveConflictsRequest{ - ResolveConflictsRequestPayload: &gitalypb.ResolveConflictsRequest_FilesJson{ - FilesJson: filesJSON, - }, - })) - - response, err := stream.CloseAndRecv() - require.EqualError(t, err, `rpc error: code = Internal desc = running pre-receive hooks: af339cb882d1e3cf8d6751651e58bbaff0265d6e -tree 89fad81bbfa38070b90ca8f4c404625bf0999013 -parent 29449b1d52cd77fd060a083a1de691bbaf12d8af -parent 26dac52be85c92742b2c0c19eb7303de9feccb63 -author John Doe <johndoe@gitlab.com> 12345 +0000 -committer John Doe <johndoe@gitlab.com> 12345 +0000 - -Solve conflicts`) - require.Empty(t, response.GetResolutionError()) - - // The file shouldn't have been updated and is thus expected to still have the same old - // contents. - require.Equal(t, []byte("contents-1\n"), gittest.Exec(t, cfg, "-C", sourceRepoPath, "cat-file", "-p", "refs/heads/source:file.txt")) - - // In case we use an object quarantine directory, the tag should not exist in the target - // repository because the RPC failed to update the revision. - exists, err := localrepo.NewTestRepo(t, cfg, sourceRepoProto).HasRevision(ctx, "af339cb882d1e3cf8d6751651e58bbaff0265d6e^{commit}") - require.NoError(t, err) - require.False(t, exists, "object should have not been migrated") -} diff --git a/internal/gitaly/service/conflicts/testhelper_test.go b/internal/gitaly/service/conflicts/testhelper_test.go index 02c45f3d3..75eaf1cfa 100644 --- a/internal/gitaly/service/conflicts/testhelper_test.go +++ b/internal/gitaly/service/conflicts/testhelper_test.go @@ -3,10 +3,8 @@ package conflicts import ( - "context" "testing" - "gitlab.com/gitlab-org/gitaly/v16/internal/git/gittest" "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/config" "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/hook" "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/service" @@ -26,7 +24,7 @@ func TestMain(m *testing.M) { testhelper.Run(m) } -func setupConflictsServiceWithoutRepo(tb testing.TB, hookManager hook.Manager) (config.Cfg, gitalypb.ConflictsServiceClient) { +func setupConflictsService(tb testing.TB, hookManager hook.Manager) (config.Cfg, gitalypb.ConflictsServiceClient) { cfg := testcfg.Build(tb) testcfg.BuildGitalyGit2Go(tb, cfg) @@ -40,16 +38,6 @@ func setupConflictsServiceWithoutRepo(tb testing.TB, hookManager hook.Manager) ( return cfg, client } -func setupConflictsService(tb testing.TB, ctx context.Context, hookManager hook.Manager) (config.Cfg, *gitalypb.Repository, string, gitalypb.ConflictsServiceClient) { - cfg, client := setupConflictsServiceWithoutRepo(tb, hookManager) - - repo, repoPath := gittest.CreateRepository(tb, ctx, cfg, gittest.CreateRepositoryConfig{ - Seed: gittest.SeedGitLabTest, - }) - - return cfg, repo, repoPath, client -} - func runConflictsServer(tb testing.TB, cfg config.Cfg, hookManager hook.Manager) string { return testserver.RunGitalyServer(tb, cfg, func(srv *grpc.Server, deps *service.Dependencies) { gitalypb.RegisterConflictsServiceServer(srv, NewServer( |