1package conflicts 2 3import ( 4 "bytes" 5 "context" 6 "io" 7 "io/ioutil" 8 "path/filepath" 9 "testing" 10 11 "github.com/stretchr/testify/require" 12 "gitlab.com/gitlab-org/gitaly/v14/internal/git" 13 "gitlab.com/gitlab-org/gitaly/v14/internal/git/gittest" 14 "gitlab.com/gitlab-org/gitaly/v14/internal/git/localrepo" 15 "gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/config" 16 "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper" 17 "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" 18 "google.golang.org/grpc/codes" 19) 20 21type conflictFile struct { 22 header *gitalypb.ConflictFileHeader 23 content []byte 24} 25 26func TestSuccessfulListConflictFilesRequest(t *testing.T) { 27 ctx, cleanup := testhelper.Context() 28 defer cleanup() 29 30 _, repo, _, client := SetupConflictsService(t, false) 31 32 ourCommitOid := "1a35b5a77cf6af7edf6703f88e82f6aff613666f" 33 theirCommitOid := "8309e68585b28d61eb85b7e2834849dda6bf1733" 34 35 conflictContent1 := `<<<<<<< encoding/codagé 36Content is not important, file name is 37======= 38Content can be important, but here, file name is of utmost importance 39>>>>>>> encoding/codagé 40` 41 conflictContent2 := `<<<<<<< files/ruby/feature.rb 42class Feature 43 def foo 44 puts 'bar' 45 end 46======= 47# This file was changed in feature branch 48# We put different code here to make merge conflict 49class Conflict 50>>>>>>> files/ruby/feature.rb 51end 52` 53 54 request := &gitalypb.ListConflictFilesRequest{ 55 Repository: repo, 56 OurCommitOid: ourCommitOid, 57 TheirCommitOid: theirCommitOid, 58 } 59 60 c, err := client.ListConflictFiles(ctx, request) 61 require.NoError(t, err) 62 63 expectedFiles := []*conflictFile{ 64 { 65 header: &gitalypb.ConflictFileHeader{ 66 CommitOid: ourCommitOid, 67 OurMode: int32(0100644), 68 OurPath: []byte("encoding/codagé"), 69 TheirPath: []byte("encoding/codagé"), 70 }, 71 content: []byte(conflictContent1), 72 }, 73 { 74 header: &gitalypb.ConflictFileHeader{ 75 CommitOid: ourCommitOid, 76 OurMode: int32(0100644), 77 OurPath: []byte("files/ruby/feature.rb"), 78 TheirPath: []byte("files/ruby/feature.rb"), 79 }, 80 content: []byte(conflictContent2), 81 }, 82 } 83 84 receivedFiles := getConflictFiles(t, c) 85 require.Len(t, receivedFiles, len(expectedFiles)) 86 87 for i := 0; i < len(expectedFiles); i++ { 88 testhelper.ProtoEqual(t, receivedFiles[i].header, expectedFiles[i].header) 89 require.Equal(t, expectedFiles[i].content, receivedFiles[i].content) 90 } 91} 92 93func TestSuccessfulListConflictFilesRequestWithAncestor(t *testing.T) { 94 ctx, cleanup := testhelper.Context() 95 defer cleanup() 96 97 _, repo, _, client := SetupConflictsService(t, true) 98 99 ourCommitOid := "824be604a34828eb682305f0d963056cfac87b2d" 100 theirCommitOid := "1450cd639e0bc6721eb02800169e464f212cde06" 101 102 request := &gitalypb.ListConflictFilesRequest{ 103 Repository: repo, 104 OurCommitOid: ourCommitOid, 105 TheirCommitOid: theirCommitOid, 106 } 107 108 c, err := client.ListConflictFiles(ctx, request) 109 require.NoError(t, err) 110 111 expectedFiles := []*conflictFile{ 112 { 113 header: &gitalypb.ConflictFileHeader{ 114 CommitOid: ourCommitOid, 115 OurMode: int32(0100644), 116 OurPath: []byte("files/ruby/popen.rb"), 117 TheirPath: []byte("files/ruby/popen.rb"), 118 AncestorPath: []byte("files/ruby/popen.rb"), 119 }, 120 }, 121 { 122 header: &gitalypb.ConflictFileHeader{ 123 CommitOid: ourCommitOid, 124 OurMode: int32(0100644), 125 OurPath: []byte("files/ruby/regex.rb"), 126 TheirPath: []byte("files/ruby/regex.rb"), 127 AncestorPath: []byte("files/ruby/regex.rb"), 128 }, 129 }, 130 } 131 132 receivedFiles := getConflictFiles(t, c) 133 require.Len(t, receivedFiles, len(expectedFiles)) 134 135 for i := 0; i < len(expectedFiles); i++ { 136 testhelper.ProtoEqual(t, receivedFiles[i].header, expectedFiles[i].header) 137 } 138} 139 140func TestListConflictFilesHugeDiff(t *testing.T) { 141 ctx, cleanup := testhelper.Context() 142 defer cleanup() 143 144 cfg, repo, repoPath, client := SetupConflictsService(t, false) 145 146 our := buildCommit(t, ctx, cfg, repo, repoPath, map[string][]byte{ 147 "a": bytes.Repeat([]byte("a\n"), 128*1024), 148 "b": bytes.Repeat([]byte("b\n"), 128*1024), 149 }) 150 151 their := buildCommit(t, ctx, cfg, repo, repoPath, map[string][]byte{ 152 "a": bytes.Repeat([]byte("x\n"), 128*1024), 153 "b": bytes.Repeat([]byte("y\n"), 128*1024), 154 }) 155 156 request := &gitalypb.ListConflictFilesRequest{ 157 Repository: repo, 158 OurCommitOid: our, 159 TheirCommitOid: their, 160 } 161 162 c, err := client.ListConflictFiles(ctx, request) 163 require.NoError(t, err) 164 165 receivedFiles := getConflictFiles(t, c) 166 require.Len(t, receivedFiles, 2) 167 testhelper.ProtoEqual(t, &gitalypb.ConflictFileHeader{ 168 CommitOid: our, 169 OurMode: int32(0100644), 170 OurPath: []byte("a"), 171 TheirPath: []byte("a"), 172 }, receivedFiles[0].header) 173 174 testhelper.ProtoEqual(t, &gitalypb.ConflictFileHeader{ 175 CommitOid: our, 176 OurMode: int32(0100644), 177 OurPath: []byte("b"), 178 TheirPath: []byte("b"), 179 }, receivedFiles[1].header) 180} 181 182func buildCommit(t *testing.T, ctx context.Context, cfg config.Cfg, repo *gitalypb.Repository, repoPath string, files map[string][]byte) string { 183 t.Helper() 184 185 for file, contents := range files { 186 filePath := filepath.Join(repoPath, file) 187 require.NoError(t, ioutil.WriteFile(filePath, contents, 0666)) 188 gittest.Exec(t, cfg, "-C", repoPath, "add", filePath) 189 } 190 191 gittest.Exec(t, cfg, "-C", repoPath, "commit", "-m", "message") 192 193 oid, err := localrepo.NewTestRepo(t, cfg, repo).ResolveRevision(ctx, git.Revision("HEAD")) 194 require.NoError(t, err) 195 196 gittest.Exec(t, cfg, "-C", repoPath, "reset", "--hard", "HEAD~") 197 198 return oid.String() 199} 200 201func TestListConflictFilesFailedPrecondition(t *testing.T) { 202 ctx, cleanup := testhelper.Context() 203 defer cleanup() 204 205 _, repo, _, client := SetupConflictsService(t, true) 206 207 testCases := []struct { 208 desc string 209 ourCommitOid string 210 theirCommitOid string 211 }{ 212 { 213 desc: "conflict side missing", 214 ourCommitOid: "eb227b3e214624708c474bdab7bde7afc17cefcc", 215 theirCommitOid: "824be604a34828eb682305f0d963056cfac87b2d", 216 }, 217 { 218 // These commits have a conflict on the 'VERSION' file in the test repo. 219 // The conflict is expected to raise an encoding error. 220 desc: "encoding error", 221 ourCommitOid: "bd493d44ae3c4dd84ce89cb75be78c4708cbd548", 222 theirCommitOid: "7df99c9ad5b8c9bfc5ae4fb7a91cc87adcce02ef", 223 }, 224 { 225 desc: "submodule object lookup error", 226 ourCommitOid: "de78448b0b504f3f60093727bddfda1ceee42345", 227 theirCommitOid: "2f61d70f862c6a4f782ef7933e020a118282db29", 228 }, 229 { 230 desc: "invalid commit id on 'our' side", 231 ourCommitOid: "abcdef0000000000000000000000000000000000", 232 theirCommitOid: "1a35b5a77cf6af7edf6703f88e82f6aff613666f", 233 }, 234 { 235 desc: "invalid commit id on 'their' side", 236 ourCommitOid: "1a35b5a77cf6af7edf6703f88e82f6aff613666f", 237 theirCommitOid: "abcdef0000000000000000000000000000000000", 238 }, 239 } 240 241 for _, tc := range testCases { 242 t.Run(tc.desc, func(t *testing.T) { 243 request := &gitalypb.ListConflictFilesRequest{ 244 Repository: repo, 245 OurCommitOid: tc.ourCommitOid, 246 TheirCommitOid: tc.theirCommitOid, 247 } 248 249 c, err := client.ListConflictFiles(ctx, request) 250 if err == nil { 251 err = drainListConflictFilesResponse(c) 252 } 253 254 testhelper.RequireGrpcError(t, err, codes.FailedPrecondition) 255 }) 256 } 257} 258 259func TestFailedListConflictFilesRequestDueToValidation(t *testing.T) { 260 ctx, cleanup := testhelper.Context() 261 defer cleanup() 262 263 _, repo, _, client := SetupConflictsService(t, true) 264 265 ourCommitOid := "0b4bc9a49b562e85de7cc9e834518ea6828729b9" 266 theirCommitOid := "bb5206fee213d983da88c47f9cf4cc6caf9c66dc" 267 268 testCases := []struct { 269 desc string 270 request *gitalypb.ListConflictFilesRequest 271 code codes.Code 272 }{ 273 { 274 desc: "empty repo", 275 request: &gitalypb.ListConflictFilesRequest{ 276 Repository: nil, 277 OurCommitOid: ourCommitOid, 278 TheirCommitOid: theirCommitOid, 279 }, 280 code: codes.InvalidArgument, 281 }, 282 { 283 desc: "empty OurCommitId field", 284 request: &gitalypb.ListConflictFilesRequest{ 285 Repository: repo, 286 OurCommitOid: "", 287 TheirCommitOid: theirCommitOid, 288 }, 289 code: codes.InvalidArgument, 290 }, 291 { 292 desc: "empty TheirCommitId field", 293 request: &gitalypb.ListConflictFilesRequest{ 294 Repository: repo, 295 OurCommitOid: ourCommitOid, 296 TheirCommitOid: "", 297 }, 298 code: codes.InvalidArgument, 299 }, 300 } 301 302 for _, testCase := range testCases { 303 t.Run(testCase.desc, func(t *testing.T) { 304 c, _ := client.ListConflictFiles(ctx, testCase.request) 305 testhelper.RequireGrpcError(t, drainListConflictFilesResponse(c), testCase.code) 306 }) 307 } 308} 309 310func getConflictFiles(t *testing.T, c gitalypb.ConflictsService_ListConflictFilesClient) []*conflictFile { 311 t.Helper() 312 313 var files []*conflictFile 314 var currentFile *conflictFile 315 316 for { 317 r, err := c.Recv() 318 if err == io.EOF { 319 break 320 } 321 require.NoError(t, err) 322 323 for _, file := range r.GetFiles() { 324 // If there's a header this is the beginning of a new file 325 if header := file.GetHeader(); header != nil { 326 if currentFile != nil { 327 files = append(files, currentFile) 328 } 329 330 currentFile = &conflictFile{header: header} 331 } else { 332 // Append to current file's content 333 currentFile.content = append(currentFile.content, file.GetContent()...) 334 } 335 } 336 } 337 338 // Append leftover file 339 files = append(files, currentFile) 340 341 return files 342} 343 344func drainListConflictFilesResponse(c gitalypb.ConflictsService_ListConflictFilesClient) error { 345 var err error 346 for err == nil { 347 _, err = c.Recv() 348 } 349 return err 350} 351