v0.1.1 de06cbd

refresh_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6	"errors"
7	"os"
8	"testing"
9	"time"
10)
11
12// age sets the modification time of the given files of the Git directory to
13// an hour ago (the missing ones are skipped).
14func ageFiles(t *testing.T, dir string, names ...string) {
15	t.Helper()
16	old := time.Now().Add(-time.Hour)
17	for _, n := range names {
18		if err := os.Chtimes(dir+"/"+n, old, old); err != nil && !os.IsNotExist(err) {
19			t.Fatal(err)
20		}
21	}
22}
23
24// An unchanged repository costs Refresh no allocation once the stat data of
25// what it looks at is older than the racy window.
26func TestRefreshUnchangedAllocatesNothing(t *testing.T) {
27	if raceEnabled {
28		t.Skip("allocation counts are not stable under the race detector")
29	}
30	f := newFx(t)
31	f.write("a", []byte("a"), 0o644)
32	f.commit("one")
33	f.git("repack", "-adq")
34	f.git("pack-refs", "--all")
35	f.git("commit-graph", "write", "--reachable", "--split")
36	if err := os.MkdirAll(f.dir+"/refs/replace", 0o755); err != nil {
37		t.Fatal(err)
38	}
39	ageFiles(t, f.dir, "packed-refs", "objects/pack", "objects/info/commit-graph", "objects/info/commit-graphs/commit-graph-chain", "refs/replace")
40	r := f.open()
41	for range 3 { // records the old stat data
42		if err := r.Refresh(); err != nil {
43			t.Fatal(err)
44		}
45	}
46	snap, graph := r.snap.Load(), r.graph.Load()
47	got := testing.AllocsPerRun(100, func() {
48		if err := r.Refresh(); err != nil {
49			t.Fatal(err)
50		}
51	})
52	if got != 0 {
53		t.Errorf("an unchanged Refresh allocates %v, want 0", got)
54	}
55	if r.snap.Load() != snap || r.graph.Load() != graph {
56		t.Error("an unchanged Refresh replaced the pack list or the graph")
57	}
58}
59
60// refs/replace is only listed again when its stat data moved, and a replace
61// ref added later is still refused, also right after the directory was
62// listed (inside the racy window).
63func TestRefreshSeesReplaceRefs(t *testing.T) {
64	f := newFx(t)
65	f.write("a", []byte("a"), 0o644)
66	c := f.commit("one")
67	if err := os.MkdirAll(f.dir+"/refs/replace", 0o755); err != nil {
68		t.Fatal(err)
69	}
70	r := f.open() // an empty refs/replace is fine
71	for range 3 {
72		if err := r.Refresh(); err != nil {
73			t.Fatal(err)
74		}
75	}
76	writeFile(t, f.dir+"/refs/replace/"+c, c+"\n")
77	if err := r.Refresh(); !errors.Is(err, ErrUnsupported) {
78		t.Fatalf("Refresh after a replace ref = %v, want ErrUnsupported", err)
79	}
80	// And the same once the empty directory is old enough to be trusted.
81	_ = os.Remove(f.dir + "/refs/replace/" + c)
82	ageFiles(t, f.dir, "refs/replace")
83	for range 3 {
84		if err := r.Refresh(); err != nil {
85			t.Fatal(err)
86		}
87	}
88	if r.paths.replaceSig.read-r.paths.replaceSig.mtime <= int64(racyWindow) {
89		t.Fatal("the empty directory should have been recorded as trusted")
90	}
91	writeFile(t, f.dir+"/refs/replace/"+c, c+"\n")
92	if err := r.Refresh(); !errors.Is(err, ErrUnsupported) {
93		t.Fatalf("Refresh after a replace ref in a trusted directory = %v, want ErrUnsupported", err)
94	}
95	// Removing the directory is fine again.
96	_ = os.Remove(f.dir + "/refs/replace/" + c)
97	_ = os.Remove(f.dir + "/refs/replace")
98	if err := r.Refresh(); err != nil {
99		t.Fatalf("Refresh with no refs/replace: %v", err)
100	}
101}
102
103// The other unsupported layouts are noticed at each Refresh.
104func TestRefreshSeesUnsupportedLayouts(t *testing.T) {
105	for _, name := range []string{"shallow", "info/grafts", "commondir", "objects/info/alternates"} {
106		f := newFx(t)
107		f.write("a", []byte("a"), 0o644)
108		f.commit("one")
109		r := f.open()
110		if err := r.Refresh(); err != nil {
111			t.Fatal(err)
112		}
113		_ = os.MkdirAll(f.dir+"/"+name[:max(0, len(name)-len(baseName(name)))], 0o755)
114		writeFile(t, f.dir+"/"+name, "x\n")
115		if err := r.Refresh(); !errors.Is(err, ErrUnsupported) {
116			t.Errorf("%s: Refresh = %v, want ErrUnsupported", name, err)
117		}
118	}
119}
120
121func baseName(p string) string {
122	for i := len(p) - 1; i >= 0; i-- {
123		if p[i] == '/' {
124			return p[i+1:]
125		}
126	}
127	return p
128}
129
130func TestStatPath(t *testing.T) {
131	dir := t.TempDir()
132	writeFile(t, dir+"/f", "hello")
133	f := newStatPath(dir + "/f")
134	st, err := f.stat(false)
135	if err != nil || !st.exists || st.isDir || st.size != 5 || st.ino == 0 {
136		t.Fatalf("stat file = %+v, %v", st, err)
137	}
138	fi, _ := os.Stat(dir + "/f")
139	if st.mtime != fi.ModTime().UnixNano() || st.ino != fileIno(fi) {
140		t.Errorf("mtime %d ino %d, os.Stat says %d %d", st.mtime, st.ino, fi.ModTime().UnixNano(), fileIno(fi))
141	}
142	d := newStatPath(dir)
143	if st, err := d.stat(false); err != nil || !st.exists || !st.isDir {
144		t.Fatalf("stat dir = %+v, %v", st, err)
145	}
146	_ = os.Symlink(dir+"/f", dir+"/link")
147	l := newStatPath(dir + "/link")
148	if st, _ := l.stat(false); !st.exists || st.isDir || st.size != 5 {
149		t.Errorf("stat through a link = %+v", st)
150	}
151	if st, _ := l.stat(true); !st.exists || st.size == 5 {
152		t.Errorf("lstat of a link = %+v, want the link itself", st)
153	}
154	_ = os.Symlink(dir+"/gone", dir+"/dangling")
155	dl := newStatPath(dir + "/dangling")
156	if st, err := dl.stat(false); err != nil || st.exists {
157		t.Errorf("stat of a dangling link = %+v, %v", st, err)
158	}
159	if st, err := dl.stat(true); err != nil || !st.exists {
160		t.Errorf("lstat of a dangling link = %+v, %v", st, err)
161	}
162	// Absent, and a file where a directory is expected: not errors.
163	for _, p := range []string{dir + "/missing", dir + "/f/below", dir + "/missing/below"} {
164		sp := newStatPath(p)
165		if st, err := sp.stat(false); err != nil || st.exists {
166			t.Errorf("stat(%s) = %+v, %v, want a quiet absence", p, st, err)
167		}
168	}
169	// A real failure is reported with the path.
170	long := make([]byte, 5000)
171	for i := range long {
172		long[i] = 'x'
173	}
174	sp := newStatPath(dir + "/" + string(long))
175	if _, err := sp.stat(false); err == nil {
176		t.Error("a name that is too long should be an error")
177	} else {
178		var pe *os.PathError
179		if !errors.As(err, &pe) {
180			t.Errorf("error %v is not a *os.PathError", err)
181		}
182	}
183	// sig matches statSig.
184	want, _ := statSig(dir + "/f")
185	got, _ := f.sig()
186	if !got.sameStat(want) {
187		t.Errorf("sig = %+v, statSig = %+v", got, want)
188	}
189	if gone := newStatPath(dir + "/missing"); true {
190		if s, err := gone.sig(); err != nil || s.exists || s.read == 0 {
191			t.Errorf("sig of a missing file = %+v, %v", s, err)
192		}
193	}
194}

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