main de06cbd

helpers_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6	"bytes"
7	"crypto/sha1"
8	"fmt"
9	"os"
10	"os/exec"
11	"path/filepath"
12	"strconv"
13	"strings"
14	"testing"
15)
16
17// fx builds a fixture repository with the git binary, which is also the
18// oracle of the tests.
19type fx struct {
20	t    *testing.T
21	dir  string // the git directory (bare) or work/.git
22	work string // work tree, "" for a bare repository
23	n    int    // commit counter, drives the dates
24}
25
26func gitEnv() []string {
27	return append(os.Environ(),
28		"GIT_CONFIG_GLOBAL=/dev/null",
29		"GIT_CONFIG_SYSTEM=/dev/null",
30		"GIT_CONFIG_NOSYSTEM=1",
31		"GIT_TERMINAL_PROMPT=0",
32		"GIT_AUTHOR_NAME=Ada Lovelace",
33		"GIT_AUTHOR_EMAIL=ada@example.com",
34		"GIT_COMMITTER_NAME=Grace Hopper",
35		"GIT_COMMITTER_EMAIL=grace@example.com",
36		"LC_ALL=C",
37	)
38}
39
40func requireGit(t testing.TB) {
41	t.Helper()
42	if _, err := exec.LookPath("git"); err != nil {
43		t.Skip("git binary not available")
44	}
45}
46
47// runGit runs git in dir and returns its stdout.
48func runGit(t testing.TB, dir string, stdin []byte, extraEnv []string, args ...string) []byte {
49	t.Helper()
50	cmd := exec.Command("git", append([]string{"-c", "commit.gpgsign=false", "-c", "tag.gpgsign=false", "-c", "gc.auto=0", "-c", "init.defaultBranch=main", "-c", "protocol.file.allow=always"}, args...)...)
51	cmd.Dir = dir
52	cmd.Env = append(gitEnv(), extraEnv...)
53	if stdin != nil {
54		cmd.Stdin = bytes.NewReader(stdin)
55	}
56	var out, errb bytes.Buffer
57	cmd.Stdout, cmd.Stderr = &out, &errb
58	if err := cmd.Run(); err != nil {
59		t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, errb.String())
60	}
61	return out.Bytes()
62}
63
64// newFx creates an empty non-bare repository; Open is given work, so the
65// ".git" directory handling is covered.
66func newFx(t *testing.T) *fx {
67	requireGit(t)
68	work := filepath.Join(t.TempDir(), "work")
69	if err := os.MkdirAll(work, 0o755); err != nil {
70		t.Fatal(err)
71	}
72	f := &fx{t: t, work: work, dir: filepath.Join(work, ".git")}
73	f.git("init", "-q")
74	return f
75}
76
77// newBareFx creates an empty bare repository.
78func newBareFx(t *testing.T) *fx {
79	requireGit(t)
80	dir := filepath.Join(t.TempDir(), "bare.git")
81	f := &fx{t: t, dir: dir}
82	runGit(t, t.TempDir(), nil, nil, "init", "-q", "--bare", dir)
83	return f
84}
85
86func (f *fx) root() string {
87	if f.work != "" {
88		return f.work
89	}
90	return f.dir
91}
92
93func (f *fx) git(args ...string) string {
94	f.t.Helper()
95	return strings.TrimRight(string(runGit(f.t, f.root(), nil, f.env(), args...)), "\n")
96}
97
98func (f *fx) gitIn(stdin []byte, args ...string) string {
99	f.t.Helper()
100	return strings.TrimRight(string(runGit(f.t, f.root(), stdin, f.env(), args...)), "\n")
101}
102
103func (f *fx) env() []string {
104	if f.work != "" {
105		return nil
106	}
107	return []string{"GIT_DIR=" + f.dir}
108}
109
110func (f *fx) tick() []string {
111	f.n++
112	d := fmt.Sprintf("%d +0%d00", 1700000000+f.n*3600, f.n%10)
113	if f.n%3 == 0 {
114		d = fmt.Sprintf("%d -0%d30", 1700000000+f.n*3600, f.n%9)
115	}
116	return []string{"GIT_AUTHOR_DATE=" + d, "GIT_COMMITTER_DATE=" + d}
117}
118
119func (f *fx) write(path string, data []byte, mode os.FileMode) {
120	f.t.Helper()
121	full := filepath.Join(f.work, path)
122	if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
123		f.t.Fatal(err)
124	}
125	if err := os.WriteFile(full, data, mode); err != nil {
126		f.t.Fatal(err)
127	}
128	if err := os.Chmod(full, mode); err != nil {
129		f.t.Fatal(err)
130	}
131}
132
133// commit stages everything and commits with the next date.
134func (f *fx) commit(msg string) string {
135	f.t.Helper()
136	f.git("add", "-A")
137	runGit(f.t, f.work, nil, f.tick(), "commit", "-q", "--allow-empty", "--allow-empty-message", "-m", msg)
138	return f.git("rev-parse", "HEAD")
139}
140
141func (f *fx) open(opts ...Option) *Repo {
142	f.t.Helper()
143	path := f.dir
144	if f.work != "" {
145		path = f.work
146	}
147	r, err := Open(path, opts...)
148	if err != nil {
149		f.t.Fatalf("Open: %v", err)
150	}
151	f.t.Cleanup(func() { _ = r.Close() })
152	return r
153}
154
155// lines returns the non-empty lines of s.
156func lines(s string) []string {
157	var out []string
158	for _, l := range strings.Split(s, "\n") {
159		if l != "" {
160			out = append(out, l)
161		}
162	}
163	return out
164}
165
166func mustID(t testing.TB, s string) ObjectID {
167	t.Helper()
168	id, err := ParseID(strings.TrimSpace(s))
169	if err != nil {
170		t.Fatal(err)
171	}
172	return id
173}
174
175// textLines builds a compressible, delta-friendly text of n lines.
176func textLines(n, seed int) []byte {
177	var b bytes.Buffer
178	for i := 0; i < n; i++ {
179		fmt.Fprintf(&b, "line %d of the file, seed %d, some filler text to make it compress\n", i, (i*7+seed)%97)
180	}
181	return b.Bytes()
182}
183
184// ambiguousPair finds the contents of two blobs whose ids share their first
185// 4 hex digits.
186func ambiguousPair(t *testing.T) (a, b []byte) {
187	t.Helper()
188	seen := map[string]int{}
189	for i := 0; i < 200000; i++ {
190		data := "ambiguous " + strconv.Itoa(i) + "\n"
191		h := sha1.New()
192		_, _ = fmt.Fprintf(h, "blob %d\x00%s", len(data), data)
193		p := fmt.Sprintf("%x", h.Sum(nil)[:2])
194		if j, ok := seen[p]; ok {
195			return []byte("ambiguous " + strconv.Itoa(j) + "\n"), []byte(data)
196		}
197		seen[p] = i
198	}
199	t.Fatal("no ambiguous pair found")
200	return nil, nil
201}
202
203// writeBlob stores data as a loose blob and returns its id.
204func (f *fx) writeBlob(data []byte) ObjectID {
205	f.t.Helper()
206	return mustID(f.t, f.gitIn(data, "hash-object", "-w", "--stdin"))
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 1

Tags 2

↑↓ moveenter openesc close