v0.1.0 5228465

refs_cache_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6	"fmt"
7	"os"
8	"strings"
9	"testing"
10	"time"
11)
12
13// writePackedRefs writes n branches, all on id, to packed-refs, in the format
14// of "git pack-refs --all".
15func writePackedRefs(t testing.TB, dir string, id ObjectID, n int) {
16	t.Helper()
17	var sb strings.Builder
18	sb.WriteString("# pack-refs with: peeled fully-peeled sorted \n")
19	for i := 0; i < n; i++ {
20		fmt.Fprintf(&sb, "%s refs/heads/b%05d\n", id, i)
21	}
22	if err := os.WriteFile(dir+"/packed-refs", []byte(sb.String()), 0o644); err != nil {
23		t.Fatal(err)
24	}
25}
26
27// Right after packed-refs was rewritten its stat data cannot be trusted, but
28// listing the refs must still parse it once, not once per ref.
29func TestRefsRacyPackedRefsParsedOnce(t *testing.T) {
30	f := newFx(t)
31	f.write("a", []byte("a"), 0o644)
32	id := mustID(t, f.commit("one"))
33	const n = 5000
34	writePackedRefs(t, f.dir, id, n)
35	r := f.open()
36	first, err := r.loadPacked(false)
37	if err != nil {
38		t.Fatal(err)
39	}
40	if time.Now().UnixNano()-first.sig.mtime > int64(racyWindow) {
41		t.Skip("slow machine: the file is no longer racy")
42	}
43	start := time.Now()
44	count := 0
45	for ref, err := range r.Refs("refs/heads/") {
46		if err != nil {
47			t.Fatal(err)
48		}
49		if ref.ID != id {
50			t.Fatalf("%s = %s", ref.Name, ref.ID)
51		}
52		count++
53	}
54	took := time.Since(start)
55	if count != n+1 { // plus the branch of the fixture
56		t.Fatalf("%d refs, want %d", count, n+1)
57	}
58	again, err := r.loadPacked(false)
59	if err != nil {
60		t.Fatal(err)
61	}
62	if &again.refs[0] != &first.refs[0] {
63		t.Error("packed-refs was parsed again although its content did not change")
64	}
65	// Before the fix every ref re-read and re-parsed the file: 0.7 s and more.
66	if !raceEnabled && took > 250*time.Millisecond {
67		t.Errorf("Refs over %d packed refs took %v right after a rewrite", n, took)
68	}
69	// A real change inside the racy window is seen.
70	writePackedRefs(t, f.dir, id, n-1)
71	changed, err := r.loadPacked(false)
72	if err != nil {
73		t.Fatal(err)
74	}
75	if len(changed.refs) != n-1 {
76		t.Errorf("rewritten packed-refs has %d refs, want %d", len(changed.refs), n-1)
77	}
78}
79
80func TestLoadPackedConcurrentShareOneParse(t *testing.T) {
81	f := newFx(t)
82	f.write("a", []byte("a"), 0o644)
83	id := mustID(t, f.commit("one"))
84	writePackedRefs(t, f.dir, id, 2000)
85	r := f.open()
86	base, err := r.loadPacked(false)
87	if err != nil {
88		t.Fatal(err)
89	}
90	writePackedRefs(t, f.dir, id, 1999) // a different size
91	res := make(chan *packedRefs, 16)
92	for range 16 {
93		go func() {
94			p, err := r.loadPacked(false)
95			if err != nil {
96				t.Error(err)
97			}
98			res <- p
99		}()
100	}
101	var got *packedRefs
102	for range 16 {
103		p := <-res
104		if p == nil {
105			continue
106		}
107		if len(p.refs) != 1999 || p == base {
108			t.Fatalf("stale packed-refs: %d refs", len(p.refs))
109		}
110		if got != nil && &p.refs[0] != &got.refs[0] {
111			t.Fatal("concurrent callers parsed the file more than once")
112		}
113		got = p
114	}
115}
116
117// The racy rule runs from the time the file was read, not from now.
118func TestFileSigRacy(t *testing.T) {
119	mtime := time.Now().Add(-time.Hour).UnixNano()
120	w := int64(racyWindow)
121	cases := []struct {
122		name string
123		read int64 // relative to mtime
124		same bool
125	}{
126		{"read just after the write", 1, false},
127		{"read inside the window", w / 2, false},
128		{"read at the end of the window", w, false},
129		{"read after the window", w + 1, true},
130	}
131	for _, c := range cases {
132		a := fileSig{exists: true, size: 10, mtime: mtime, ino: 7, read: mtime + c.read}
133		b := a
134		b.read = time.Now().UnixNano() // when the new stat was taken: irrelevant
135		// The check happens an hour after the read: the old code, which
136		// measured from "now", trusted every one of these.
137		if got := a.same(b); got != c.same {
138			t.Errorf("%s: same = %v, want %v", c.name, got, c.same)
139		}
140	}
141	a := fileSig{exists: true, size: 10, mtime: mtime, read: mtime + 10*w}
142	for _, b := range []fileSig{
143		{exists: true, size: 11, mtime: mtime},
144		{exists: true, size: 10, mtime: mtime + 1},
145		{exists: true, size: 10, mtime: mtime, ino: 1},
146		{},
147	} {
148		if a.same(b) {
149			t.Errorf("%+v taken for %+v", b, a)
150		}
151	}
152	if gone := (fileSig{read: 1}); !gone.same(fileSig{read: 99}) {
153		t.Error("a missing file is not the same as a missing file")
154	}
155	// A recorded signature carries the time of the read.
156	f := newFx(t)
157	writeFile(t, f.dir+"/probe", "x")
158	before := time.Now().UnixNano()
159	sig, err := statSig(f.dir + "/probe")
160	if err != nil {
161		t.Fatal(err)
162	}
163	if sig.read < before || sig.read > time.Now().UnixNano() {
164		t.Errorf("read = %d, want a time between %d and now", sig.read, before)
165	}
166}
167
168// A pack directory that was racy when the list was built is trusted again
169// once a reload has seen it unchanged after the window.
170func TestPackDirRacyUntilReread(t *testing.T) {
171	f := newFx(t)
172	f.write("a", []byte("a"), 0o644)
173	f.commit("one")
174	f.git("repack", "-adq")
175	r := f.open()
176	s := r.snap.Load()
177	// Pretend the directory was modified right before the list was read.
178	r.mu.Lock()
179	s.dirSig.mtime = s.dirSig.read - int64(time.Millisecond)
180	r.mu.Unlock()
181	if s.dirSig.same(s.dirSig) {
182		t.Fatal("a signature read 1ms after the change must not be trusted")
183	}
184	before := s.dirSig.read
185	// The stat data is the real one, but the stored mtime was faked: the
186	// reload sees a different directory and installs a new snapshot, which
187	// carries the time of its own read.
188	if _, err := r.reloadPacks(false); err != nil {
189		t.Fatal(err)
190	}
191	if got := r.snap.Load().dirSig.read; got <= before {
192		t.Errorf("dirSig.read = %d, was %d: not refreshed", got, before)
193	}
194}
195
196// BenchmarkRefsPackedRefs lists 5000 packed refs with a steady packed-refs
197// and right after it was rewritten (inside the racy window).
198func BenchmarkRefsPackedRefs(b *testing.B) {
199	requireGit(b)
200	dir := b.TempDir()
201	runGit(b, dir, nil, nil, "init", "-q")
202	runGit(b, dir, nil, nil, "commit", "-q", "--allow-empty", "-m", "x")
203	id := mustID(b, strings.TrimSpace(string(runGit(b, dir, nil, nil, "rev-parse", "HEAD"))))
204	const n = 5000
205	writePackedRefs(b, dir+"/.git", id, n)
206	r, err := Open(dir)
207	if err != nil {
208		b.Fatal(err)
209	}
210	defer func() { _ = r.Close() }()
211	list := func(b *testing.B) {
212		count := 0
213		for _, err := range r.Refs("refs/heads/") {
214			if err != nil {
215				b.Fatal(err)
216			}
217			count++
218		}
219		if count != n+1 {
220			b.Fatalf("%d refs", count)
221		}
222	}
223	for _, c := range []struct {
224		name string
225		age  time.Duration
226	}{{"steady", time.Hour}, {"just-rewritten", 0}} {
227		b.Run(c.name, func(b *testing.B) {
228			mt := time.Now().Add(-c.age)
229			if err := os.Chtimes(dir+"/.git/packed-refs", mt, mt); err != nil {
230				b.Fatal(err)
231			}
232			if _, err := r.loadPacked(true); err != nil {
233				b.Fatal(err)
234			}
235			b.ReportAllocs()
236			for b.Loop() {
237				list(b)
238			}
239		})
240	}
241}

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