v0.5.0 bfab294

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

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 3

Tags 43

↑↓ moveenter openesc close