v0.2.0 41cf2ec

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

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