v0.22.0 4c072b1

merge_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package gitcli_test
4
5import (
6	"context"
7	"strings"
8	"testing"
9
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/require"
12
13	"bnema.dev/ermite/internal/domain"
14	"bnema.dev/ermite/internal/readertest"
15)
16
17func oidOf(t *testing.T, s string) domain.ObjectID {
18	t.Helper()
19	id, err := domain.ParseObjectID(s)
20	require.NoError(t, err)
21	return id
22}
23
24// mergeFixture is main (a, b) and feat (a, b, c, d) with a rename on feat.
25type mergeFixture struct {
26	f                *readertest.Fixture
27	a, b, c, d       domain.ObjectID
28	mainTip, featTip domain.ObjectID
29}
30
31func newMergeFixture(t *testing.T, data string) mergeFixture {
32	t.Helper()
33	f := readertest.NewFixture(t, data, "proj")
34	f.Write("a.txt", "one\ntwo\nthree\n")
35	a := f.Commit("add a")
36	f.Write("b.txt", "bee\n")
37	b := f.Commit("add b")
38	f.Git("update-ref", "refs/heads/main", b)
39	f.CheckoutNew("feat")
40	f.Write("a.txt", "one\n2\nthree\n")
41	c := f.Commit("change a")
42	f.Write("c.txt", "see\n")
43	d := f.Commit("add c")
44	f.Git("update-ref", "refs/heads/feat", d)
45	return mergeFixture{
46		f: f, a: oidOf(t, a), b: oidOf(t, b), c: oidOf(t, c), d: oidOf(t, d),
47		mainTip: oidOf(t, b), featTip: oidOf(t, d),
48	}
49}
50
51func TestBranchHeadAndHasCommit(t *testing.T) {
52	g, data := newGit(t)
53	ctx := context.Background()
54	m := newMergeFixture(t, data)
55	t.Cleanup(func() { _ = g.Close(ctx) })
56
57	got, err := g.BranchHead(ctx, m.f.Name, "feat")
58	require.NoError(t, err)
59	assert.Equal(t, m.d, got)
60
61	for _, branch := range []string{"nope", "", "-x", "a..b", "refs/heads/feat", "feat^{tree}"} {
62		_, err = g.BranchHead(ctx, m.f.Name, branch)
63		assert.ErrorIs(t, err, domain.ErrNotFound, "%q", branch)
64	}
65	_, err = g.BranchHead(ctx, name(t, "missing"), "main")
66	assert.ErrorIs(t, err, domain.ErrNotFound)
67
68	ok, err := g.HasCommit(ctx, m.f.Name, m.c)
69	require.NoError(t, err)
70	assert.True(t, ok)
71	ok, err = g.HasCommit(ctx, m.f.Name, oidOf(t, strings.Repeat("1", 40)))
72	require.NoError(t, err)
73	assert.False(t, ok)
74	// A tree is not a commit.
75	tree, err := g.CommitTree(ctx, m.f.Name, m.d)
76	require.NoError(t, err)
77	ok, err = g.HasCommit(ctx, m.f.Name, tree)
78	require.NoError(t, err)
79	assert.False(t, ok)
80}
81
82func TestMergeBaseAndAncestry(t *testing.T) {
83	g, data := newGit(t)
84	ctx := context.Background()
85	m := newMergeFixture(t, data)
86	// A branch that diverged from a.
87	m.f.Git("checkout", "--quiet", "-b", "side", m.a.String())
88	m.f.Write("s.txt", "side\n")
89	side := oidOf(t, m.f.Commit("side"))
90	m.f.Git("update-ref", "refs/heads/side", side.String())
91	// An unrelated root.
92	m.f.Git("checkout", "--quiet", "--orphan", "other")
93	m.f.Git("rm", "-rf", "--quiet", ".")
94	m.f.Write("o.txt", "other\n")
95	other := oidOf(t, m.f.Commit("other root"))
96
97	base, err := g.MergeBase(ctx, m.f.Name, m.mainTip, m.featTip)
98	require.NoError(t, err)
99	assert.Equal(t, m.b, base, "main is an ancestor of feat: it is the base")
100	base, err = g.MergeBase(ctx, m.f.Name, side, m.featTip)
101	require.NoError(t, err)
102	assert.Equal(t, m.a, base)
103	_, err = g.MergeBase(ctx, m.f.Name, other, m.featTip)
104	assert.ErrorIs(t, err, domain.ErrNotFound, "no shared history")
105
106	for _, tc := range []struct {
107		anc, desc domain.ObjectID
108		want      bool
109	}{
110		{m.mainTip, m.featTip, true},
111		{m.featTip, m.mainTip, false},
112		{m.featTip, m.featTip, true},
113		{side, m.featTip, false},
114		{other, m.featTip, false},
115	} {
116		got, err := g.IsAncestor(ctx, m.f.Name, tc.anc, tc.desc)
117		require.NoError(t, err)
118		assert.Equal(t, tc.want, got, "%s -> %s", tc.anc, tc.desc)
119	}
120	_, err = g.IsAncestor(ctx, m.f.Name, oidOf(t, strings.Repeat("1", 40)), m.featTip)
121	assert.Error(t, err, "an unknown commit is an error, not a plain no")
122}
123
124func TestListRange(t *testing.T) {
125	g, data := newGit(t)
126	ctx := context.Background()
127	m := newMergeFixture(t, data)
128
129	commits, more, err := g.ListRange(ctx, m.f.Name, m.mainTip, m.featTip, 10)
130	require.NoError(t, err)
131	assert.False(t, more)
132	require.Len(t, commits, 2)
133	assert.Equal(t, m.d, commits[0].ID, "newest first")
134	assert.Equal(t, m.c, commits[1].ID)
135	assert.Equal(t, "add c", commits[0].Summary)
136
137	commits, more, err = g.ListRange(ctx, m.f.Name, m.mainTip, m.featTip, 1)
138	require.NoError(t, err)
139	assert.True(t, more)
140	require.Len(t, commits, 1)
141
142	commits, more, err = g.ListRange(ctx, m.f.Name, domain.ObjectID{}, m.featTip, 10)
143	require.NoError(t, err)
144	assert.False(t, more)
145	assert.Len(t, commits, 4, "a zero exclude lists the whole history")
146
147	commits, _, err = g.ListRange(ctx, m.f.Name, m.featTip, m.mainTip, 10)
148	require.NoError(t, err)
149	assert.Empty(t, commits)
150
151	_, _, err = g.ListRange(ctx, m.f.Name, m.mainTip, m.featTip, 0)
152	assert.ErrorIs(t, err, domain.ErrInvalidPageSize)
153}
154
155func TestDiffCommits(t *testing.T) {
156	g, data := newGit(t)
157	ctx := context.Background()
158	m := newMergeFixture(t, data)
159
160	stats, diff, err := g.DiffCommits(ctx, m.f.Name, m.mainTip, m.featTip, domain.DefaultDiffLimits())
161	require.NoError(t, err)
162	assert.Equal(t, domain.DiffStats{FilesChanged: 2, Additions: 2, Deletions: 1}, stats)
163	require.Len(t, diff.Files, 2)
164	assert.Equal(t, "a.txt", diff.Files[0].NewPath)
165	assert.Equal(t, "c.txt", diff.Files[1].NewPath)
166	assert.True(t, diff.HasLine("a.txt", domain.SideNew, 2))
167	assert.True(t, diff.HasLine("a.txt", domain.SideOld, 2))
168	assert.True(t, diff.HasLine("c.txt", domain.SideNew, 1))
169	assert.False(t, diff.HasLine("c.txt", domain.SideOld, 1))
170
171	// The diff between two commits of a branch.
172	stats, _, err = g.DiffCommits(ctx, m.f.Name, m.c, m.featTip, domain.DefaultDiffLimits())
173	require.NoError(t, err)
174	assert.Equal(t, domain.DiffStats{FilesChanged: 1, Additions: 1}, stats)
175}
176
177func TestWriteCommitAndUpdateBranch(t *testing.T) {
178	g, data := newGit(t)
179	ctx := context.Background()
180	m := newMergeFixture(t, data)
181
182	tree, err := g.CommitTree(ctx, m.f.Name, m.featTip)
183	require.NoError(t, err)
184	assert.Equal(t, m.f.Git("rev-parse", m.featTip.String()+"^{tree}"), tree.String())
185
186	obj := "tree " + tree.String() + "\nparent " + m.mainTip.String() + "\n" +
187		"author A <a@x.org> 1 +0000\ncommitter C <c@x.org> 2 +0000\n\nsquashed\n"
188	id, err := g.WriteCommit(ctx, m.f.Name, []byte(obj))
189	require.NoError(t, err)
190	assert.Equal(t, "commit", m.f.Git("cat-file", "-t", id.String()))
191	assert.Equal(t, "squashed", m.f.Git("log", "-1", "--format=%s", id.String()))
192
193	_, err = g.WriteCommit(ctx, m.f.Name, []byte("not a commit"))
194	assert.Error(t, err, "git checks the object")
195
196	// The old value is a precondition.
197	err = g.UpdateBranch(ctx, m.f.Name, "main", m.featTip, id)
198	assert.ErrorIs(t, err, domain.ErrConflict, "main is not at feat")
199	assert.Equal(t, m.mainTip.String(), m.f.Git("rev-parse", "refs/heads/main"))
200
201	require.NoError(t, g.UpdateBranch(ctx, m.f.Name, "main", m.mainTip, id))
202	assert.Equal(t, id.String(), m.f.Git("rev-parse", "refs/heads/main"))
203
204	err = g.UpdateBranch(ctx, m.f.Name, "refs/heads/main", id, m.mainTip)
205	assert.Error(t, err, "only short branch names")
206	err = g.UpdateBranch(ctx, name(t, "missing"), "main", id, m.mainTip)
207	assert.ErrorIs(t, err, domain.ErrNotFound)
208}
209
210func TestDeleteBranch(t *testing.T) {
211	g, data := newGit(t)
212	ctx := context.Background()
213	m := newMergeFixture(t, data)
214	m.f.Git("pack-refs", "--all") // the ref is in packed-refs too
215
216	// The old value is a precondition.
217	err := g.DeleteBranch(ctx, m.f.Name, "feat", m.mainTip)
218	assert.ErrorIs(t, err, domain.ErrConflict, "feat is not at main")
219	assert.Equal(t, m.featTip.String(), m.f.Git("rev-parse", "refs/heads/feat"))
220
221	require.NoError(t, g.DeleteBranch(ctx, m.f.Name, "feat", m.featTip))
222	_, err = g.BranchHead(ctx, m.f.Name, "feat")
223	assert.ErrorIs(t, err, domain.ErrNotFound)
224	assert.Empty(t, m.f.Git("for-each-ref", "refs/heads/feat"), "gone from packed-refs too")
225
226	err = g.DeleteBranch(ctx, m.f.Name, "feat", m.featTip)
227	assert.ErrorIs(t, err, domain.ErrConflict, "already gone")
228	err = g.DeleteBranch(ctx, m.f.Name, "refs/heads/main", m.mainTip)
229	assert.Error(t, err, "only short branch names")
230	assert.Equal(t, m.mainTip.String(), m.f.Git("rev-parse", "refs/heads/main"))
231	err = g.DeleteBranch(ctx, name(t, "missing"), "main", m.mainTip)
232	assert.ErrorIs(t, err, domain.ErrNotFound)
233}

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 2

Tags 45

↑↓ moveenter openesc close