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