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}