1package commit 2 3import ( 4 "context" 5 "fmt" 6 "io" 7 "os/exec" 8 "testing" 9 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 "gitlab.com/gitlab-org/gitaly/v14/internal/git/gittest" 13 "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper" 14 "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testassert" 15 "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" 16 "google.golang.org/grpc/codes" 17 "google.golang.org/protobuf/types/known/timestamppb" 18) 19 20func TestFindCommitsFields(t *testing.T) { 21 t.Parallel() 22 windows1251Message := testhelper.MustReadFile(t, "testdata/commit-c809470461118b7bcab850f6e9a7ca97ac42f8ea-message.txt") 23 24 _, repo, _, client := setupCommitServiceWithRepo(t, true) 25 26 testCases := []struct { 27 id string 28 trailers bool 29 commit *gitalypb.GitCommit 30 }{ 31 { 32 id: "b83d6e391c22777fca1ed3012fce84f633d7fed0", 33 commit: testhelper.GitLabTestCommit("b83d6e391c22777fca1ed3012fce84f633d7fed0"), 34 }, 35 { 36 id: "c809470461118b7bcab850f6e9a7ca97ac42f8ea", 37 commit: &gitalypb.GitCommit{ 38 Id: "c809470461118b7bcab850f6e9a7ca97ac42f8ea", 39 Subject: windows1251Message[:len(windows1251Message)-1], 40 Body: windows1251Message, 41 Author: &gitalypb.CommitAuthor{ 42 Name: []byte("Jacob Vosmaer"), 43 Email: []byte("jacob@gitlab.com"), 44 Date: ×tamppb.Timestamp{Seconds: 1512132977}, 45 Timezone: []byte("+0100"), 46 }, 47 Committer: &gitalypb.CommitAuthor{ 48 Name: []byte("Jacob Vosmaer"), 49 Email: []byte("jacob@gitlab.com"), 50 Date: ×tamppb.Timestamp{Seconds: 1512132977}, 51 Timezone: []byte("+0100"), 52 }, 53 ParentIds: []string{"e63f41fe459e62e1228fcef60d7189127aeba95a"}, 54 BodySize: 49, 55 TreeId: "86ec18bfe87ad42a782fdabd8310f9b7ac750f51", 56 }, 57 }, 58 { 59 id: "0999bb770f8dc92ab5581cc0b474b3e31a96bf5c", 60 commit: testhelper.GitLabTestCommit("0999bb770f8dc92ab5581cc0b474b3e31a96bf5c"), 61 }, 62 { 63 id: "77e835ef0856f33c4f0982f84d10bdb0567fe440", 64 commit: testhelper.GitLabTestCommit("77e835ef0856f33c4f0982f84d10bdb0567fe440"), 65 }, 66 { 67 id: "189a6c924013fc3fe40d6f1ec1dc20214183bc97", 68 commit: testhelper.GitLabTestCommit("189a6c924013fc3fe40d6f1ec1dc20214183bc97"), 69 }, 70 { 71 id: "5937ac0a7beb003549fc5fd26fc247adbce4a52e", 72 trailers: false, 73 commit: &gitalypb.GitCommit{ 74 Id: "5937ac0a7beb003549fc5fd26fc247adbce4a52e", 75 Subject: []byte("Add submodule from gitlab.com"), 76 Body: []byte("Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"), 77 Author: &gitalypb.CommitAuthor{ 78 Name: []byte("Dmitriy Zaporozhets"), 79 Email: []byte("dmitriy.zaporozhets@gmail.com"), 80 Date: ×tamppb.Timestamp{Seconds: 1393491698}, 81 Timezone: []byte("+0200"), 82 }, 83 Committer: &gitalypb.CommitAuthor{ 84 Name: []byte("Dmitriy Zaporozhets"), 85 Email: []byte("dmitriy.zaporozhets@gmail.com"), 86 Date: ×tamppb.Timestamp{Seconds: 1393491698}, 87 Timezone: []byte("+0200"), 88 }, 89 ParentIds: []string{"570e7b2abdd848b95f2f578043fc23bd6f6fd24d"}, 90 BodySize: 98, 91 SignatureType: gitalypb.SignatureType_PGP, 92 TreeId: "a6973545d42361b28bfba5ced3b75dba5848b955", 93 }, 94 }, 95 { 96 id: "5937ac0a7beb003549fc5fd26fc247adbce4a52e", 97 trailers: true, 98 commit: &gitalypb.GitCommit{ 99 Id: "5937ac0a7beb003549fc5fd26fc247adbce4a52e", 100 Subject: []byte("Add submodule from gitlab.com"), 101 Body: []byte("Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"), 102 Author: &gitalypb.CommitAuthor{ 103 Name: []byte("Dmitriy Zaporozhets"), 104 Email: []byte("dmitriy.zaporozhets@gmail.com"), 105 Date: ×tamppb.Timestamp{Seconds: 1393491698}, 106 Timezone: []byte("+0200"), 107 }, 108 Committer: &gitalypb.CommitAuthor{ 109 Name: []byte("Dmitriy Zaporozhets"), 110 Email: []byte("dmitriy.zaporozhets@gmail.com"), 111 Date: ×tamppb.Timestamp{Seconds: 1393491698}, 112 Timezone: []byte("+0200"), 113 }, 114 ParentIds: []string{"570e7b2abdd848b95f2f578043fc23bd6f6fd24d"}, 115 BodySize: 98, 116 SignatureType: gitalypb.SignatureType_PGP, 117 TreeId: "a6973545d42361b28bfba5ced3b75dba5848b955", 118 Trailers: []*gitalypb.CommitTrailer{ 119 { 120 Key: []byte("Signed-off-by"), 121 Value: []byte("Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>"), 122 }, 123 }, 124 }, 125 }, 126 { 127 id: "c1c67abbaf91f624347bb3ae96eabe3a1b742478", 128 commit: &gitalypb.GitCommit{ 129 Id: "c1c67abbaf91f624347bb3ae96eabe3a1b742478", 130 Subject: []byte("Add file with a _flattable_ path"), 131 Body: []byte("Add file with a _flattable_ path\n\n\n(cherry picked from commit ce369011c189f62c815f5971d096b26759bab0d1)"), 132 Author: &gitalypb.CommitAuthor{ 133 Name: []byte("Alejandro Rodríguez"), 134 Email: []byte("alejorro70@gmail.com"), 135 Date: ×tamppb.Timestamp{Seconds: 1504382739}, 136 Timezone: []byte("+0000"), 137 }, 138 Committer: &gitalypb.CommitAuthor{ 139 Name: []byte("Drew Blessing"), 140 Email: []byte("drew@blessing.io"), 141 Date: ×tamppb.Timestamp{Seconds: 1540823671}, 142 Timezone: []byte("+0000"), 143 }, 144 ParentIds: []string{"7975be0116940bf2ad4321f79d02a55c5f7779aa"}, 145 BodySize: 103, 146 TreeId: "07f8147e8e73aab6c935c296e8cdc5194dee729b", 147 }, 148 }, 149 } 150 151 for _, tc := range testCases { 152 t.Run(tc.id, func(t *testing.T) { 153 request := &gitalypb.FindCommitsRequest{ 154 Repository: repo, 155 Revision: []byte(tc.id), 156 Trailers: tc.trailers, 157 Limit: 1, 158 } 159 160 ctx, cancel := testhelper.Context() 161 defer cancel() 162 stream, err := client.FindCommits(ctx, request) 163 require.NoError(t, err) 164 165 resp, err := stream.Recv() 166 require.NoError(t, err) 167 168 require.Equal(t, 1, len(resp.Commits), "expected exactly one commit in the first message") 169 firstCommit := resp.Commits[0] 170 171 testassert.ProtoEqual(t, tc.commit, firstCommit) 172 173 _, err = stream.Recv() 174 require.Equal(t, io.EOF, err, "there should be no further messages in the stream") 175 }) 176 } 177} 178 179func TestSuccessfulFindCommitsRequest(t *testing.T) { 180 t.Parallel() 181 _, repo, _, client := setupCommitServiceWithRepo(t, true) 182 183 testCases := []struct { 184 desc string 185 request *gitalypb.FindCommitsRequest 186 // Use 'ids' if you know the exact commits id's that should be returned 187 ids []string 188 // Use minCommits if you don't know the exact commit id's 189 minCommits int 190 }{ 191 { 192 desc: "commit by author", 193 request: &gitalypb.FindCommitsRequest{ 194 Repository: repo, 195 Revision: []byte("0031876facac3f2b2702a0e53a26e89939a42209"), 196 Author: []byte("Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>"), 197 Limit: 20, 198 }, 199 ids: []string{"1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"}, 200 }, 201 { 202 desc: "only revision, limit commits", 203 request: &gitalypb.FindCommitsRequest{ 204 Repository: repo, 205 Revision: []byte("0031876facac3f2b2702a0e53a26e89939a42209"), 206 Limit: 3, 207 }, 208 ids: []string{ 209 "0031876facac3f2b2702a0e53a26e89939a42209", 210 "bf6e164cac2dc32b1f391ca4290badcbe4ffc5fb", 211 "48ca272b947f49eee601639d743784a176574a09", 212 }, 213 }, 214 { 215 desc: "revision, default commit limit", 216 request: &gitalypb.FindCommitsRequest{ 217 Repository: repo, 218 Revision: []byte("0031876facac3f2b2702a0e53a26e89939a42209"), 219 }, 220 }, 221 { 222 desc: "revision, default commit limit, bypassing rugged walk", 223 request: &gitalypb.FindCommitsRequest{ 224 Repository: repo, 225 Revision: []byte("0031876facac3f2b2702a0e53a26e89939a42209"), 226 DisableWalk: true, 227 }, 228 }, 229 { 230 desc: "revision and paths", 231 request: &gitalypb.FindCommitsRequest{ 232 Repository: repo, 233 Revision: []byte("0031876facac3f2b2702a0e53a26e89939a42209"), 234 Paths: [][]byte{[]byte("LICENSE")}, 235 Limit: 10, 236 }, 237 ids: []string{"1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"}, 238 }, 239 { 240 desc: "revision and wildcard pathspec", 241 request: &gitalypb.FindCommitsRequest{ 242 Repository: repo, 243 Revision: []byte("0031876facac3f2b2702a0e53a26e89939a42209"), 244 Paths: [][]byte{[]byte("LICEN*")}, 245 Limit: 10, 246 }, 247 ids: []string{"1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"}, 248 }, 249 { 250 desc: "revision and non-existent literal pathspec", 251 request: &gitalypb.FindCommitsRequest{ 252 Repository: repo, 253 Revision: []byte("0031876facac3f2b2702a0e53a26e89939a42209"), 254 Paths: [][]byte{[]byte("LICEN*")}, 255 Limit: 10, 256 GlobalOptions: &gitalypb.GlobalOptions{LiteralPathspecs: true}, 257 }, 258 ids: []string{}, 259 }, 260 { 261 desc: "empty revision", 262 request: &gitalypb.FindCommitsRequest{ 263 Repository: repo, 264 Limit: 35, 265 }, 266 minCommits: 35, 267 }, 268 { 269 desc: "before and after", 270 request: &gitalypb.FindCommitsRequest{ 271 Repository: repo, 272 Before: ×tamppb.Timestamp{Seconds: 1483225200}, 273 After: ×tamppb.Timestamp{Seconds: 1472680800}, 274 Limit: 10, 275 }, 276 ids: []string{ 277 "b83d6e391c22777fca1ed3012fce84f633d7fed0", 278 "498214de67004b1da3d820901307bed2a68a8ef6", 279 }, 280 }, 281 { 282 desc: "no merges", 283 request: &gitalypb.FindCommitsRequest{ 284 Repository: repo, 285 Revision: []byte("e63f41fe459e62e1228fcef60d7189127aeba95a"), 286 SkipMerges: true, 287 Limit: 10, 288 }, 289 ids: []string{ 290 "4a24d82dbca5c11c61556f3b35ca472b7463187e", 291 "498214de67004b1da3d820901307bed2a68a8ef6", 292 "38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e", 293 "c347ca2e140aa667b968e51ed0ffe055501fe4f4", 294 "d59c60028b053793cecfb4022de34602e1a9218e", 295 "a5391128b0ef5d21df5dd23d98557f4ef12fae20", 296 "54fcc214b94e78d7a41a9a8fe6d87a5e59500e51", 297 "048721d90c449b244b7b4c53a9186b04330174ec", 298 "5f923865dde3436854e9ceb9cdb7815618d4e849", 299 "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73", 300 }, 301 }, 302 { 303 desc: "following renames", 304 request: &gitalypb.FindCommitsRequest{ 305 Repository: repo, 306 Revision: []byte("94bb47ca1297b7b3731ff2a36923640991e9236f"), 307 Paths: [][]byte{[]byte("CHANGELOG.md")}, 308 Follow: true, 309 Limit: 10, 310 }, 311 ids: []string{ 312 "94bb47ca1297b7b3731ff2a36923640991e9236f", 313 "5f923865dde3436854e9ceb9cdb7815618d4e849", 314 "913c66a37b4a45b9769037c55c2d238bd0942d2e", 315 }, 316 }, 317 { 318 desc: "all refs", 319 request: &gitalypb.FindCommitsRequest{ 320 Repository: repo, 321 All: true, 322 Limit: 90, 323 }, 324 minCommits: 90, 325 }, 326 { 327 desc: "first parents", 328 request: &gitalypb.FindCommitsRequest{ 329 Repository: repo, 330 Revision: []byte("e63f41fe459e62e1228fcef60d7189127aeba95a"), 331 FirstParent: true, 332 Limit: 10, 333 }, 334 ids: []string{ 335 "e63f41fe459e62e1228fcef60d7189127aeba95a", 336 "b83d6e391c22777fca1ed3012fce84f633d7fed0", 337 "1b12f15a11fc6e62177bef08f47bc7b5ce50b141", 338 "6907208d755b60ebeacb2e9dfea74c92c3449a1f", 339 "281d3a76f31c812dbf48abce82ccf6860adedd81", 340 "54fcc214b94e78d7a41a9a8fe6d87a5e59500e51", 341 "be93687618e4b132087f430a4d8fc3a609c9b77c", 342 "5f923865dde3436854e9ceb9cdb7815618d4e849", 343 "d2d430676773caa88cdaf7c55944073b2fd5561a", 344 "59e29889be61e6e0e5e223bfa9ac2721d31605b8", 345 }, 346 }, 347 { 348 // Ordering by none implies that commits appear in 349 // chronological order: 350 // 351 // git log --graph -n 6 --pretty=format:"%h" --date-order 0031876 352 // * 0031876 353 // |\ 354 // * | bf6e164 355 // | * 48ca272 356 // * | 9d526f8 357 // | * 335bc94 358 // |/ 359 // * 1039376 360 desc: "ordered by none", 361 request: &gitalypb.FindCommitsRequest{ 362 Repository: repo, 363 Revision: []byte("0031876"), 364 Order: gitalypb.FindCommitsRequest_NONE, 365 Limit: 6, 366 }, 367 ids: []string{ 368 "0031876facac3f2b2702a0e53a26e89939a42209", 369 "bf6e164cac2dc32b1f391ca4290badcbe4ffc5fb", 370 "48ca272b947f49eee601639d743784a176574a09", 371 "9d526f87b82e2b2fd231ca44c95508e5e85624ca", 372 "335bc94d5b7369b10251e612158da2e4a4aaa2a5", 373 "1039376155a0d507eba0ea95c29f8f5b983ea34b", 374 }, 375 }, 376 { 377 // When ordering by topology, all commit children will 378 // be shown before parents: 379 // 380 // git log --graph -n 6 --pretty=format:"%h" --topo-order 0031876 381 // * 0031876 382 // |\ 383 // | * 48ca272 384 // | * 335bc94 385 // * | bf6e164 386 // * | 9d526f8 387 // |/ 388 // * 1039376 389 desc: "ordered by topo", 390 request: &gitalypb.FindCommitsRequest{ 391 Repository: repo, 392 Revision: []byte("0031876"), 393 Order: gitalypb.FindCommitsRequest_TOPO, 394 Limit: 6, 395 }, 396 ids: []string{ 397 "0031876facac3f2b2702a0e53a26e89939a42209", 398 "48ca272b947f49eee601639d743784a176574a09", 399 "335bc94d5b7369b10251e612158da2e4a4aaa2a5", 400 "bf6e164cac2dc32b1f391ca4290badcbe4ffc5fb", 401 "9d526f87b82e2b2fd231ca44c95508e5e85624ca", 402 "1039376155a0d507eba0ea95c29f8f5b983ea34b", 403 }, 404 }, 405 } 406 407 for _, tc := range testCases { 408 t.Run(tc.desc, func(t *testing.T) { 409 ctx, cancel := testhelper.Context() 410 defer cancel() 411 412 stream, err := client.FindCommits(ctx, tc.request) 413 require.NoError(t, err) 414 415 var ids []string 416 for err == nil { 417 var resp *gitalypb.FindCommitsResponse 418 resp, err = stream.Recv() 419 for _, c := range resp.GetCommits() { 420 ids = append(ids, c.Id) 421 } 422 } 423 require.Equal(t, io.EOF, err) 424 425 if tc.minCommits > 0 { 426 require.True(t, len(ids) >= tc.minCommits, "expected at least %d commits, got %d", tc.minCommits, len(ids)) 427 return 428 } 429 430 require.Equal(t, len(tc.ids), len(ids)) 431 for i, id := range tc.ids { 432 require.Equal(t, id, ids[i]) 433 } 434 }) 435 } 436} 437 438func TestSuccessfulFindCommitsRequestWithAltGitObjectDirs(t *testing.T) { 439 t.Parallel() 440 cfg, repo, repoPath, client := setupCommitServiceWithRepo(t, false) 441 442 committerName := "Scrooge McDuck" 443 committerEmail := "scrooge@mcduck.com" 444 445 cmd := exec.Command(cfg.Git.BinPath, "-C", repoPath, 446 "-c", fmt.Sprintf("user.name=%s", committerName), 447 "-c", fmt.Sprintf("user.email=%s", committerEmail), 448 "commit", "--allow-empty", "-m", "An empty commit") 449 altObjectsDir := "./alt-objects" 450 currentHead := gittest.CreateCommitInAlternateObjectDirectory(t, cfg.Git.BinPath, repoPath, altObjectsDir, cmd) 451 452 testCases := []struct { 453 desc string 454 altDirs []string 455 expectedCount int 456 }{ 457 { 458 desc: "present GIT_ALTERNATE_OBJECT_DIRECTORIES", 459 altDirs: []string{altObjectsDir}, 460 expectedCount: 1, 461 }, 462 { 463 desc: "empty GIT_ALTERNATE_OBJECT_DIRECTORIES", 464 altDirs: []string{}, 465 expectedCount: 0, 466 }, 467 } 468 469 for _, testCase := range testCases { 470 t.Run(testCase.desc, func(t *testing.T) { 471 repo.GitAlternateObjectDirectories = testCase.altDirs 472 request := &gitalypb.FindCommitsRequest{ 473 Repository: repo, 474 Revision: currentHead, 475 Limit: 1, 476 } 477 478 ctx, cancel := testhelper.Context() 479 defer cancel() 480 481 c, err := client.FindCommits(ctx, request) 482 require.NoError(t, err) 483 484 receivedCommits := getAllCommits(t, func() (gitCommitsGetter, error) { return c.Recv() }) 485 486 require.Equal(t, testCase.expectedCount, len(receivedCommits), "number of commits received") 487 }) 488 } 489} 490 491func TestSuccessfulFindCommitsRequestWithAmbiguousRef(t *testing.T) { 492 t.Parallel() 493 cfg, repo, repoPath, client := setupCommitServiceWithRepo(t, false) 494 495 // These are arbitrary SHAs in the repository. The important part is 496 // that we create a branch using one of them with a different SHA so 497 // that Git detects an ambiguous reference. 498 branchName := "1e292f8fedd741b75372e19097c76d327140c312" 499 commitSha := "6907208d755b60ebeacb2e9dfea74c92c3449a1f" 500 501 gittest.Exec(t, cfg, "-C", repoPath, "checkout", "-b", branchName, commitSha) 502 503 request := &gitalypb.FindCommitsRequest{ 504 Repository: repo, 505 Revision: []byte(branchName), 506 Limit: 1, 507 } 508 509 ctx, cancel := testhelper.Context() 510 defer cancel() 511 512 c, err := client.FindCommits(ctx, request) 513 require.NoError(t, err) 514 515 receivedCommits := getAllCommits(t, func() (gitCommitsGetter, error) { return c.Recv() }) 516 517 require.Equal(t, 1, len(receivedCommits), "number of commits received") 518} 519 520func TestFailureFindCommitsRequest(t *testing.T) { 521 t.Parallel() 522 _, repo, _, client := setupCommitServiceWithRepo(t, true) 523 524 testCases := []struct { 525 desc string 526 request *gitalypb.FindCommitsRequest 527 code codes.Code 528 }{ 529 { 530 desc: "empty path string", 531 request: &gitalypb.FindCommitsRequest{ 532 Repository: repo, 533 Paths: [][]byte{[]byte("")}, 534 }, 535 code: codes.InvalidArgument, 536 }, 537 { 538 desc: "invalid revision", 539 request: &gitalypb.FindCommitsRequest{ 540 Repository: repo, 541 Revision: []byte("--output=/meow"), 542 Limit: 1, 543 }, 544 code: codes.InvalidArgument, 545 }, 546 } 547 548 for _, tc := range testCases { 549 t.Run(tc.desc, func(t *testing.T) { 550 ctx, cancel := testhelper.Context() 551 defer cancel() 552 553 stream, err := client.FindCommits(ctx, tc.request) 554 require.NoError(t, err) 555 556 for err == nil { 557 _, err = stream.Recv() 558 } 559 560 testhelper.RequireGrpcError(t, err, tc.code) 561 }) 562 } 563} 564 565func TestFindCommitsRequestWithFollowAndOffset(t *testing.T) { 566 t.Parallel() 567 _, repo, _, client := setupCommitServiceWithRepo(t, true) 568 569 request := &gitalypb.FindCommitsRequest{ 570 Repository: repo, 571 Follow: true, 572 Paths: [][]byte{[]byte("CHANGELOG")}, 573 Limit: 100, 574 } 575 ctx, cancel := testhelper.Context() 576 defer cancel() 577 allCommits := getCommits(ctx, t, request, client) 578 totalCommits := len(allCommits) 579 580 for offset := 0; offset < totalCommits; offset++ { 581 t.Run(fmt.Sprintf("testing with offset %d", offset), func(t *testing.T) { 582 ctx, cancel := testhelper.Context() 583 defer cancel() 584 request.Offset = int32(offset) 585 request.Limit = int32(totalCommits) 586 commits := getCommits(ctx, t, request, client) 587 assert.Len(t, commits, totalCommits-offset) 588 assert.Equal(t, allCommits[offset:], commits) 589 }) 590 } 591} 592 593func TestFindCommitsWithExceedingOffset(t *testing.T) { 594 t.Parallel() 595 _, repo, _, client := setupCommitServiceWithRepo(t, true) 596 597 ctx, cancel := testhelper.Context() 598 defer cancel() 599 600 stream, err := client.FindCommits(ctx, &gitalypb.FindCommitsRequest{ 601 Repository: repo, 602 Follow: true, 603 Paths: [][]byte{[]byte("CHANGELOG")}, 604 Offset: 9000, 605 }) 606 require.NoError(t, err) 607 608 response, err := stream.Recv() 609 require.Nil(t, response) 610 require.EqualError(t, err, "EOF") 611} 612 613func getCommits(ctx context.Context, t *testing.T, request *gitalypb.FindCommitsRequest, client gitalypb.CommitServiceClient) []*gitalypb.GitCommit { 614 t.Helper() 615 616 stream, err := client.FindCommits(ctx, request) 617 require.NoError(t, err) 618 619 var commits []*gitalypb.GitCommit 620 for err == nil { 621 var resp *gitalypb.FindCommitsResponse 622 resp, err = stream.Recv() 623 commits = append(commits, resp.GetCommits()...) 624 } 625 626 require.Equal(t, io.EOF, err) 627 return commits 628} 629