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:     &timestamppb.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:     &timestamppb.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:     &timestamppb.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:     &timestamppb.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:     &timestamppb.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:     &timestamppb.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:     &timestamppb.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:     &timestamppb.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:     &timestamppb.Timestamp{Seconds: 1483225200},
273				After:      &timestamppb.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