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}