perf(repo): compute Refresh paths once and stat without allocating (GO-006)
Open builds the paths of everything Refresh looks at (layout files, refs/replace, packed-refs, objects/pack, the commit-graph files). They are stat'ed into a stack Stat_t: on linux amd64 and arm64 with the fstatat system call on a NUL-terminated path built once, elsewhere on unix with syscall.Stat (one allocation), with os.Stat behind a build tag for other systems. refs/replace is listed only when its stat data changed since it was last found empty, with Git's racy rule. An unchanged Refresh allocates nothing.
- commit
- 3d0ad4fda2d7072f7d1d9ecc9ad82992a4605db8
- author
- brice · · 2026-10-11 00:18 UTC
- parent
- 3da52c2
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
16 files · +531 -29
M bench_test.go +16 -0
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "strings" | |
| 14 | 14 | "sync" | |
| 15 | 15 | "testing" | |
| 16 | + | "time" | |
| 16 | 17 | ) | |
| 17 | 18 | ||
| 18 | 19 | // benchFixture is a repository with loose objects, a deltified pack and a | |
| @@ -412,8 +413,23 @@ func BenchmarkOpenBlob(b *testing.B) { | |||
| 412 | 413 | } | |
| 413 | 414 | } | |
| 414 | 415 | ||
| 416 | + | // BenchmarkRefresh is Refresh on a repository that did not change, the case | |
| 417 | + | // of every Refresh but the one after a push. The generated fixture was written | |
| 418 | + | // a moment ago, inside the racy window of its stat data, so its files are | |
| 419 | + | // aged first and one Refresh records that they are old. | |
| 415 | 420 | func BenchmarkRefresh(b *testing.B) { | |
| 416 | 421 | fx := getBench(b) | |
| 422 | + | if os.Getenv("ERMITE_GIT_BENCH_REPO") == "" { | |
| 423 | + | old := time.Now().Add(-time.Hour) | |
| 424 | + | for _, p := range []string{"packed-refs", "objects/pack", "objects/info/commit-graph", "objects/info/commit-graphs/commit-graph-chain"} { | |
| 425 | + | _ = os.Chtimes(fx.r.Dir()+"/"+p, old, old) | |
| 426 | + | } | |
| 427 | + | for range 3 { | |
| 428 | + | if err := fx.r.Refresh(); err != nil { | |
| 429 | + | b.Fatal(err) | |
| 430 | + | } | |
| 431 | + | } | |
| 432 | + | } | |
| 417 | 433 | b.ReportAllocs() | |
| 418 | 434 | for b.Loop() { | |
| 419 | 435 | if err := fx.r.Refresh(); err != nil { | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "strings" | 13 | "strings" | ||
| 14 | "sync" | 14 | "sync" | ||
| 15 | "testing" | 15 | "testing" | ||
| 16 | + | "time" | |||
| 16 | ) | 17 | ) | ||
| 17 | 18 | ||||
| 18 | // benchFixture is a repository with loose objects, a deltified pack and a | 19 | // benchFixture is a repository with loose objects, a deltified pack and a | ||
| @@ -412,8 +413,23 @@ func BenchmarkOpenBlob(b *testing.B) { | |||||
| 412 | } | 413 | } | ||
| 413 | } | 414 | } | ||
| 414 | 415 | ||||
| 416 | + | // BenchmarkRefresh is Refresh on a repository that did not change, the case | |||
| 417 | + | // of every Refresh but the one after a push. The generated fixture was written | |||
| 418 | + | // a moment ago, inside the racy window of its stat data, so its files are | |||
| 419 | + | // aged first and one Refresh records that they are old. | |||
| 415 | func BenchmarkRefresh(b *testing.B) { | 420 | func BenchmarkRefresh(b *testing.B) { | ||
| 416 | fx := getBench(b) | 421 | fx := getBench(b) | ||
| 422 | + | if os.Getenv("ERMITE_GIT_BENCH_REPO") == "" { | |||
| 423 | + | old := time.Now().Add(-time.Hour) | |||
| 424 | + | for _, p := range []string{"packed-refs", "objects/pack", "objects/info/commit-graph", "objects/info/commit-graphs/commit-graph-chain"} { | |||
| 425 | + | _ = os.Chtimes(fx.r.Dir()+"/"+p, old, old) | |||
| 426 | + | } | |||
| 427 | + | for range 3 { | |||
| 428 | + | if err := fx.r.Refresh(); err != nil { | |||
| 429 | + | b.Fatal(err) | |||
| 430 | + | } | |||
| 431 | + | } | |||
| 432 | + | } | |||
| 417 | b.ReportAllocs() | 433 | b.ReportAllocs() | ||
| 418 | for b.Loop() { | 434 | for b.Loop() { | ||
| 419 | if err := fx.r.Refresh(); err != nil { | 435 | if err := fx.r.Refresh(); err != nil { | ||
M graph.go +22 -12
| @@ -746,22 +746,32 @@ func (s graphStamp) trusted(at int64) bool { | |||
| 746 | 746 | return newest == math.MinInt64 || at-newest > int64(racyWindow) | |
| 747 | 747 | } | |
| 748 | 748 | ||
| 749 | - | func statGraphFile(path string) graphFileStamp { | |
| 750 | - | st, err := os.Stat(path) | |
| 751 | - | if err != nil { | |
| 749 | + | // graphPaths are the paths statGraph looks at, built once. | |
| 750 | + | type graphPaths struct{ single, chain statPath } | |
| 751 | + | ||
| 752 | + | func newGraphPaths(objectsDir string) graphPaths { | |
| 753 | + | return graphPaths{ | |
| 754 | + | single: newStatPath(objectsDir + "/info/commit-graph"), | |
| 755 | + | chain: newStatPath(objectsDir + "/info/commit-graphs/commit-graph-chain"), | |
| 756 | + | } | |
| 757 | + | } | |
| 758 | + | ||
| 759 | + | func statGraphFile(p *statPath) graphFileStamp { | |
| 760 | + | st, err := p.stat(false) | |
| 761 | + | if err != nil || !st.exists { | |
| 752 | 762 | return graphFileStamp{} | |
| 753 | 763 | } | |
| 754 | - | s := graphFileStamp{exists: true, size: st.Size(), mtime: st.ModTime().UnixNano()} | |
| 755 | - | s.ino = fileIno(st) | |
| 756 | - | return s | |
| 764 | + | return graphFileStamp{exists: true, size: st.size, mtime: st.mtime, ino: st.ino} | |
| 765 | + | } | |
| 766 | + | ||
| 767 | + | // stamp returns the stamp of the commit-graph files. Comparing two stamps | |
| 768 | + | // tells whether openCommitGraph may give another result. | |
| 769 | + | func (g *graphPaths) stamp() graphStamp { | |
| 770 | + | return graphStamp{single: statGraphFile(&g.single), chain: statGraphFile(&g.chain)} | |
| 757 | 771 | } | |
| 758 | 772 | ||
| 759 | 773 | // statGraph returns the stamp of the commit-graph files of objectsDir. | |
| 760 | - | // Comparing two stamps tells whether openCommitGraph may give another result. | |
| 761 | 774 | func statGraph(objectsDir string) graphStamp { | |
| 762 | - | info := filepath.Join(objectsDir, "info") | |
| 763 | - | return graphStamp{ | |
| 764 | - | single: statGraphFile(filepath.Join(info, "commit-graph")), | |
| 765 | - | chain: statGraphFile(filepath.Join(info, "commit-graphs", "commit-graph-chain")), | |
| 766 | - | } | |
| 775 | + | p := newGraphPaths(objectsDir) | |
| 776 | + | return p.stamp() | |
| 767 | 777 | } | |
| @@ -746,22 +746,32 @@ func (s graphStamp) trusted(at int64) bool { | |||||
| 746 | return newest == math.MinInt64 || at-newest > int64(racyWindow) | 746 | return newest == math.MinInt64 || at-newest > int64(racyWindow) | ||
| 747 | } | 747 | } | ||
| 748 | 748 | ||||
| 749 | - | func statGraphFile(path string) graphFileStamp { | 749 | + | // graphPaths are the paths statGraph looks at, built once. |
| 750 | - | st, err := os.Stat(path) | 750 | + | type graphPaths struct{ single, chain statPath } |
| 751 | - | if err != nil { | 751 | + | |
| 752 | + | func newGraphPaths(objectsDir string) graphPaths { | |||
| 753 | + | return graphPaths{ | |||
| 754 | + | single: newStatPath(objectsDir + "/info/commit-graph"), | |||
| 755 | + | chain: newStatPath(objectsDir + "/info/commit-graphs/commit-graph-chain"), | |||
| 756 | + | } | |||
| 757 | + | } | |||
| 758 | + | ||||
| 759 | + | func statGraphFile(p *statPath) graphFileStamp { | |||
| 760 | + | st, err := p.stat(false) | |||
| 761 | + | if err != nil || !st.exists { | |||
| 752 | return graphFileStamp{} | 762 | return graphFileStamp{} | ||
| 753 | } | 763 | } | ||
| 754 | - | s := graphFileStamp{exists: true, size: st.Size(), mtime: st.ModTime().UnixNano()} | 764 | + | return graphFileStamp{exists: true, size: st.size, mtime: st.mtime, ino: st.ino} |
| 755 | - | s.ino = fileIno(st) | 765 | + | } |
| 756 | - | return s | 766 | + | |
| 767 | + | // stamp returns the stamp of the commit-graph files. Comparing two stamps | |||
| 768 | + | // tells whether openCommitGraph may give another result. | |||
| 769 | + | func (g *graphPaths) stamp() graphStamp { | |||
| 770 | + | return graphStamp{single: statGraphFile(&g.single), chain: statGraphFile(&g.chain)} | |||
| 757 | } | 771 | } | ||
| 758 | 772 | ||||
| 759 | // statGraph returns the stamp of the commit-graph files of objectsDir. | 773 | // statGraph returns the stamp of the commit-graph files of objectsDir. | ||
| 760 | - | // Comparing two stamps tells whether openCommitGraph may give another result. | |||
| 761 | func statGraph(objectsDir string) graphStamp { | 774 | func statGraph(objectsDir string) graphStamp { | ||
| 762 | - | info := filepath.Join(objectsDir, "info") | 775 | + | p := newGraphPaths(objectsDir) |
| 763 | - | return graphStamp{ | 776 | + | return p.stamp() |
| 764 | - | single: statGraphFile(filepath.Join(info, "commit-graph")), | |||
| 765 | - | chain: statGraphFile(filepath.Join(info, "commit-graphs", "commit-graph-chain")), | |||
| 766 | - | } | |||
| 767 | } | 777 | } | ||
A refresh_test.go +194 -0
| @@ -0,0 +1,194 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import ( | |
| 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). | |
| 14 | + | func 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. | |
| 26 | + | func 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). | |
| 63 | + | func 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. | |
| 104 | + | func 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 | + | ||
| 121 | + | func 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 | + | ||
| 130 | + | func 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 | + | } | |
| @@ -0,0 +1,194 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 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). | |||
| 14 | + | func 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. | |||
| 26 | + | func 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). | |||
| 63 | + | func 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. | |||
| 104 | + | func 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 | + | ||||
| 121 | + | func 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 | + | ||||
| 130 | + | func 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 | + | } | |||
M refs.go +2 -2
| @@ -70,8 +70,7 @@ func (p *packedRefs) find(name string) (packedRef, bool) { | |||
| 70 | 70 | // when its stat data changed (or always with force). Concurrent callers share | |
| 71 | 71 | // one parse: the check is repeated once pmu is held. | |
| 72 | 72 | func (r *Repo) loadPacked(force bool) (*packedRefs, error) { | |
| 73 | - | path := r.dir + "/packed-refs" | |
| 74 | - | sig, err := statSig(path) | |
| 73 | + | sig, err := r.paths.packedRefs.sig() | |
| 75 | 74 | if err != nil { | |
| 76 | 75 | return nil, err | |
| 77 | 76 | } | |
| @@ -88,6 +87,7 @@ func (r *Repo) loadPacked(force bool) (*packedRefs, error) { | |||
| 88 | 87 | return cur, nil | |
| 89 | 88 | } | |
| 90 | 89 | read := time.Now().UnixNano() | |
| 90 | + | path := r.dir + "/packed-refs" | |
| 91 | 91 | f, err := os.Open(path) | |
| 92 | 92 | if errors.Is(err, fs.ErrNotExist) { | |
| 93 | 93 | p := &packedRefs{sig: fileSig{read: read}} | |
| @@ -70,8 +70,7 @@ func (p *packedRefs) find(name string) (packedRef, bool) { | |||||
| 70 | // when its stat data changed (or always with force). Concurrent callers share | 70 | // when its stat data changed (or always with force). Concurrent callers share | ||
| 71 | // one parse: the check is repeated once pmu is held. | 71 | // one parse: the check is repeated once pmu is held. | ||
| 72 | func (r *Repo) loadPacked(force bool) (*packedRefs, error) { | 72 | func (r *Repo) loadPacked(force bool) (*packedRefs, error) { | ||
| 73 | - | path := r.dir + "/packed-refs" | 73 | + | sig, err := r.paths.packedRefs.sig() |
| 74 | - | sig, err := statSig(path) | |||
| 75 | if err != nil { | 74 | if err != nil { | ||
| 76 | return nil, err | 75 | return nil, err | ||
| 77 | } | 76 | } | ||
| @@ -88,6 +87,7 @@ func (r *Repo) loadPacked(force bool) (*packedRefs, error) { | |||||
| 88 | return cur, nil | 87 | return cur, nil | ||
| 89 | } | 88 | } | ||
| 90 | read := time.Now().UnixNano() | 89 | read := time.Now().UnixNano() | ||
| 90 | + | path := r.dir + "/packed-refs" | |||
| 91 | f, err := os.Open(path) | 91 | f, err := os.Open(path) | ||
| 92 | if errors.Is(err, fs.ErrNotExist) { | 92 | if errors.Is(err, fs.ErrNotExist) { | ||
| 93 | p := &packedRefs{sig: fileSig{read: read}} | 93 | p := &packedRefs{sig: fileSig{read: read}} | ||
M repo.go +79 -10
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | "math" | |
| 10 | 10 | "os" | |
| 11 | 11 | "path/filepath" | |
| 12 | + | "strings" | |
| 12 | 13 | "sync" | |
| 13 | 14 | "sync/atomic" | |
| 14 | 15 | "syscall" | |
| @@ -75,6 +76,8 @@ type Repo struct { | |||
| 75 | 76 | graph atomic.Pointer[graphState] // see walk.go | |
| 76 | 77 | gmu sync.Mutex // serializes commit-graph reloads | |
| 77 | 78 | ||
| 79 | + | paths *repoPaths // the paths Refresh stats, built once | |
| 80 | + | ||
| 78 | 81 | clock func() time.Time // tests only: time.Now when nil | |
| 79 | 82 | } | |
| 80 | 83 | ||
| @@ -109,10 +112,11 @@ func Open(dir string, opts ...Option) (*Repo, error) { | |||
| 109 | 112 | if err := checkConfig(filepath.Join(gitDir, "config")); err != nil { | |
| 110 | 113 | return nil, err | |
| 111 | 114 | } | |
| 112 | - | if err := checkLayout(gitDir); err != nil { | |
| 115 | + | paths := newRepoPaths(gitDir) | |
| 116 | + | if err := paths.checkLayout(); err != nil { | |
| 113 | 117 | return nil, err | |
| 114 | 118 | } | |
| 115 | - | r := &Repo{dir: gitDir, opts: o, cache: newDeltaCache(o.cacheBytes)} | |
| 119 | + | r := &Repo{dir: gitDir, opts: o, cache: newDeltaCache(o.cacheBytes), paths: paths} | |
| 116 | 120 | r.snap.Store(newSnapshot(nil)) | |
| 117 | 121 | if _, err := r.reloadPacks(true); err != nil { | |
| 118 | 122 | _ = r.Close() | |
| @@ -151,7 +155,7 @@ func (r *Repo) Close() error { | |||
| 151 | 155 | // that disappeared stay mapped until the readers using them are done, so a | |
| 152 | 156 | // concurrent "git repack -ad" never breaks a read in flight. | |
| 153 | 157 | func (r *Repo) Refresh() error { | |
| 154 | - | if err := checkLayout(r.dir); err != nil { | |
| 158 | + | if err := r.paths.checkLayout(); err != nil { | |
| 155 | 159 | return err | |
| 156 | 160 | } | |
| 157 | 161 | if _, err := r.reloadPacks(false); err != nil { | |
| @@ -202,20 +206,84 @@ func exists(path string) bool { | |||
| 202 | 206 | return err == nil | |
| 203 | 207 | } | |
| 204 | 208 | ||
| 205 | - | // checkLayout rejects the repository layouts that change what objects or | |
| 206 | - | // refs mean and that the package does not implement. | |
| 207 | - | func checkLayout(dir string) error { | |
| 208 | - | for _, c := range []struct{ path, why string }{ | |
| 209 | + | // repoPaths holds the paths that Refresh looks at, built once by Open, so | |
| 210 | + | // that an unchanged repository costs a few stat system calls and no | |
| 211 | + | // allocation. | |
| 212 | + | type repoPaths struct { | |
| 213 | + | dir string | |
| 214 | + | ||
| 215 | + | // unsupported are the files whose presence makes a repository | |
| 216 | + | // unsupported, with the reason. | |
| 217 | + | unsupported [4]layoutFile | |
| 218 | + | replace statPath // refs/replace | |
| 219 | + | packedRefs statPath | |
| 220 | + | packDir statPath // objects/pack | |
| 221 | + | graph graphPaths | |
| 222 | + | ||
| 223 | + | mu sync.Mutex | |
| 224 | + | replaceSig fileSig // of refs/replace when it was last listed and found empty | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | type layoutFile struct { | |
| 228 | + | path statPath | |
| 229 | + | why string | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | func newRepoPaths(dir string) *repoPaths { | |
| 233 | + | p := &repoPaths{ | |
| 234 | + | dir: dir, | |
| 235 | + | replace: newStatPath(dir + "/refs/replace"), | |
| 236 | + | packedRefs: newStatPath(dir + "/packed-refs"), | |
| 237 | + | packDir: newStatPath(dir + "/objects/pack"), | |
| 238 | + | graph: newGraphPaths(dir + "/objects"), | |
| 239 | + | } | |
| 240 | + | for i, c := range [...]struct{ path, why string }{ | |
| 209 | 241 | {"shallow", "shallow repository"}, | |
| 210 | 242 | {"info/grafts", "grafts file"}, | |
| 211 | 243 | {"commondir", "linked worktree"}, | |
| 212 | 244 | {"objects/info/alternates", "alternate object stores"}, | |
| 213 | 245 | } { | |
| 214 | - | if exists(filepath.Join(dir, c.path)) { | |
| 215 | - | return unsupported("%s (%s)", c.why, c.path) | |
| 246 | + | p.unsupported[i] = layoutFile{newStatPath(dir + "/" + c.path), c.why} | |
| 247 | + | } | |
| 248 | + | return p | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | // checkLayout rejects the repository layouts that change what objects or | |
| 252 | + | // refs mean and that the package does not implement. | |
| 253 | + | func checkLayout(dir string) error { return newRepoPaths(dir).checkLayout() } | |
| 254 | + | ||
| 255 | + | func (p *repoPaths) checkLayout() error { | |
| 256 | + | for i := range p.unsupported { | |
| 257 | + | c := &p.unsupported[i] | |
| 258 | + | if st, err := c.path.stat(true); err == nil && st.exists { | |
| 259 | + | return unsupported("%s (%s)", c.why, strings.TrimPrefix(c.path.path(), p.dir+"/")) | |
| 216 | 260 | } | |
| 217 | 261 | } | |
| 218 | - | f, err := os.Open(filepath.Join(dir, "refs", "replace")) | |
| 262 | + | return p.checkReplace() | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | // checkReplace refuses a repository with replace refs. The directory is only | |
| 266 | + | // listed when its stat data changed since it was last found empty (a replace | |
| 267 | + | // ref is a file in it, so adding one changes its modification time), with | |
| 268 | + | // Git's racy rule for a change that leaves the stat data as it was. | |
| 269 | + | func (p *repoPaths) checkReplace() error { | |
| 270 | + | read := time.Now().UnixNano() // before the stat, see sigOf | |
| 271 | + | st, err := p.replace.stat(false) | |
| 272 | + | if err != nil { | |
| 273 | + | return err | |
| 274 | + | } | |
| 275 | + | p.mu.Lock() | |
| 276 | + | defer p.mu.Unlock() | |
| 277 | + | if !st.exists || !st.isDir { | |
| 278 | + | p.replaceSig = fileSig{} | |
| 279 | + | return nil | |
| 280 | + | } | |
| 281 | + | sig := fileSig{exists: true, size: st.size, mtime: st.mtime, ino: st.ino, read: read} | |
| 282 | + | if p.replaceSig.exists && p.replaceSig.same(sig) { | |
| 283 | + | return nil | |
| 284 | + | } | |
| 285 | + | p.replaceSig = fileSig{} | |
| 286 | + | f, err := os.Open(p.replace.path()) | |
| 219 | 287 | if err != nil { | |
| 220 | 288 | if isNotExist(err) { | |
| 221 | 289 | return nil | |
| @@ -228,6 +296,7 @@ func checkLayout(dir string) error { | |||
| 228 | 296 | case rerr == nil && len(names) > 0: | |
| 229 | 297 | return unsupported("replace refs (refs/replace)") | |
| 230 | 298 | case rerr == nil, errors.Is(rerr, io.EOF), isNotExist(rerr): | |
| 299 | + | p.replaceSig = sig | |
| 231 | 300 | return nil | |
| 232 | 301 | } | |
| 233 | 302 | return rerr | |
| @@ -9,6 +9,7 @@ import ( | |||||
| 9 | "math" | 9 | "math" | ||
| 10 | "os" | 10 | "os" | ||
| 11 | "path/filepath" | 11 | "path/filepath" | ||
| 12 | + | "strings" | |||
| 12 | "sync" | 13 | "sync" | ||
| 13 | "sync/atomic" | 14 | "sync/atomic" | ||
| 14 | "syscall" | 15 | "syscall" | ||
| @@ -75,6 +76,8 @@ type Repo struct { | |||||
| 75 | graph atomic.Pointer[graphState] // see walk.go | 76 | graph atomic.Pointer[graphState] // see walk.go | ||
| 76 | gmu sync.Mutex // serializes commit-graph reloads | 77 | gmu sync.Mutex // serializes commit-graph reloads | ||
| 77 | 78 | ||||
| 79 | + | paths *repoPaths // the paths Refresh stats, built once | |||
| 80 | + | ||||
| 78 | clock func() time.Time // tests only: time.Now when nil | 81 | clock func() time.Time // tests only: time.Now when nil | ||
| 79 | } | 82 | } | ||
| 80 | 83 | ||||
| @@ -109,10 +112,11 @@ func Open(dir string, opts ...Option) (*Repo, error) { | |||||
| 109 | if err := checkConfig(filepath.Join(gitDir, "config")); err != nil { | 112 | if err := checkConfig(filepath.Join(gitDir, "config")); err != nil { | ||
| 110 | return nil, err | 113 | return nil, err | ||
| 111 | } | 114 | } | ||
| 112 | - | if err := checkLayout(gitDir); err != nil { | 115 | + | paths := newRepoPaths(gitDir) |
| 116 | + | if err := paths.checkLayout(); err != nil { | |||
| 113 | return nil, err | 117 | return nil, err | ||
| 114 | } | 118 | } | ||
| 115 | - | r := &Repo{dir: gitDir, opts: o, cache: newDeltaCache(o.cacheBytes)} | 119 | + | r := &Repo{dir: gitDir, opts: o, cache: newDeltaCache(o.cacheBytes), paths: paths} |
| 116 | r.snap.Store(newSnapshot(nil)) | 120 | r.snap.Store(newSnapshot(nil)) | ||
| 117 | if _, err := r.reloadPacks(true); err != nil { | 121 | if _, err := r.reloadPacks(true); err != nil { | ||
| 118 | _ = r.Close() | 122 | _ = r.Close() | ||
| @@ -151,7 +155,7 @@ func (r *Repo) Close() error { | |||||
| 151 | // that disappeared stay mapped until the readers using them are done, so a | 155 | // that disappeared stay mapped until the readers using them are done, so a | ||
| 152 | // concurrent "git repack -ad" never breaks a read in flight. | 156 | // concurrent "git repack -ad" never breaks a read in flight. | ||
| 153 | func (r *Repo) Refresh() error { | 157 | func (r *Repo) Refresh() error { | ||
| 154 | - | if err := checkLayout(r.dir); err != nil { | 158 | + | if err := r.paths.checkLayout(); err != nil { |
| 155 | return err | 159 | return err | ||
| 156 | } | 160 | } | ||
| 157 | if _, err := r.reloadPacks(false); err != nil { | 161 | if _, err := r.reloadPacks(false); err != nil { | ||
| @@ -202,20 +206,84 @@ func exists(path string) bool { | |||||
| 202 | return err == nil | 206 | return err == nil | ||
| 203 | } | 207 | } | ||
| 204 | 208 | ||||
| 205 | - | // checkLayout rejects the repository layouts that change what objects or | 209 | + | // repoPaths holds the paths that Refresh looks at, built once by Open, so |
| 206 | - | // refs mean and that the package does not implement. | 210 | + | // that an unchanged repository costs a few stat system calls and no |
| 207 | - | func checkLayout(dir string) error { | 211 | + | // allocation. |
| 208 | - | for _, c := range []struct{ path, why string }{ | 212 | + | type repoPaths struct { |
| 213 | + | dir string | |||
| 214 | + | ||||
| 215 | + | // unsupported are the files whose presence makes a repository | |||
| 216 | + | // unsupported, with the reason. | |||
| 217 | + | unsupported [4]layoutFile | |||
| 218 | + | replace statPath // refs/replace | |||
| 219 | + | packedRefs statPath | |||
| 220 | + | packDir statPath // objects/pack | |||
| 221 | + | graph graphPaths | |||
| 222 | + | ||||
| 223 | + | mu sync.Mutex | |||
| 224 | + | replaceSig fileSig // of refs/replace when it was last listed and found empty | |||
| 225 | + | } | |||
| 226 | + | ||||
| 227 | + | type layoutFile struct { | |||
| 228 | + | path statPath | |||
| 229 | + | why string | |||
| 230 | + | } | |||
| 231 | + | ||||
| 232 | + | func newRepoPaths(dir string) *repoPaths { | |||
| 233 | + | p := &repoPaths{ | |||
| 234 | + | dir: dir, | |||
| 235 | + | replace: newStatPath(dir + "/refs/replace"), | |||
| 236 | + | packedRefs: newStatPath(dir + "/packed-refs"), | |||
| 237 | + | packDir: newStatPath(dir + "/objects/pack"), | |||
| 238 | + | graph: newGraphPaths(dir + "/objects"), | |||
| 239 | + | } | |||
| 240 | + | for i, c := range [...]struct{ path, why string }{ | |||
| 209 | {"shallow", "shallow repository"}, | 241 | {"shallow", "shallow repository"}, | ||
| 210 | {"info/grafts", "grafts file"}, | 242 | {"info/grafts", "grafts file"}, | ||
| 211 | {"commondir", "linked worktree"}, | 243 | {"commondir", "linked worktree"}, | ||
| 212 | {"objects/info/alternates", "alternate object stores"}, | 244 | {"objects/info/alternates", "alternate object stores"}, | ||
| 213 | } { | 245 | } { | ||
| 214 | - | if exists(filepath.Join(dir, c.path)) { | 246 | + | p.unsupported[i] = layoutFile{newStatPath(dir + "/" + c.path), c.why} |
| 215 | - | return unsupported("%s (%s)", c.why, c.path) | 247 | + | } |
| 248 | + | return p | |||
| 249 | + | } | |||
| 250 | + | ||||
| 251 | + | // checkLayout rejects the repository layouts that change what objects or | |||
| 252 | + | // refs mean and that the package does not implement. | |||
| 253 | + | func checkLayout(dir string) error { return newRepoPaths(dir).checkLayout() } | |||
| 254 | + | ||||
| 255 | + | func (p *repoPaths) checkLayout() error { | |||
| 256 | + | for i := range p.unsupported { | |||
| 257 | + | c := &p.unsupported[i] | |||
| 258 | + | if st, err := c.path.stat(true); err == nil && st.exists { | |||
| 259 | + | return unsupported("%s (%s)", c.why, strings.TrimPrefix(c.path.path(), p.dir+"/")) | |||
| 216 | } | 260 | } | ||
| 217 | } | 261 | } | ||
| 218 | - | f, err := os.Open(filepath.Join(dir, "refs", "replace")) | 262 | + | return p.checkReplace() |
| 263 | + | } | |||
| 264 | + | ||||
| 265 | + | // checkReplace refuses a repository with replace refs. The directory is only | |||
| 266 | + | // listed when its stat data changed since it was last found empty (a replace | |||
| 267 | + | // ref is a file in it, so adding one changes its modification time), with | |||
| 268 | + | // Git's racy rule for a change that leaves the stat data as it was. | |||
| 269 | + | func (p *repoPaths) checkReplace() error { | |||
| 270 | + | read := time.Now().UnixNano() // before the stat, see sigOf | |||
| 271 | + | st, err := p.replace.stat(false) | |||
| 272 | + | if err != nil { | |||
| 273 | + | return err | |||
| 274 | + | } | |||
| 275 | + | p.mu.Lock() | |||
| 276 | + | defer p.mu.Unlock() | |||
| 277 | + | if !st.exists || !st.isDir { | |||
| 278 | + | p.replaceSig = fileSig{} | |||
| 279 | + | return nil | |||
| 280 | + | } | |||
| 281 | + | sig := fileSig{exists: true, size: st.size, mtime: st.mtime, ino: st.ino, read: read} | |||
| 282 | + | if p.replaceSig.exists && p.replaceSig.same(sig) { | |||
| 283 | + | return nil | |||
| 284 | + | } | |||
| 285 | + | p.replaceSig = fileSig{} | |||
| 286 | + | f, err := os.Open(p.replace.path()) | |||
| 219 | if err != nil { | 287 | if err != nil { | ||
| 220 | if isNotExist(err) { | 288 | if isNotExist(err) { | ||
| 221 | return nil | 289 | return nil | ||
| @@ -228,6 +296,7 @@ func checkLayout(dir string) error { | |||||
| 228 | case rerr == nil && len(names) > 0: | 296 | case rerr == nil && len(names) > 0: | ||
| 229 | return unsupported("replace refs (refs/replace)") | 297 | return unsupported("replace refs (refs/replace)") | ||
| 230 | case rerr == nil, errors.Is(rerr, io.EOF), isNotExist(rerr): | 298 | case rerr == nil, errors.Is(rerr, io.EOF), isNotExist(rerr): | ||
| 299 | + | p.replaceSig = sig | |||
| 231 | return nil | 300 | return nil | ||
| 232 | } | 301 | } | ||
| 233 | return rerr | 302 | return rerr | ||
M snapshot.go +2 -2
| @@ -132,15 +132,15 @@ func (r *Repo) reloadPacks(force bool) (bool, error) { | |||
| 132 | 132 | if r.closed { | |
| 133 | 133 | return false, ErrClosed | |
| 134 | 134 | } | |
| 135 | - | dir := r.packDir() | |
| 136 | 135 | cur := r.snap.Load() | |
| 137 | - | dsig, err := statSig(dir) | |
| 136 | + | dsig, err := r.paths.packDir.sig() | |
| 138 | 137 | if err != nil { | |
| 139 | 138 | return false, err | |
| 140 | 139 | } | |
| 141 | 140 | if !force && cur.dirSig.same(dsig) { | |
| 142 | 141 | return false, nil | |
| 143 | 142 | } | |
| 143 | + | dir := r.packDir() | |
| 144 | 144 | entries, err := os.ReadDir(dir) | |
| 145 | 145 | if err != nil && !errors.Is(err, fs.ErrNotExist) { | |
| 146 | 146 | return false, err | |
| @@ -132,15 +132,15 @@ func (r *Repo) reloadPacks(force bool) (bool, error) { | |||||
| 132 | if r.closed { | 132 | if r.closed { | ||
| 133 | return false, ErrClosed | 133 | return false, ErrClosed | ||
| 134 | } | 134 | } | ||
| 135 | - | dir := r.packDir() | |||
| 136 | cur := r.snap.Load() | 135 | cur := r.snap.Load() | ||
| 137 | - | dsig, err := statSig(dir) | 136 | + | dsig, err := r.paths.packDir.sig() |
| 138 | if err != nil { | 137 | if err != nil { | ||
| 139 | return false, err | 138 | return false, err | ||
| 140 | } | 139 | } | ||
| 141 | if !force && cur.dirSig.same(dsig) { | 140 | if !force && cur.dirSig.same(dsig) { | ||
| 142 | return false, nil | 141 | return false, nil | ||
| 143 | } | 142 | } | ||
| 143 | + | dir := r.packDir() | |||
| 144 | entries, err := os.ReadDir(dir) | 144 | entries, err := os.ReadDir(dir) | ||
| 145 | if err != nil && !errors.Is(err, fs.ErrNotExist) { | 145 | if err != nil && !errors.Is(err, fs.ErrNotExist) { | ||
| 146 | return false, err | 146 | return false, err | ||
A stat.go +50 -0
| @@ -0,0 +1,50 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import "time" | |
| 6 | + | ||
| 7 | + | // fileStat is the part of a stat result that change detection uses. | |
| 8 | + | type fileStat struct { | |
| 9 | + | exists bool | |
| 10 | + | isDir bool | |
| 11 | + | size int64 | |
| 12 | + | mtime int64 // unix nanoseconds | |
| 13 | + | ino uint64 | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | // statPath is a path prepared once, so that stat-ing it again and again (what | |
| 17 | + | // Refresh does) does not build a string, nor the NUL-terminated copy of it | |
| 18 | + | // that the system call takes, at each call. | |
| 19 | + | type statPath struct { | |
| 20 | + | nul []byte // the path and a NUL byte | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | func newStatPath(path string) statPath { | |
| 24 | + | return statPath{nul: append([]byte(path), 0)} | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | func (p *statPath) path() string { return string(p.nul[:len(p.nul)-1]) } | |
| 28 | + | ||
| 29 | + | // stat returns the stat data of the path, following symbolic links unless | |
| 30 | + | // lstat. A path that is absent, or has a file in place of a directory on the | |
| 31 | + | // way, is not an error: exists is false. | |
| 32 | + | func (p *statPath) stat(lstat bool) (fileStat, error) { | |
| 33 | + | var st fileStat | |
| 34 | + | err := statNul(p.nul, lstat, &st) | |
| 35 | + | return st, err | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // sig returns the signature of the file. The time of the read is taken | |
| 39 | + | // before the stat, see sigOf. | |
| 40 | + | func (p *statPath) sig() (fileSig, error) { | |
| 41 | + | read := time.Now().UnixNano() | |
| 42 | + | st, err := p.stat(false) | |
| 43 | + | if err != nil { | |
| 44 | + | return fileSig{}, err | |
| 45 | + | } | |
| 46 | + | if !st.exists { | |
| 47 | + | return fileSig{read: read}, nil | |
| 48 | + | } | |
| 49 | + | return fileSig{exists: true, size: st.size, mtime: st.mtime, ino: st.ino, read: read}, nil | |
| 50 | + | } | |
| @@ -0,0 +1,50 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import "time" | |||
| 6 | + | ||||
| 7 | + | // fileStat is the part of a stat result that change detection uses. | |||
| 8 | + | type fileStat struct { | |||
| 9 | + | exists bool | |||
| 10 | + | isDir bool | |||
| 11 | + | size int64 | |||
| 12 | + | mtime int64 // unix nanoseconds | |||
| 13 | + | ino uint64 | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | // statPath is a path prepared once, so that stat-ing it again and again (what | |||
| 17 | + | // Refresh does) does not build a string, nor the NUL-terminated copy of it | |||
| 18 | + | // that the system call takes, at each call. | |||
| 19 | + | type statPath struct { | |||
| 20 | + | nul []byte // the path and a NUL byte | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | func newStatPath(path string) statPath { | |||
| 24 | + | return statPath{nul: append([]byte(path), 0)} | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | func (p *statPath) path() string { return string(p.nul[:len(p.nul)-1]) } | |||
| 28 | + | ||||
| 29 | + | // stat returns the stat data of the path, following symbolic links unless | |||
| 30 | + | // lstat. A path that is absent, or has a file in place of a directory on the | |||
| 31 | + | // way, is not an error: exists is false. | |||
| 32 | + | func (p *statPath) stat(lstat bool) (fileStat, error) { | |||
| 33 | + | var st fileStat | |||
| 34 | + | err := statNul(p.nul, lstat, &st) | |||
| 35 | + | return st, err | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // sig returns the signature of the file. The time of the read is taken | |||
| 39 | + | // before the stat, see sigOf. | |||
| 40 | + | func (p *statPath) sig() (fileSig, error) { | |||
| 41 | + | read := time.Now().UnixNano() | |||
| 42 | + | st, err := p.stat(false) | |||
| 43 | + | if err != nil { | |||
| 44 | + | return fileSig{}, err | |||
| 45 | + | } | |||
| 46 | + | if !st.exists { | |||
| 47 | + | return fileSig{read: read}, nil | |||
| 48 | + | } | |||
| 49 | + | return fileSig{exists: true, size: st.size, mtime: st.mtime, ino: st.ino, read: read}, nil | |||
| 50 | + | } | |||
A stat_linux.go +37 -0
| @@ -0,0 +1,37 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | //go:build linux && (amd64 || arm64) | |
| 4 | + | ||
| 5 | + | package git | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "syscall" | |
| 9 | + | "unsafe" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | const atSymlinkNoFollow = 0x100 | |
| 13 | + | ||
| 14 | + | // statNul is syscall.Stat and syscall.Lstat without their allocation: package | |
| 15 | + | // syscall copies the path into a new NUL-terminated byte slice at each call, | |
| 16 | + | // here the caller built it once. It makes the system call they make on this | |
| 17 | + | // platform, fstatat(AT_FDCWD, path, &stat, flags), into the same Stat_t. | |
| 18 | + | func statNul(nul []byte, lstat bool, st *fileStat) error { | |
| 19 | + | var sys syscall.Stat_t | |
| 20 | + | var flags uintptr | |
| 21 | + | if lstat { | |
| 22 | + | flags = atSymlinkNoFollow | |
| 23 | + | } | |
| 24 | + | atFDCWD := -100 | |
| 25 | + | var errno syscall.Errno | |
| 26 | + | for { | |
| 27 | + | _, _, errno = syscall.Syscall6(sysFstatat, uintptr(atFDCWD), uintptr(unsafe.Pointer(&nul[0])), uintptr(unsafe.Pointer(&sys)), flags, 0, 0) | |
| 28 | + | if errno != syscall.EINTR { | |
| 29 | + | break | |
| 30 | + | } | |
| 31 | + | } | |
| 32 | + | var err error | |
| 33 | + | if errno != 0 { | |
| 34 | + | err = errno | |
| 35 | + | } | |
| 36 | + | return fillStat(st, &sys, nul, err) | |
| 37 | + | } | |
| @@ -0,0 +1,37 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | //go:build linux && (amd64 || arm64) | |||
| 4 | + | ||||
| 5 | + | package git | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "syscall" | |||
| 9 | + | "unsafe" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | const atSymlinkNoFollow = 0x100 | |||
| 13 | + | ||||
| 14 | + | // statNul is syscall.Stat and syscall.Lstat without their allocation: package | |||
| 15 | + | // syscall copies the path into a new NUL-terminated byte slice at each call, | |||
| 16 | + | // here the caller built it once. It makes the system call they make on this | |||
| 17 | + | // platform, fstatat(AT_FDCWD, path, &stat, flags), into the same Stat_t. | |||
| 18 | + | func statNul(nul []byte, lstat bool, st *fileStat) error { | |||
| 19 | + | var sys syscall.Stat_t | |||
| 20 | + | var flags uintptr | |||
| 21 | + | if lstat { | |||
| 22 | + | flags = atSymlinkNoFollow | |||
| 23 | + | } | |||
| 24 | + | atFDCWD := -100 | |||
| 25 | + | var errno syscall.Errno | |||
| 26 | + | for { | |||
| 27 | + | _, _, errno = syscall.Syscall6(sysFstatat, uintptr(atFDCWD), uintptr(unsafe.Pointer(&nul[0])), uintptr(unsafe.Pointer(&sys)), flags, 0, 0) | |||
| 28 | + | if errno != syscall.EINTR { | |||
| 29 | + | break | |||
| 30 | + | } | |||
| 31 | + | } | |||
| 32 | + | var err error | |||
| 33 | + | if errno != 0 { | |||
| 34 | + | err = errno | |||
| 35 | + | } | |||
| 36 | + | return fillStat(st, &sys, nul, err) | |||
| 37 | + | } | |||
A stat_linux_amd64.go +9 -0
| @@ -0,0 +1,9 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | //go:build linux && amd64 | |
| 4 | + | ||
| 5 | + | package git | |
| 6 | + | ||
| 7 | + | import "syscall" | |
| 8 | + | ||
| 9 | + | const sysFstatat = syscall.SYS_NEWFSTATAT | |
| @@ -0,0 +1,9 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | //go:build linux && amd64 | |||
| 4 | + | ||||
| 5 | + | package git | |||
| 6 | + | ||||
| 7 | + | import "syscall" | |||
| 8 | + | ||||
| 9 | + | const sysFstatat = syscall.SYS_NEWFSTATAT | |||
A stat_linux_arm64.go +9 -0
| @@ -0,0 +1,9 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | //go:build linux && arm64 | |
| 4 | + | ||
| 5 | + | package git | |
| 6 | + | ||
| 7 | + | import "syscall" | |
| 8 | + | ||
| 9 | + | const sysFstatat = syscall.SYS_FSTATAT | |
| @@ -0,0 +1,9 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | //go:build linux && arm64 | |||
| 4 | + | ||||
| 5 | + | package git | |||
| 6 | + | ||||
| 7 | + | import "syscall" | |||
| 8 | + | ||||
| 9 | + | const sysFstatat = syscall.SYS_FSTATAT | |||
A stat_mtim.go +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | //go:build linux || openbsd || dragonfly | |
| 4 | + | ||
| 5 | + | package git | |
| 6 | + | ||
| 7 | + | import "syscall" | |
| 8 | + | ||
| 9 | + | func statMtime(sys *syscall.Stat_t) int64 { | |
| 10 | + | return int64(sys.Mtim.Sec)*1e9 + int64(sys.Mtim.Nsec) //nolint:unconvert // the types differ per platform | |
| 11 | + | } | |
| @@ -0,0 +1,11 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | //go:build linux || openbsd || dragonfly | |||
| 4 | + | ||||
| 5 | + | package git | |||
| 6 | + | ||||
| 7 | + | import "syscall" | |||
| 8 | + | ||||
| 9 | + | func statMtime(sys *syscall.Stat_t) int64 { | |||
| 10 | + | return int64(sys.Mtim.Sec)*1e9 + int64(sys.Mtim.Nsec) //nolint:unconvert // the types differ per platform | |||
| 11 | + | } | |||
A stat_mtimespec.go +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | //go:build darwin || freebsd || netbsd | |
| 4 | + | ||
| 5 | + | package git | |
| 6 | + | ||
| 7 | + | import "syscall" | |
| 8 | + | ||
| 9 | + | func statMtime(sys *syscall.Stat_t) int64 { | |
| 10 | + | return int64(sys.Mtimespec.Sec)*1e9 + int64(sys.Mtimespec.Nsec) //nolint:unconvert // the types differ per platform | |
| 11 | + | } | |
| @@ -0,0 +1,11 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | //go:build darwin || freebsd || netbsd | |||
| 4 | + | ||||
| 5 | + | package git | |||
| 6 | + | ||||
| 7 | + | import "syscall" | |||
| 8 | + | ||||
| 9 | + | func statMtime(sys *syscall.Stat_t) int64 { | |||
| 10 | + | return int64(sys.Mtimespec.Sec)*1e9 + int64(sys.Mtimespec.Nsec) //nolint:unconvert // the types differ per platform | |||
| 11 | + | } | |||
A stat_other.go +29 -0
| @@ -0,0 +1,29 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | //go:build !(linux || darwin || freebsd || openbsd || netbsd || dragonfly) | |
| 4 | + | ||
| 5 | + | package git | |
| 6 | + | ||
| 7 | + | import "os" | |
| 8 | + | ||
| 9 | + | // statNul is os.Stat or os.Lstat where there is no stat system call to make | |
| 10 | + | // directly. | |
| 11 | + | func statNul(nul []byte, lstat bool, st *fileStat) error { | |
| 12 | + | path := string(nul[:len(nul)-1]) | |
| 13 | + | var fi os.FileInfo | |
| 14 | + | var err error | |
| 15 | + | if lstat { | |
| 16 | + | fi, err = os.Lstat(path) | |
| 17 | + | } else { | |
| 18 | + | fi, err = os.Stat(path) | |
| 19 | + | } | |
| 20 | + | *st = fileStat{} | |
| 21 | + | if err != nil { | |
| 22 | + | if isNotExist(err) { | |
| 23 | + | return nil | |
| 24 | + | } | |
| 25 | + | return err | |
| 26 | + | } | |
| 27 | + | *st = fileStat{exists: true, isDir: fi.IsDir(), size: fi.Size(), mtime: fi.ModTime().UnixNano(), ino: fileIno(fi)} | |
| 28 | + | return nil | |
| 29 | + | } | |
| @@ -0,0 +1,29 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | //go:build !(linux || darwin || freebsd || openbsd || netbsd || dragonfly) | |||
| 4 | + | ||||
| 5 | + | package git | |||
| 6 | + | ||||
| 7 | + | import "os" | |||
| 8 | + | ||||
| 9 | + | // statNul is os.Stat or os.Lstat where there is no stat system call to make | |||
| 10 | + | // directly. | |||
| 11 | + | func statNul(nul []byte, lstat bool, st *fileStat) error { | |||
| 12 | + | path := string(nul[:len(nul)-1]) | |||
| 13 | + | var fi os.FileInfo | |||
| 14 | + | var err error | |||
| 15 | + | if lstat { | |||
| 16 | + | fi, err = os.Lstat(path) | |||
| 17 | + | } else { | |||
| 18 | + | fi, err = os.Stat(path) | |||
| 19 | + | } | |||
| 20 | + | *st = fileStat{} | |||
| 21 | + | if err != nil { | |||
| 22 | + | if isNotExist(err) { | |||
| 23 | + | return nil | |||
| 24 | + | } | |||
| 25 | + | return err | |||
| 26 | + | } | |||
| 27 | + | *st = fileStat{exists: true, isDir: fi.IsDir(), size: fi.Size(), mtime: fi.ModTime().UnixNano(), ino: fileIno(fi)} | |||
| 28 | + | return nil | |||
| 29 | + | } | |||
A stat_portable.go +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | //go:build (linux && !(amd64 || arm64)) || darwin || freebsd || openbsd || netbsd || dragonfly | |
| 4 | + | ||
| 5 | + | package git | |
| 6 | + | ||
| 7 | + | import "syscall" | |
| 8 | + | ||
| 9 | + | // statNul fills st with a stat system call into a Stat_t on the stack. The | |
| 10 | + | // one allocation is the NUL-free copy of the path that package syscall turns | |
| 11 | + | // into a C string at each call; stat_linux.go avoids it where the system call | |
| 12 | + | // can be made directly. | |
| 13 | + | func statNul(nul []byte, lstat bool, st *fileStat) error { | |
| 14 | + | var sys syscall.Stat_t | |
| 15 | + | path := string(nul[:len(nul)-1]) | |
| 16 | + | var err error | |
| 17 | + | for { | |
| 18 | + | if lstat { | |
| 19 | + | err = syscall.Lstat(path, &sys) | |
| 20 | + | } else { | |
| 21 | + | err = syscall.Stat(path, &sys) | |
| 22 | + | } | |
| 23 | + | if err != syscall.EINTR { | |
| 24 | + | break | |
| 25 | + | } | |
| 26 | + | } | |
| 27 | + | return fillStat(st, &sys, nul, err) | |
| 28 | + | } | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | //go:build (linux && !(amd64 || arm64)) || darwin || freebsd || openbsd || netbsd || dragonfly | |||
| 4 | + | ||||
| 5 | + | package git | |||
| 6 | + | ||||
| 7 | + | import "syscall" | |||
| 8 | + | ||||
| 9 | + | // statNul fills st with a stat system call into a Stat_t on the stack. The | |||
| 10 | + | // one allocation is the NUL-free copy of the path that package syscall turns | |||
| 11 | + | // into a C string at each call; stat_linux.go avoids it where the system call | |||
| 12 | + | // can be made directly. | |||
| 13 | + | func statNul(nul []byte, lstat bool, st *fileStat) error { | |||
| 14 | + | var sys syscall.Stat_t | |||
| 15 | + | path := string(nul[:len(nul)-1]) | |||
| 16 | + | var err error | |||
| 17 | + | for { | |||
| 18 | + | if lstat { | |||
| 19 | + | err = syscall.Lstat(path, &sys) | |||
| 20 | + | } else { | |||
| 21 | + | err = syscall.Stat(path, &sys) | |||
| 22 | + | } | |||
| 23 | + | if err != syscall.EINTR { | |||
| 24 | + | break | |||
| 25 | + | } | |||
| 26 | + | } | |||
| 27 | + | return fillStat(st, &sys, nul, err) | |||
| 28 | + | } | |||
A stat_unix.go +30 -0
| @@ -0,0 +1,30 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | //go:build linux || darwin || freebsd || openbsd || netbsd || dragonfly | |
| 4 | + | ||
| 5 | + | package git | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "os" | |
| 9 | + | "syscall" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // fillStat converts the result of a stat system call into st. An absent path | |
| 13 | + | // is not an error. | |
| 14 | + | func fillStat(st *fileStat, sys *syscall.Stat_t, nul []byte, err error) error { | |
| 15 | + | *st = fileStat{} | |
| 16 | + | if err != nil { | |
| 17 | + | if err == syscall.ENOENT || err == syscall.ENOTDIR { | |
| 18 | + | return nil | |
| 19 | + | } | |
| 20 | + | return &os.PathError{Op: "stat", Path: string(nul[:len(nul)-1]), Err: err} | |
| 21 | + | } | |
| 22 | + | *st = fileStat{ | |
| 23 | + | exists: true, | |
| 24 | + | isDir: sys.Mode&syscall.S_IFMT == syscall.S_IFDIR, | |
| 25 | + | size: sys.Size, | |
| 26 | + | mtime: statMtime(sys), | |
| 27 | + | ino: uint64(sys.Ino), //nolint:unconvert // the type differs per OS | |
| 28 | + | } | |
| 29 | + | return nil | |
| 30 | + | } | |
| @@ -0,0 +1,30 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | //go:build linux || darwin || freebsd || openbsd || netbsd || dragonfly | |||
| 4 | + | ||||
| 5 | + | package git | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "os" | |||
| 9 | + | "syscall" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // fillStat converts the result of a stat system call into st. An absent path | |||
| 13 | + | // is not an error. | |||
| 14 | + | func fillStat(st *fileStat, sys *syscall.Stat_t, nul []byte, err error) error { | |||
| 15 | + | *st = fileStat{} | |||
| 16 | + | if err != nil { | |||
| 17 | + | if err == syscall.ENOENT || err == syscall.ENOTDIR { | |||
| 18 | + | return nil | |||
| 19 | + | } | |||
| 20 | + | return &os.PathError{Op: "stat", Path: string(nul[:len(nul)-1]), Err: err} | |||
| 21 | + | } | |||
| 22 | + | *st = fileStat{ | |||
| 23 | + | exists: true, | |||
| 24 | + | isDir: sys.Mode&syscall.S_IFMT == syscall.S_IFDIR, | |||
| 25 | + | size: sys.Size, | |||
| 26 | + | mtime: statMtime(sys), | |||
| 27 | + | ino: uint64(sys.Ino), //nolint:unconvert // the type differs per OS | |||
| 28 | + | } | |||
| 29 | + | return nil | |||
| 30 | + | } | |||
M walk.go +2 -3
| @@ -506,16 +506,15 @@ func (r *Repo) refreshGraph(force bool) error { | |||
| 506 | 506 | if cur != nil && cur.err == ErrClosed { | |
| 507 | 507 | return ErrClosed | |
| 508 | 508 | } | |
| 509 | - | objects := r.dir + "/objects" | |
| 510 | 509 | // Take the time before the stat data and the content, so that a write | |
| 511 | 510 | // that follows is never mistaken for the version that was loaded. | |
| 512 | 511 | at := r.now().UnixNano() | |
| 513 | - | stamp := statGraph(objects) | |
| 512 | + | stamp := r.paths.graph.stamp() | |
| 514 | 513 | if cur != nil && !force && cur.stamp == stamp && cur.stamp.trusted(cur.at) { | |
| 515 | 514 | return nil | |
| 516 | 515 | } | |
| 517 | 516 | st := &graphState{stamp: stamp, at: at} | |
| 518 | - | g, err := openCommitGraph(objects) | |
| 517 | + | g, err := openCommitGraph(r.dir + "/objects") | |
| 519 | 518 | switch { | |
| 520 | 519 | case err == nil: | |
| 521 | 520 | st.g = g | |
| @@ -506,16 +506,15 @@ func (r *Repo) refreshGraph(force bool) error { | |||||
| 506 | if cur != nil && cur.err == ErrClosed { | 506 | if cur != nil && cur.err == ErrClosed { | ||
| 507 | return ErrClosed | 507 | return ErrClosed | ||
| 508 | } | 508 | } | ||
| 509 | - | objects := r.dir + "/objects" | |||
| 510 | // Take the time before the stat data and the content, so that a write | 509 | // Take the time before the stat data and the content, so that a write | ||
| 511 | // that follows is never mistaken for the version that was loaded. | 510 | // that follows is never mistaken for the version that was loaded. | ||
| 512 | at := r.now().UnixNano() | 511 | at := r.now().UnixNano() | ||
| 513 | - | stamp := statGraph(objects) | 512 | + | stamp := r.paths.graph.stamp() |
| 514 | if cur != nil && !force && cur.stamp == stamp && cur.stamp.trusted(cur.at) { | 513 | if cur != nil && !force && cur.stamp == stamp && cur.stamp.trusted(cur.at) { | ||
| 515 | return nil | 514 | return nil | ||
| 516 | } | 515 | } | ||
| 517 | st := &graphState{stamp: stamp, at: at} | 516 | st := &graphState{stamp: stamp, at: at} | ||
| 518 | - | g, err := openCommitGraph(objects) | 517 | + | g, err := openCommitGraph(r.dir + "/objects") |
| 519 | switch { | 518 | switch { | ||
| 520 | case err == nil: | 519 | case err == nil: | ||
| 521 | st.g = g | 520 | st.g = g | ||