detached 388d3be

mergerequests_files_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase_test
4
5import (
6	"fmt"
7	"os"
8	"path/filepath"
9	"testing"
10
11	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/require"
13
14	"bnema.dev/ermite/internal/domain"
15	"bnema.dev/ermite/internal/ports"
16)
17
18// manyFiles commits n one-line files m/f000... in one commit and
19// returns the commit id.
20func (r *mrRig) manyFiles(n int) domain.ObjectID {
21	r.t.Helper()
22	for i := range n {
23		full := filepath.Join(r.work, "m", fmt.Sprintf("f%03d", i))
24		require.NoError(r.t, os.MkdirAll(filepath.Dir(full), 0o755))
25		require.NoError(r.t, os.WriteFile(full, []byte(fmt.Sprintf("line %d\n", i)), 0o644))
26	}
27	r.sh("add", "m")
28	r.sh("commit", "-q", "-m", "many files")
29	return r.head()
30}
31
32const manyFilesCount = 320
33
34// bigMR opens a request that changes manyFilesCount files.
35func bigMR(t *testing.T) (*mrRig, domain.MergeRequest) {
36	t.Helper()
37	r := newMRRig(t)
38	r.seed()
39	r.sh("checkout", "-q", "-B", "feat", "main")
40	r.manyFiles(manyFilesCount)
41	r.push("feat")
42	return r, r.open("feat")
43}
44
45func TestGetMergeRequestFilesWindow(t *testing.T) {
46	r, mr := bigMR(t)
47	n := mr.Number
48
49	t.Run("the default diff keeps the contract limit", func(t *testing.T) {
50		d, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{})
51		require.NoError(t, err)
52		assert.Len(t, d.Diff.Files, 300)
53		assert.True(t, d.Diff.Truncated)
54		assert.Equal(t, manyFilesCount, d.Stats.FilesChanged)
55	})
56
57	t.Run("every file is listed, the hunks are those of the window", func(t *testing.T) {
58		d, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Files: &ports.FileWindow{From: 305, To: 310}})
59		require.NoError(t, err)
60		require.Len(t, d.Diff.Files, manyFilesCount)
61		assert.Equal(t, manyFilesCount, d.Stats.FilesChanged)
62		assert.False(t, d.Diff.Truncated)
63		for i, f := range d.Diff.Files {
64			assert.Equal(t, fmt.Sprintf("m/f%03d", i), f.NewPath)
65			assert.Equal(t, 1, f.Additions)
66			assert.False(t, f.Truncated)
67			if i >= 305 && i < 310 {
68				require.Len(t, f.Hunks, 1, f.NewPath)
69				assert.Equal(t, fmt.Sprintf("line %d", i), f.Hunks[0].Lines[0].Text)
70			} else {
71				assert.Empty(t, f.Hunks, f.NewPath)
72			}
73		}
74	})
75
76	t.Run("an empty window past the end lists the files only", func(t *testing.T) {
77		d, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Files: &ports.FileWindow{From: 1000}})
78		require.NoError(t, err)
79		require.Len(t, d.Diff.Files, manyFilesCount)
80		for _, f := range d.Diff.Files {
81			assert.Empty(t, f.Hunks)
82		}
83	})
84
85	t.Run("an open window reads through the end", func(t *testing.T) {
86		d, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Files: &ports.FileWindow{From: 315}})
87		require.NoError(t, err)
88		require.Len(t, d.Diff.Files, manyFilesCount)
89		assert.Empty(t, d.Diff.Files[314].Hunks)
90		for _, f := range d.Diff.Files[315:] {
91			assert.Len(t, f.Hunks, 1)
92		}
93	})
94
95	t.Run("an invalid window is refused", func(t *testing.T) {
96		for _, w := range []ports.FileWindow{{From: -1}, {To: -1}, {From: 5, To: 3}} {
97			_, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Files: &w})
98			require.ErrorIs(t, err, domain.ErrInvalid, "%+v", w)
99		}
100	})
101
102	t.Run("a comment on a line of a file past the 300th is accepted", func(t *testing.T) {
103		c, err := r.mrs.AddComment(r.ctx, mrW, r.repo, n, ports.AddCommentParams{Path: "m/f315", Line: 1, Side: domain.SideNew, Text: "late file"})
104		require.NoError(t, err)
105		assert.Equal(t, "m/f315", c.Path)
106		_, err = r.mrs.AddComment(r.ctx, mrW, r.repo, n, ports.AddCommentParams{Path: "m/f319", Line: 1, Side: domain.SideNew, Text: "last file"})
107		require.NoError(t, err)
108
109		_, err = r.mrs.AddComment(r.ctx, mrW, r.repo, n, ports.AddCommentParams{Path: "m/f315", Line: 2, Side: domain.SideNew, Text: "no such line"})
110		require.ErrorIs(t, err, domain.ErrInvalid)
111		_, err = r.mrs.AddComment(r.ctx, mrW, r.repo, n, ports.AddCommentParams{Path: "m/f999", Line: 1, Side: domain.SideNew, Text: "no such file"})
112		require.ErrorIs(t, err, domain.ErrInvalid)
113	})
114}
115
116func TestGetMergeRequestBaseAndHeadChecks(t *testing.T) {
117	r := newMRRig(t)
118	base := r.seed()
119	head1 := r.feature("feat")
120	mr := r.open("feat")
121	n := mr.Number
122	other := oidFrom(t, "1111111111111111111111111111111111111111")
123
124	t.Run("matching or unset base and head", func(t *testing.T) {
125		for _, o := range []ports.GetMergeRequestOptions{
126			{}, {Base: base}, {Head: head1}, {Base: base, Head: head1}, {Base: base, Head: head1, Files: &ports.FileWindow{To: 1}},
127		} {
128			d, err := r.mrs.Get(r.ctx, reader, r.repo, n, o)
129			require.NoError(t, err, "%+v", o)
130			assert.Equal(t, base, d.DiffBaseID)
131		}
132	})
133
134	t.Run("another base or head is a conflict", func(t *testing.T) {
135		for _, o := range []ports.GetMergeRequestOptions{
136			{Base: other}, {Head: other}, {Base: base, Head: other}, {Base: other, Head: head1},
137			{Base: head1}, // the head is not the base
138			{Base: other, Files: &ports.FileWindow{From: 1, To: 2}},
139		} {
140			_, err := r.mrs.Get(r.ctx, reader, r.repo, n, o)
141			require.ErrorIs(t, err, domain.ErrConflict, "%+v", o)
142		}
143	})
144
145	t.Run("the head moved", func(t *testing.T) {
146		r.commit(author, "d.txt", "dee\n", "add d")
147		head2 := r.head()
148		r.push("feat")
149		_, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Base: base, Head: head1})
150		require.ErrorIs(t, err, domain.ErrConflict)
151		d, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Base: base, Head: head2})
152		require.NoError(t, err)
153		assert.Equal(t, head2, d.MergeRequest.HeadID)
154	})
155
156	t.Run("the base is the one after since-last-read", func(t *testing.T) {
157		head2 := r.branchHead("feat")
158		_, err := r.mrs.MarkRead(r.ctx, mrW, r.repo, n, head1)
159		require.NoError(t, err)
160		// The whole diff (base) is no longer what since-last-read shows (head1).
161		_, err = r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{SinceLastRead: true, Base: base, Head: head2})
162		require.ErrorIs(t, err, domain.ErrConflict)
163		d, err := r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{SinceLastRead: true, Base: head1, Head: head2})
164		require.NoError(t, err)
165		assert.Equal(t, head1, d.DiffBaseID)
166		require.Len(t, d.Diff.Files, 1)
167		assert.Equal(t, "d.txt", d.Diff.Files[0].NewPath)
168		// Without since-last-read the base is still the merge base.
169		_, err = r.mrs.Get(r.ctx, reader, r.repo, n, ports.GetMergeRequestOptions{Base: head1, Head: head2})
170		require.ErrorIs(t, err, domain.ErrConflict)
171	})
172}
173
174func TestGetMergeRequestDiffOnly(t *testing.T) {
175	r := newMRRig(t)
176	base := r.seed()
177	r.feature("feat")
178	mr := r.open("feat")
179
180	full, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{})
181	require.NoError(t, err)
182	require.Len(t, full.Commits, 2)
183
184	d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, ports.GetMergeRequestOptions{DiffOnly: true})
185	require.NoError(t, err)
186	assert.Empty(t, d.Commits)
187	assert.False(t, d.CommitsTruncated)
188	assert.Equal(t, base, d.DiffBaseID)
189	assert.Equal(t, full.Diff, d.Diff)
190	assert.Equal(t, full.Stats, d.Stats)
191	assert.Equal(t, full.Comments, d.Comments)
192}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 45

↑↓ moveenter openesc close