v0.1.0 3f267b5

fixture_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package gitcli_test
4
5import (
6	"fmt"
7	"math/rand/v2"
8	"os"
9	"os/exec"
10	"path/filepath"
11	"strings"
12	"testing"
13	"time"
14
15	"github.com/stretchr/testify/require"
16
17	"bnema.dev/ermite/internal/domain"
18)
19
20// fixtureRepo builds a repository with real git: a bare repository in the
21// data directory, and a work tree beside it that the helpers edit and commit.
22type fixtureRepo struct {
23	t    *testing.T
24	name domain.RepoName
25	bare string
26	work string
27	when time.Time
28}
29
30// newFixtureRepo creates an empty bare repository repos/<name>.git whose HEAD
31// is refs/heads/main.
32func newFixtureRepo(t *testing.T, data, repo string) *fixtureRepo {
33	t.Helper()
34	f := &fixtureRepo{
35		t:    t,
36		name: name(t, repo),
37		bare: filepath.Join(data, "repos", repo+".git"),
38		work: t.TempDir(),
39		when: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC),
40	}
41	out, err := exec.Command("git", "init", "--quiet", "--bare", "--initial-branch=main", f.bare).CombinedOutput()
42	require.NoError(t, err, string(out))
43	f.git("config", "core.compression", "0")
44	f.git("config", "core.autocrlf", "false")
45	return f
46}
47
48// git runs git against the bare repository and the work tree.
49func (f *fixtureRepo) git(args ...string) string {
50	f.t.Helper()
51	cmd := exec.Command("git", args...)
52	cmd.Dir = f.work
53	cmd.Env = []string{
54		"PATH=" + os.Getenv("PATH"),
55		"HOME=" + f.work,
56		"GIT_CONFIG_NOSYSTEM=1",
57		"GIT_CONFIG_GLOBAL=/dev/null",
58		"GIT_DIR=" + f.bare,
59		"GIT_WORK_TREE=" + f.work,
60		"GIT_AUTHOR_NAME=Ada Lovelace", "GIT_AUTHOR_EMAIL=ada@example.org",
61		"GIT_COMMITTER_NAME=Grace Hopper", "GIT_COMMITTER_EMAIL=grace@example.org",
62		"GIT_AUTHOR_DATE=" + f.when.Format(time.RFC3339),
63		"GIT_COMMITTER_DATE=" + f.when.Format(time.RFC3339),
64	}
65	out, err := cmd.CombinedOutput()
66	require.NoError(f.t, err, "git %s: %s", strings.Join(args, " "), out)
67	return strings.TrimSpace(string(out))
68}
69
70func (f *fixtureRepo) path(p string) string { return filepath.Join(f.work, filepath.FromSlash(p)) }
71
72// write creates or replaces a file with the content.
73func (f *fixtureRepo) write(p, content string) *fixtureRepo {
74	f.t.Helper()
75	require.NoError(f.t, os.MkdirAll(filepath.Dir(f.path(p)), 0o755))
76	require.NoError(f.t, os.WriteFile(f.path(p), []byte(content), 0o644))
77	return f
78}
79
80// writeBytes is write for binary content.
81func (f *fixtureRepo) writeBytes(p string, content []byte) *fixtureRepo {
82	f.t.Helper()
83	require.NoError(f.t, os.MkdirAll(filepath.Dir(f.path(p)), 0o755))
84	require.NoError(f.t, os.WriteFile(f.path(p), content, 0o644))
85	return f
86}
87
88// lines writes n lines "<prefix> <i>" to the file.
89func (f *fixtureRepo) lines(p, prefix string, n int) *fixtureRepo {
90	var b strings.Builder
91	for i := range n {
92		fmt.Fprintf(&b, "%s %d\n", prefix, i)
93	}
94	return f.write(p, b.String())
95}
96
97func (f *fixtureRepo) remove(p string) *fixtureRepo {
98	f.t.Helper()
99	require.NoError(f.t, os.Remove(f.path(p)))
100	return f
101}
102
103func (f *fixtureRepo) chmod(p string, mode os.FileMode) *fixtureRepo {
104	f.t.Helper()
105	require.NoError(f.t, os.Chmod(f.path(p), mode))
106	return f
107}
108
109func (f *fixtureRepo) symlink(target, p string) *fixtureRepo {
110	f.t.Helper()
111	require.NoError(f.t, os.Symlink(target, f.path(p)))
112	return f
113}
114
115func (f *fixtureRepo) mv(from, to string) *fixtureRepo {
116	f.t.Helper()
117	require.NoError(f.t, os.MkdirAll(filepath.Dir(f.path(to)), 0o755))
118	require.NoError(f.t, os.Rename(f.path(from), f.path(to)))
119	return f
120}
121
122// commit stages everything and commits with the next timestamp; it returns
123// the commit id.
124func (f *fixtureRepo) commit(msg string) string {
125	f.t.Helper()
126	f.when = f.when.Add(time.Minute)
127	f.git("add", "-A")
128	f.git("commit", "--quiet", "--allow-empty", "-m", msg)
129	return f.git("rev-parse", "HEAD")
130}
131
132// checkoutNew creates a branch from the current HEAD and switches to it.
133func (f *fixtureRepo) checkoutNew(branch string) {
134	f.t.Helper()
135	f.git("checkout", "--quiet", "-b", branch)
136}
137
138func (f *fixtureRepo) checkout(branch string) { f.t.Helper(); f.git("checkout", "--quiet", branch) }
139
140// merge merges the branch with a merge commit.
141func (f *fixtureRepo) merge(branch string) string {
142	f.t.Helper()
143	f.when = f.when.Add(time.Minute)
144	f.git("merge", "--quiet", "--no-ff", "-m", "merge "+branch, branch)
145	return f.git("rev-parse", "HEAD")
146}
147
148// randomBytes returns n deterministic incompressible bytes.
149func randomBytes(n int, seed uint64) []byte {
150	var s [32]byte
151	for i := range 8 {
152		s[i] = byte(seed >> (8 * i))
153	}
154	r := rand.NewChaCha8(s)
155	b := make([]byte, n)
156	_, _ = r.Read(b)
157	return b
158}

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