v0.35.1 2aca1df

fixtures.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package readertest
4
5import (
6	"fmt"
7	"os"
8	"path/filepath"
9	"strings"
10	"testing"
11	"time"
12
13	"github.com/stretchr/testify/require"
14
15	"bnema.dev/ermite/internal/domain"
16)
17
18// Case is a repository the readers are compared on.
19type Case struct {
20	Name  string
21	Build func(t *testing.T, data string) *Fixture
22}
23
24// Maintain runs what ermite runs after a push: a split commit-graph with
25// changed-path Bloom filters.
26func (f *Fixture) Maintain() {
27	f.T.Helper()
28	f.Git("commit-graph", "write", "--reachable", "--changed-paths", "--split")
29}
30
31// Cases are the fixtures of the comparison: everything the gitcli tests use,
32// plus merges, tags, deep paths, a large blob, an unborn HEAD, repositories
33// with and without commit-graph (Bloom filters v1 and v2), and one whose
34// graph is stale because commits were pushed after the last maintenance.
35func Cases() []Case {
36	return []Case{
37		{"history", func(t *testing.T, data string) *Fixture { return NewHistory(t, data).Fixture }},
38		{"history with commit-graph v2", func(t *testing.T, data string) *Fixture {
39			h := NewHistory(t, data)
40			h.Git("config", "commitGraph.changedPathsVersion", "2")
41			h.Maintain()
42			return h.Fixture
43		}},
44		{"history with commit-graph v1", func(t *testing.T, data string) *Fixture {
45			h := NewHistory(t, data)
46			h.Git("config", "commitGraph.changedPathsVersion", "1")
47			h.Maintain()
48			return h.Fixture
49		}},
50		{"history packed", func(t *testing.T, data string) *Fixture {
51			h := NewHistory(t, data)
52			h.Git("repack", "-adf", "--depth=50", "--window=250")
53			h.Git("pack-refs", "--all")
54			h.Maintain()
55			return h.Fixture
56		}},
57		{"unborn HEAD", func(t *testing.T, data string) *Fixture { return NewFixture(t, data, "empty") }},
58		{"stale commit-graph", buildStale},
59		{"merges", buildMerges},
60		{"deep paths", buildDeep},
61		{"large blobs", buildLarge},
62		{"names and messages", buildNames},
63		{"refs", buildRefs},
64		{"dangling ref", buildDangling},
65		{"submodule", buildSubmodule},
66		{"paths", buildPaths},
67		{"wide directory", buildWide},
68		{"detached HEAD", func(t *testing.T, data string) *Fixture {
69			h := NewHistory(t, data)
70			h.Git("update-ref", "--no-deref", "HEAD", h.C2)
71			return h.Fixture
72		}},
73	}
74}
75
76// buildStale writes the graph, then adds commits and refs that it does not
77// know: a push followed by maintenance that has not run yet.
78func buildStale(t *testing.T, data string) *Fixture {
79	f := NewFixture(t, data, "stale")
80	f.Git("config", "commitGraph.changedPathsVersion", "2")
81	for i := range 12 {
82		f.Write(fmt.Sprintf("dir%d/file.txt", i%3), fmt.Sprintf("v%d\n", i)).Write("root.txt", fmt.Sprintf("r%d\n", i/4))
83		f.Commit(fmt.Sprintf("commit %d", i))
84	}
85	f.Maintain()
86	f.Git("config", "commitGraph.changedPathsVersion", "1")
87	f.CheckoutNew("late")
88	for i := range 4 {
89		f.Write("dir1/file.txt", fmt.Sprintf("late%d\n", i)).Write(fmt.Sprintf("late%d.txt", i), "x\n")
90		f.Commit(fmt.Sprintf("late %d", i))
91	}
92	f.Checkout("main")
93	f.Write("dir0/new.txt", "n\n")
94	f.Commit("after maintenance")
95	f.Merge("late")
96	return f
97}
98
99// buildMerges has criss-cross merges, an octopus merge, a merge that takes
100// one side of a file and one that is TREESAME to a parent, so that the path
101// history simplification matters.
102func buildMerges(t *testing.T, data string) *Fixture {
103	f := NewFixture(t, data, "merges")
104	f.Write("shared.txt", "base\n").Write("a/one.txt", "1\n").Write("b/one.txt", "1\n")
105	f.Commit("base")
106	f.CheckoutNew("left")
107	f.Write("a/one.txt", "left\n")
108	f.Commit("left a")
109	f.Checkout("main")
110	f.CheckoutNew("right")
111	f.Write("b/one.txt", "right\n").Write("shared.txt", "right\n")
112	f.Commit("right b")
113	f.Checkout("main")
114	f.Write("main.txt", "m\n")
115	f.Commit("main")
116	f.Merge("left")
117	f.Merge("right")
118	f.CheckoutNew("x1")
119	f.Write("x.txt", "x1\n")
120	f.Commit("x1")
121	f.Checkout("main")
122	f.CheckoutNew("x2")
123	f.Write("y.txt", "y\n")
124	f.Commit("x2")
125	f.Checkout("main")
126	f.Write("main.txt", "m2\n")
127	f.Commit("main again")
128	f.When = f.When.Add(time.Minute)
129	f.Git("merge", "--quiet", "--no-ff", "-m", "octopus", "x1", "x2")
130	// A merge whose result is the same as its first parent for shared.txt.
131	f.CheckoutNew("same")
132	f.Write("shared.txt", "same\n")
133	f.Commit("same 1")
134	f.Checkout("main")
135	f.Git("merge", "--quiet", "--no-ff", "-s", "ours", "-m", "ours merge", "same")
136	f.Write("tail.txt", "t\n")
137	f.Commit("tail")
138	f.Maintain()
139	return f
140}
141
142func buildDeep(t *testing.T, data string) *Fixture {
143	f := NewFixture(t, data, "deep")
144	deep := strings.Repeat("d/", 30) + "leaf"
145	f.Write(deep+"/file.txt", "deep\n").Write("top.txt", "top\n")
146	f.Commit("deep")
147	f.Write(deep+"/file.txt", "deeper\n")
148	f.Commit("change deep")
149	f.Write("top.txt", "top2\n")
150	f.Commit("change top")
151	f.Write("héllo/ünï.txt", "utf8\n").Write("sp ace/a b.txt", "sp\n")
152	f.Commit("odd names")
153	f.Maintain()
154	return f
155}
156
157// buildLarge has blobs on both sides of the in-memory threshold of gitcli
158// (256 KiB), loose, packed as is and packed as deltas.
159func buildLarge(t *testing.T, data string) *Fixture {
160	f := NewFixture(t, data, "large")
161	big := RandomBytes(12<<20, 9)
162	f.WriteBytes("big.bin", big).WriteBytes("mid.bin", RandomBytes(300<<10, 2)).
163		Write("small.txt", "s\n")
164	var text strings.Builder
165	for i := range 20000 {
166		fmt.Fprintf(&text, "line %d of a text that deltifies well\n", i)
167	}
168	f.Write("text.txt", text.String())
169	f.Commit("big")
170	f.Write("text.txt", text.String()+"one more line\n")
171	f.Commit("big text, second version")
172	f.Git("repack", "-adf", "--depth=50", "--window=250")
173	f.Write("loose.bin", string(RandomBytes(400<<10, 4)))
174	f.Commit("loose")
175	f.Maintain()
176	return f
177}
178
179// buildNames has messages and names git must convert or keep: no message,
180// non-UTF-8, an encoding header, trailing blank lines, long summaries, CRLF.
181func buildNames(t *testing.T, data string) *Fixture {
182	f := NewFixture(t, data, "names")
183	f.Write("a.txt", "a\n")
184	f.When = f.When.Add(time.Minute)
185	f.Git("add", "-A")
186	f.Git("commit", "--quiet", "--allow-empty-message", "-m", "")
187	f.Write("a.txt", "b\n")
188	f.Commit("subject\n\n\n\nbody\n\n\n")
189	f.Write("a.txt", "c\n")
190	f.Commit("caf\xe9 latin-1 message")
191	f.Write("a.txt", "d\n")
192	f.Commit("crlf\r\nsecond\r\n")
193	f.Write("a.txt", "e\n")
194	f.Git("-c", "i18n.commitEncoding=ISO-8859-1", "add", "-A")
195	f.When = f.When.Add(time.Minute)
196	f.Git("-c", "i18n.commitEncoding=ISO-8859-1", "commit", "--quiet", "-m", "enc: caf\xe9")
197	f.Write("a.txt", "f\n")
198	f.When = f.When.Add(time.Minute)
199	f.Git("add", "-A")
200	f.Git("-c", "user.name=Zoë <Ünï>", "commit", "--quiet", "--author=Zoë Ünï <zoe@example.org>", "-m", strings.Repeat("long ", 400))
201	f.Write("héllo wörld.txt", "x\n").Write("tab\tname.txt", "t\n").Write("-dash", "d\n")
202	f.Commit("names")
203	// Git trims the trailing whitespace of a name when it writes a commit but
204	// shows one that has some as it is read: the readers must agree on it.
205	head, tree := f.Git("rev-parse", "HEAD"), f.Git("rev-parse", "HEAD^{tree}")
206	f.Git("update-ref", "refs/heads/main", f.WriteObject("commit", "tree "+tree+"\nparent "+head+"\n"+
207		"author Ada\t <a@b> 1767268860 +0000\ncommitter Grace \t\r <g@b> 1767268860 +0000\n\nraw names\n"))
208	f.Maintain()
209	return f
210}
211
212// buildRefs has a branch and a tag with the same name, a branch named like a
213// full commit id, a tag named like an abbreviation, nested tags, tags of
214// non-commits, packed and loose refs, and refs outside heads and tags.
215func buildRefs(t *testing.T, data string) *Fixture {
216	f := NewFixture(t, data, "refs")
217	f.Write("a.txt", "1\n")
218	c1 := f.Commit("one")
219	f.Write("a.txt", "2\n")
220	c2 := f.Commit("two")
221	f.Write("a.txt", "3\n")
222	c3 := f.Commit("three")
223	f.Git("branch", "same", c1)
224	f.Git("tag", "same", c2)
225	f.Git("tag", "-a", "-m", "annotated", "ann", c2)
226	f.Git("tag", "-a", "-m", "nested", "ann2", "ann")
227	f.Git("tag", "-a", "-m", "nested 3", "ann3", "ann2")
228	f.Git("tag", "-a", "-m", "tree", "tree-tag", c3+"^{tree}")
229	f.Git("tag", "-a", "-m", "blob", "blob-tag", c3+":a.txt")
230	f.Git("-c", "advice.objectNameWarning=false", "branch", c1, c3)
231	f.Git("-c", "advice.objectNameWarning=false", "tag", c2, c3)
232	ghost := strings.Repeat("cd", 20)
233	f.Git("-c", "advice.objectNameWarning=false", "branch", ghost, c3)
234	f.Git("-c", "advice.objectNameWarning=false", "tag", strings.Repeat("ef", 20), c1)
235	f.Git("branch", "dir/sub/branch", c2)
236	f.Git("update-ref", "refs/remotes/origin/main", c1)
237	f.Git("update-ref", "refs/notes/x", c1)
238	f.Git("pack-refs", "--all")
239	f.Git("branch", "loose-after-pack", c3)
240	f.Git("tag", "loose-tag", c1)
241	f.Git("branch", "-f", "same", c3) // loose overrides the packed one
242	return f
243}
244
245// buildDangling has a loose branch and a loose tag whose objects do not exist,
246// next to sound refs. git lists them, and fails when it must follow them.
247func buildDangling(t *testing.T, data string) *Fixture {
248	f := NewFixture(t, data, "dangling")
249	f.Write("a.txt", "1\n")
250	f.Commit("one")
251	ghost := strings.Repeat("0123456789", 4)
252	for _, ref := range []string{"refs/heads/ghost", "refs/tags/ghost"} {
253		require.NoError(t, os.WriteFile(filepath.Join(f.Bare, filepath.FromSlash(ref)), []byte(ghost+"\n"), 0o644))
254	}
255	return f
256}
257
258// buildSubmodule has a gitlink entry, a symlink and an executable.
259func buildSubmodule(t *testing.T, data string) *Fixture {
260	f := NewFixture(t, data, "submod")
261	f.Write("a.txt", "a\n").Write("run.sh", "#!/bin/sh\n").Chmod("run.sh", 0o755).Symlink("a.txt", "link")
262	f.Commit("base")
263	f.Git("update-index", "--add", "--cacheinfo", "160000,"+strings.Repeat("ab", 20)+",vendor/mod")
264	f.Git("commit", "--quiet", "-m", "add submodule")
265	f.Maintain()
266	return f
267}
268
269// buildPaths has what ListPaths must order and skip: nested directories whose
270// names sort around "/" (a/b/c.txt, a/b.txt, a-b.txt), an executable, a
271// symlink, a submodule, and a name with a tab that no address can carry. The
272// tag v1 points at the commit.
273func buildPaths(t *testing.T, data string) *Fixture {
274	f := NewFixture(t, data, "paths")
275	f.Write("a/b/c.txt", "c\n").Write("a/b.txt", "b\n").Write("a-b.txt", "ab\n").Write("z.txt", "z\n")
276	f.Write("run.sh", "#!/bin/sh\n").Chmod("run.sh", 0o755).Symlink("z.txt", "link")
277	f.Write("tab\tname.txt", "t\n").Write("d/e/f/deep.txt", "d\n").Write("héllo wörld.txt", "h\n")
278	f.Commit("base")
279	f.Git("update-index", "--add", "--cacheinfo", "160000,"+strings.Repeat("ab", 20)+",vendor/mod")
280	f.Git("commit", "--quiet", "-m", "add submodule")
281	f.Git("tag", "v1")
282	f.Maintain()
283	return f
284}
285
286// buildWide has a directory over MaxTreeEntries entries and one over
287// MaxLastCommitEntries, with commits that touch some of them.
288func buildWide(t *testing.T, data string) *Fixture {
289	f := NewFixture(t, data, "wide")
290	for i := range domain.MaxTreeEntries + 7 {
291		f.Write(fmt.Sprintf("d/f%05d", i), "x")
292	}
293	for i := range domain.MaxLastCommitEntries + 20 {
294		f.Write(fmt.Sprintf("m/e%03d", i), "x\n")
295		if i%7 == 0 {
296			f.Write(fmt.Sprintf("m/sub%03d/x", i), "x\n")
297		}
298	}
299	f.Commit("wide")
300	f.Write("m/e003", "changed\n").Write("m/e250", "changed\n").Write("d/f00010", "y")
301	f.Commit("touch some")
302	f.Maintain()
303	return f
304}

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