detached 0e6522c

fixture.go RawHistoryFolder

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

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