package conflicts import ( "bytes" "context" "io" "io/ioutil" "path/filepath" "testing" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/v14/internal/git" "gitlab.com/gitlab-org/gitaly/v14/internal/git/gittest" "gitlab.com/gitlab-org/gitaly/v14/internal/git/localrepo" "gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/config" "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" "google.golang.org/grpc/codes" ) type conflictFile struct { header *gitalypb.ConflictFileHeader content []byte } func TestSuccessfulListConflictFilesRequest(t *testing.T) { ctx, cleanup := testhelper.Context() defer cleanup() _, repo, _, client := SetupConflictsService(t, false) ourCommitOid := "1a35b5a77cf6af7edf6703f88e82f6aff613666f" theirCommitOid := "8309e68585b28d61eb85b7e2834849dda6bf1733" conflictContent1 := `<<<<<<< encoding/codagé Content is not important, file name is ======= Content can be important, but here, file name is of utmost importance >>>>>>> encoding/codagé ` conflictContent2 := `<<<<<<< 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 ` request := &gitalypb.ListConflictFilesRequest{ Repository: repo, OurCommitOid: ourCommitOid, TheirCommitOid: theirCommitOid, } c, err := client.ListConflictFiles(ctx, request) require.NoError(t, err) expectedFiles := []*conflictFile{ { header: &gitalypb.ConflictFileHeader{ CommitOid: ourCommitOid, OurMode: int32(0100644), OurPath: []byte("encoding/codagé"), TheirPath: []byte("encoding/codagé"), }, content: []byte(conflictContent1), }, { header: &gitalypb.ConflictFileHeader{ CommitOid: ourCommitOid, OurMode: int32(0100644), OurPath: []byte("files/ruby/feature.rb"), TheirPath: []byte("files/ruby/feature.rb"), }, content: []byte(conflictContent2), }, } receivedFiles := getConflictFiles(t, c) require.Len(t, receivedFiles, len(expectedFiles)) for i := 0; i < len(expectedFiles); i++ { testhelper.ProtoEqual(t, receivedFiles[i].header, expectedFiles[i].header) require.Equal(t, expectedFiles[i].content, receivedFiles[i].content) } } func TestSuccessfulListConflictFilesRequestWithAncestor(t *testing.T) { ctx, cleanup := testhelper.Context() defer cleanup() _, repo, _, client := SetupConflictsService(t, true) ourCommitOid := "824be604a34828eb682305f0d963056cfac87b2d" theirCommitOid := "1450cd639e0bc6721eb02800169e464f212cde06" request := &gitalypb.ListConflictFilesRequest{ Repository: repo, OurCommitOid: ourCommitOid, TheirCommitOid: theirCommitOid, } c, err := client.ListConflictFiles(ctx, request) require.NoError(t, err) expectedFiles := []*conflictFile{ { header: &gitalypb.ConflictFileHeader{ CommitOid: ourCommitOid, OurMode: int32(0100644), OurPath: []byte("files/ruby/popen.rb"), TheirPath: []byte("files/ruby/popen.rb"), AncestorPath: []byte("files/ruby/popen.rb"), }, }, { header: &gitalypb.ConflictFileHeader{ CommitOid: ourCommitOid, OurMode: int32(0100644), OurPath: []byte("files/ruby/regex.rb"), TheirPath: []byte("files/ruby/regex.rb"), AncestorPath: []byte("files/ruby/regex.rb"), }, }, } receivedFiles := getConflictFiles(t, c) require.Len(t, receivedFiles, len(expectedFiles)) for i := 0; i < len(expectedFiles); i++ { testhelper.ProtoEqual(t, receivedFiles[i].header, expectedFiles[i].header) } } func TestListConflictFilesHugeDiff(t *testing.T) { ctx, cleanup := testhelper.Context() defer cleanup() cfg, repo, repoPath, client := SetupConflictsService(t, false) our := buildCommit(t, ctx, cfg, repo, repoPath, map[string][]byte{ "a": bytes.Repeat([]byte("a\n"), 128*1024), "b": bytes.Repeat([]byte("b\n"), 128*1024), }) their := buildCommit(t, ctx, cfg, repo, repoPath, map[string][]byte{ "a": bytes.Repeat([]byte("x\n"), 128*1024), "b": bytes.Repeat([]byte("y\n"), 128*1024), }) request := &gitalypb.ListConflictFilesRequest{ Repository: repo, OurCommitOid: our, TheirCommitOid: their, } c, err := client.ListConflictFiles(ctx, request) require.NoError(t, err) receivedFiles := getConflictFiles(t, c) require.Len(t, receivedFiles, 2) testhelper.ProtoEqual(t, &gitalypb.ConflictFileHeader{ CommitOid: our, OurMode: int32(0100644), OurPath: []byte("a"), TheirPath: []byte("a"), }, receivedFiles[0].header) testhelper.ProtoEqual(t, &gitalypb.ConflictFileHeader{ CommitOid: our, OurMode: int32(0100644), OurPath: []byte("b"), TheirPath: []byte("b"), }, receivedFiles[1].header) } func buildCommit(t *testing.T, ctx context.Context, cfg config.Cfg, repo *gitalypb.Repository, repoPath string, files map[string][]byte) string { t.Helper() for file, contents := range files { filePath := filepath.Join(repoPath, file) require.NoError(t, ioutil.WriteFile(filePath, contents, 0666)) gittest.Exec(t, cfg, "-C", repoPath, "add", filePath) } gittest.Exec(t, cfg, "-C", repoPath, "commit", "-m", "message") oid, err := localrepo.NewTestRepo(t, cfg, repo).ResolveRevision(ctx, git.Revision("HEAD")) require.NoError(t, err) gittest.Exec(t, cfg, "-C", repoPath, "reset", "--hard", "HEAD~") return oid.String() } func TestListConflictFilesFailedPrecondition(t *testing.T) { ctx, cleanup := testhelper.Context() defer cleanup() _, repo, _, client := SetupConflictsService(t, true) testCases := []struct { desc string ourCommitOid string theirCommitOid string }{ { desc: "conflict side missing", ourCommitOid: "eb227b3e214624708c474bdab7bde7afc17cefcc", theirCommitOid: "824be604a34828eb682305f0d963056cfac87b2d", }, { // These commits have a conflict on the 'VERSION' file in the test repo. // The conflict is expected to raise an encoding error. desc: "encoding error", ourCommitOid: "bd493d44ae3c4dd84ce89cb75be78c4708cbd548", theirCommitOid: "7df99c9ad5b8c9bfc5ae4fb7a91cc87adcce02ef", }, { desc: "submodule object lookup error", ourCommitOid: "de78448b0b504f3f60093727bddfda1ceee42345", theirCommitOid: "2f61d70f862c6a4f782ef7933e020a118282db29", }, { desc: "invalid commit id on 'our' side", ourCommitOid: "abcdef0000000000000000000000000000000000", theirCommitOid: "1a35b5a77cf6af7edf6703f88e82f6aff613666f", }, { desc: "invalid commit id on 'their' side", ourCommitOid: "1a35b5a77cf6af7edf6703f88e82f6aff613666f", theirCommitOid: "abcdef0000000000000000000000000000000000", }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { request := &gitalypb.ListConflictFilesRequest{ Repository: repo, OurCommitOid: tc.ourCommitOid, TheirCommitOid: tc.theirCommitOid, } c, err := client.ListConflictFiles(ctx, request) if err == nil { err = drainListConflictFilesResponse(c) } testhelper.RequireGrpcError(t, err, codes.FailedPrecondition) }) } } func TestFailedListConflictFilesRequestDueToValidation(t *testing.T) { ctx, cleanup := testhelper.Context() defer cleanup() _, repo, _, client := SetupConflictsService(t, true) ourCommitOid := "0b4bc9a49b562e85de7cc9e834518ea6828729b9" theirCommitOid := "bb5206fee213d983da88c47f9cf4cc6caf9c66dc" testCases := []struct { desc string request *gitalypb.ListConflictFilesRequest code codes.Code }{ { desc: "empty repo", request: &gitalypb.ListConflictFilesRequest{ Repository: nil, OurCommitOid: ourCommitOid, TheirCommitOid: theirCommitOid, }, code: codes.InvalidArgument, }, { desc: "empty OurCommitId field", request: &gitalypb.ListConflictFilesRequest{ Repository: repo, OurCommitOid: "", TheirCommitOid: theirCommitOid, }, code: codes.InvalidArgument, }, { desc: "empty TheirCommitId field", request: &gitalypb.ListConflictFilesRequest{ Repository: repo, OurCommitOid: ourCommitOid, TheirCommitOid: "", }, code: codes.InvalidArgument, }, } for _, testCase := range testCases { t.Run(testCase.desc, func(t *testing.T) { c, _ := client.ListConflictFiles(ctx, testCase.request) testhelper.RequireGrpcError(t, drainListConflictFilesResponse(c), testCase.code) }) } } func getConflictFiles(t *testing.T, c gitalypb.ConflictsService_ListConflictFilesClient) []*conflictFile { t.Helper() var files []*conflictFile var currentFile *conflictFile for { r, err := c.Recv() if err == io.EOF { break } require.NoError(t, 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 { if currentFile != nil { 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 gitalypb.ConflictsService_ListConflictFilesClient) error { var err error for err == nil { _, err = c.Recv() } return err }