perf(refs): add LookupRefID, a ref lookup that allocates nothing on a miss (GO-005)
ResolveRef builds a notFound error with fmt.Errorf on each miss, and a revision lookup tries several candidates. LookupRefID returns (id, ok, err) with no error for a ref that does not exist. The loose ref is opened with a plain system call on a path built in a stack buffer, an absent file is a bare errno, and the content is parsed on the bytes: a miss, a loose ref and a packed ref allocate nothing. ResolveRef and the other lookups share the cheaper read.
- commit
- bcb6fbd903f90141a70acbd00ad7fe16b81196d7
- author
- brice · · 2026-10-11 00:18 UTC
- parent
- 3d0ad4f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
10 files · +258 -24
M refs.go +40 -22
| @@ -3,6 +3,7 @@ | |||
| 3 | 3 | package git | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | + | "bytes" | |
| 6 | 7 | "errors" | |
| 7 | 8 | "fmt" | |
| 8 | 9 | "io" | |
| @@ -227,28 +228,32 @@ type looseRef struct { | |||
| 227 | 228 | // readLooseRef reads a loose ref. found is false when there is no such file | |
| 228 | 229 | // (or it is a directory). | |
| 229 | 230 | func (r *Repo) readLooseRef(name string) (lr looseRef, found bool, err error) { | |
| 230 | - | f, err := os.Open(r.dir + "/" + name) | |
| 231 | + | // The path goes through a stack buffer and the file is opened with a | |
| 232 | + | // plain system call: a ref that does not exist, which is what most | |
| 233 | + | // candidates of a revision lookup are, allocates nothing. | |
| 234 | + | var arr [256]byte | |
| 235 | + | nul := append(arr[:0], r.dir...) | |
| 236 | + | nul = append(nul, '/') | |
| 237 | + | nul = append(nul, name...) | |
| 238 | + | nul = append(nul, 0) | |
| 239 | + | file, err := openNul(nul) | |
| 231 | 240 | if err != nil { | |
| 232 | - | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { | |
| 241 | + | if isNotExistErrno(err) { | |
| 233 | 242 | return lr, false, nil | |
| 234 | 243 | } | |
| 235 | - | return lr, false, err | |
| 244 | + | return lr, false, openError(nul, err) | |
| 236 | 245 | } | |
| 237 | - | defer func() { _ = f.Close() }() | |
| 246 | + | defer file.close() | |
| 238 | 247 | pb := getBuf() | |
| 239 | 248 | defer putBuf(pb) | |
| 240 | 249 | pb.b = grow(pb.b, maxRefFile+1) | |
| 241 | 250 | buf := pb.b | |
| 242 | - | n, err := io.ReadFull(f, buf) | |
| 243 | - | if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) { | |
| 244 | - | err = nil | |
| 245 | - | } | |
| 251 | + | n, dir, err := file.readFull(buf) | |
| 246 | 252 | if err != nil { | |
| 247 | - | var pe *fs.PathError | |
| 248 | - | if errors.As(err, &pe) && errors.Is(pe.Err, syscall.EISDIR) { | |
| 249 | - | return lr, false, nil | |
| 250 | - | } | |
| 251 | - | return lr, false, err | |
| 253 | + | return lr, false, &fs.PathError{Op: "read", Path: r.dir + "/" + name, Err: err} | |
| 254 | + | } | |
| 255 | + | if dir { | |
| 256 | + | return lr, false, nil | |
| 252 | 257 | } | |
| 253 | 258 | if n > maxRefFile { | |
| 254 | 259 | return lr, true, corrupt("ref %s is larger than %d bytes", name, maxRefFile) | |
| @@ -261,20 +266,21 @@ func (r *Repo) readLooseRef(name string) (lr looseRef, found bool, err error) { | |||
| 261 | 266 | } | |
| 262 | 267 | ||
| 263 | 268 | func parseRefContent(b []byte) (looseRef, error) { | |
| 264 | - | s := strings.TrimRight(string(b), " \t\r\n") | |
| 265 | - | if target, ok := strings.CutPrefix(s, "ref:"); ok { | |
| 266 | - | target = strings.TrimSpace(target) | |
| 267 | - | if !validRef(target) { | |
| 268 | - | return looseRef{}, corrupt("bad symbolic ref target %q", truncate(target)) | |
| 269 | + | // On bytes: an id ref, the common case, allocates nothing. | |
| 270 | + | b = bytes.TrimRight(b, " \t\r\n") | |
| 271 | + | if target, ok := bytes.CutPrefix(b, []byte("ref:")); ok { | |
| 272 | + | t := string(bytes.TrimSpace(target)) | |
| 273 | + | if !validRef(t) { | |
| 274 | + | return looseRef{}, corrupt("bad symbolic ref target %q", truncate(t)) | |
| 269 | 275 | } | |
| 270 | - | return looseRef{symref: target}, nil | |
| 276 | + | return looseRef{symref: t}, nil | |
| 271 | 277 | } | |
| 272 | - | if len(s) >= 40 { | |
| 273 | - | if id, ok := parseIDBytes([]byte(s[:40])); ok && (len(s) == 40 || s[40] == ' ' || s[40] == '\t') { | |
| 278 | + | if len(b) >= 40 { | |
| 279 | + | if id, ok := parseIDBytes(b[:40]); ok && (len(b) == 40 || b[40] == ' ' || b[40] == '\t') { | |
| 274 | 280 | return looseRef{id: id}, nil | |
| 275 | 281 | } | |
| 276 | 282 | } | |
| 277 | - | return looseRef{}, corrupt("bad ref content %q", truncate(s)) | |
| 283 | + | return looseRef{}, corrupt("bad ref content %q", truncate(string(b))) | |
| 278 | 284 | } | |
| 279 | 285 | ||
| 280 | 286 | // validRootRef reports whether name is a pseudo-ref of the Git directory such | |
| @@ -405,6 +411,18 @@ func (r *Repo) ResolveRef(name string) (ObjectID, error) { | |||
| 405 | 411 | return ref.ID, nil | |
| 406 | 412 | } | |
| 407 | 413 | ||
| 414 | + | // LookupRefID is like ResolveRef but a ref that does not exist is not an | |
| 415 | + | // error: ok is false, and nothing is allocated, which makes it the call for | |
| 416 | + | // trying several candidate names. err is for what went wrong while looking | |
| 417 | + | // (a closed Repo, an unreadable or corrupt ref). | |
| 418 | + | func (r *Repo) LookupRefID(name string) (id ObjectID, ok bool, err error) { | |
| 419 | + | ref, ok, err := r.lookupRef(name) | |
| 420 | + | if err != nil || !ok { | |
| 421 | + | return ObjectID{}, false, err | |
| 422 | + | } | |
| 423 | + | return ref.ID, true, nil | |
| 424 | + | } | |
| 425 | + | ||
| 408 | 426 | // LookupRef is like ResolveRef but returns the whole Ref. | |
| 409 | 427 | func (r *Repo) LookupRef(name string) (Ref, error) { | |
| 410 | 428 | ref, ok, err := r.lookupRef(name) | |
| @@ -3,6 +3,7 @@ | |||||
| 3 | package git | 3 | package git | ||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | + | "bytes" | |||
| 6 | "errors" | 7 | "errors" | ||
| 7 | "fmt" | 8 | "fmt" | ||
| 8 | "io" | 9 | "io" | ||
| @@ -227,28 +228,32 @@ type looseRef struct { | |||||
| 227 | // readLooseRef reads a loose ref. found is false when there is no such file | 228 | // readLooseRef reads a loose ref. found is false when there is no such file | ||
| 228 | // (or it is a directory). | 229 | // (or it is a directory). | ||
| 229 | func (r *Repo) readLooseRef(name string) (lr looseRef, found bool, err error) { | 230 | func (r *Repo) readLooseRef(name string) (lr looseRef, found bool, err error) { | ||
| 230 | - | f, err := os.Open(r.dir + "/" + name) | 231 | + | // The path goes through a stack buffer and the file is opened with a |
| 232 | + | // plain system call: a ref that does not exist, which is what most | |||
| 233 | + | // candidates of a revision lookup are, allocates nothing. | |||
| 234 | + | var arr [256]byte | |||
| 235 | + | nul := append(arr[:0], r.dir...) | |||
| 236 | + | nul = append(nul, '/') | |||
| 237 | + | nul = append(nul, name...) | |||
| 238 | + | nul = append(nul, 0) | |||
| 239 | + | file, err := openNul(nul) | |||
| 231 | if err != nil { | 240 | if err != nil { | ||
| 232 | - | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { | 241 | + | if isNotExistErrno(err) { |
| 233 | return lr, false, nil | 242 | return lr, false, nil | ||
| 234 | } | 243 | } | ||
| 235 | - | return lr, false, err | 244 | + | return lr, false, openError(nul, err) |
| 236 | } | 245 | } | ||
| 237 | - | defer func() { _ = f.Close() }() | 246 | + | defer file.close() |
| 238 | pb := getBuf() | 247 | pb := getBuf() | ||
| 239 | defer putBuf(pb) | 248 | defer putBuf(pb) | ||
| 240 | pb.b = grow(pb.b, maxRefFile+1) | 249 | pb.b = grow(pb.b, maxRefFile+1) | ||
| 241 | buf := pb.b | 250 | buf := pb.b | ||
| 242 | - | n, err := io.ReadFull(f, buf) | 251 | + | n, dir, err := file.readFull(buf) |
| 243 | - | if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) { | |||
| 244 | - | err = nil | |||
| 245 | - | } | |||
| 246 | if err != nil { | 252 | if err != nil { | ||
| 247 | - | var pe *fs.PathError | 253 | + | return lr, false, &fs.PathError{Op: "read", Path: r.dir + "/" + name, Err: err} |
| 248 | - | if errors.As(err, &pe) && errors.Is(pe.Err, syscall.EISDIR) { | 254 | + | } |
| 249 | - | return lr, false, nil | 255 | + | if dir { |
| 250 | - | } | 256 | + | return lr, false, nil |
| 251 | - | return lr, false, err | |||
| 252 | } | 257 | } | ||
| 253 | if n > maxRefFile { | 258 | if n > maxRefFile { | ||
| 254 | return lr, true, corrupt("ref %s is larger than %d bytes", name, maxRefFile) | 259 | return lr, true, corrupt("ref %s is larger than %d bytes", name, maxRefFile) | ||
| @@ -261,20 +266,21 @@ func (r *Repo) readLooseRef(name string) (lr looseRef, found bool, err error) { | |||||
| 261 | } | 266 | } | ||
| 262 | 267 | ||||
| 263 | func parseRefContent(b []byte) (looseRef, error) { | 268 | func parseRefContent(b []byte) (looseRef, error) { | ||
| 264 | - | s := strings.TrimRight(string(b), " \t\r\n") | 269 | + | // On bytes: an id ref, the common case, allocates nothing. |
| 265 | - | if target, ok := strings.CutPrefix(s, "ref:"); ok { | 270 | + | b = bytes.TrimRight(b, " \t\r\n") |
| 266 | - | target = strings.TrimSpace(target) | 271 | + | if target, ok := bytes.CutPrefix(b, []byte("ref:")); ok { |
| 267 | - | if !validRef(target) { | 272 | + | t := string(bytes.TrimSpace(target)) |
| 268 | - | return looseRef{}, corrupt("bad symbolic ref target %q", truncate(target)) | 273 | + | if !validRef(t) { |
| 274 | + | return looseRef{}, corrupt("bad symbolic ref target %q", truncate(t)) | |||
| 269 | } | 275 | } | ||
| 270 | - | return looseRef{symref: target}, nil | 276 | + | return looseRef{symref: t}, nil |
| 271 | } | 277 | } | ||
| 272 | - | if len(s) >= 40 { | 278 | + | if len(b) >= 40 { |
| 273 | - | if id, ok := parseIDBytes([]byte(s[:40])); ok && (len(s) == 40 || s[40] == ' ' || s[40] == '\t') { | 279 | + | if id, ok := parseIDBytes(b[:40]); ok && (len(b) == 40 || b[40] == ' ' || b[40] == '\t') { |
| 274 | return looseRef{id: id}, nil | 280 | return looseRef{id: id}, nil | ||
| 275 | } | 281 | } | ||
| 276 | } | 282 | } | ||
| 277 | - | return looseRef{}, corrupt("bad ref content %q", truncate(s)) | 283 | + | return looseRef{}, corrupt("bad ref content %q", truncate(string(b))) |
| 278 | } | 284 | } | ||
| 279 | 285 | ||||
| 280 | // validRootRef reports whether name is a pseudo-ref of the Git directory such | 286 | // validRootRef reports whether name is a pseudo-ref of the Git directory such | ||
| @@ -405,6 +411,18 @@ func (r *Repo) ResolveRef(name string) (ObjectID, error) { | |||||
| 405 | return ref.ID, nil | 411 | return ref.ID, nil | ||
| 406 | } | 412 | } | ||
| 407 | 413 | ||||
| 414 | + | // LookupRefID is like ResolveRef but a ref that does not exist is not an | |||
| 415 | + | // error: ok is false, and nothing is allocated, which makes it the call for | |||
| 416 | + | // trying several candidate names. err is for what went wrong while looking | |||
| 417 | + | // (a closed Repo, an unreadable or corrupt ref). | |||
| 418 | + | func (r *Repo) LookupRefID(name string) (id ObjectID, ok bool, err error) { | |||
| 419 | + | ref, ok, err := r.lookupRef(name) | |||
| 420 | + | if err != nil || !ok { | |||
| 421 | + | return ObjectID{}, false, err | |||
| 422 | + | } | |||
| 423 | + | return ref.ID, true, nil | |||
| 424 | + | } | |||
| 425 | + | ||||
| 408 | // LookupRef is like ResolveRef but returns the whole Ref. | 426 | // LookupRef is like ResolveRef but returns the whole Ref. | ||
| 409 | func (r *Repo) LookupRef(name string) (Ref, error) { | 427 | func (r *Repo) LookupRef(name string) (Ref, error) { | ||
| 410 | ref, ok, err := r.lookupRef(name) | 428 | ref, ok, err := r.lookupRef(name) | ||
A refs_bench_test.go +33 -0
| @@ -0,0 +1,33 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package git | |
| 4 | + | ||
| 5 | + | import "testing" | |
| 6 | + | ||
| 7 | + | // BenchmarkResolveRefMiss is what a revision lookup costs for the candidates | |
| 8 | + | // that do not exist: the adapter tries heads, then tags, then the name as | |
| 9 | + | // written. | |
| 10 | + | func BenchmarkResolveRefMiss(b *testing.B) { | |
| 11 | + | fx := getBench(b) | |
| 12 | + | names := []string{"refs/heads/no-such-branch", "refs/tags/no-such-tag", "refs/remotes/origin/no-such-branch"} | |
| 13 | + | b.Run("LookupRefID", func(b *testing.B) { | |
| 14 | + | b.ReportAllocs() | |
| 15 | + | i := 0 | |
| 16 | + | for b.Loop() { | |
| 17 | + | if _, ok, err := fx.r.LookupRefID(names[i%len(names)]); err != nil || ok { | |
| 18 | + | b.Fatal("found or failed", err) | |
| 19 | + | } | |
| 20 | + | i++ | |
| 21 | + | } | |
| 22 | + | }) | |
| 23 | + | b.Run("ResolveRef", func(b *testing.B) { | |
| 24 | + | b.ReportAllocs() | |
| 25 | + | i := 0 | |
| 26 | + | for b.Loop() { | |
| 27 | + | if _, err := fx.r.ResolveRef(names[i%len(names)]); err == nil { | |
| 28 | + | b.Fatal("found") | |
| 29 | + | } | |
| 30 | + | i++ | |
| 31 | + | } | |
| 32 | + | }) | |
| 33 | + | } | |
| @@ -0,0 +1,33 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package git | |||
| 4 | + | ||||
| 5 | + | import "testing" | |||
| 6 | + | ||||
| 7 | + | // BenchmarkResolveRefMiss is what a revision lookup costs for the candidates | |||
| 8 | + | // that do not exist: the adapter tries heads, then tags, then the name as | |||
| 9 | + | // written. | |||
| 10 | + | func BenchmarkResolveRefMiss(b *testing.B) { | |||
| 11 | + | fx := getBench(b) | |||
| 12 | + | names := []string{"refs/heads/no-such-branch", "refs/tags/no-such-tag", "refs/remotes/origin/no-such-branch"} | |||
| 13 | + | b.Run("LookupRefID", func(b *testing.B) { | |||
| 14 | + | b.ReportAllocs() | |||
| 15 | + | i := 0 | |||
| 16 | + | for b.Loop() { | |||
| 17 | + | if _, ok, err := fx.r.LookupRefID(names[i%len(names)]); err != nil || ok { | |||
| 18 | + | b.Fatal("found or failed", err) | |||
| 19 | + | } | |||
| 20 | + | i++ | |||
| 21 | + | } | |||
| 22 | + | }) | |||
| 23 | + | b.Run("ResolveRef", func(b *testing.B) { | |||
| 24 | + | b.ReportAllocs() | |||
| 25 | + | i := 0 | |||
| 26 | + | for b.Loop() { | |||
| 27 | + | if _, err := fx.r.ResolveRef(names[i%len(names)]); err == nil { | |||
| 28 | + | b.Fatal("found") | |||
| 29 | + | } | |||
| 30 | + | i++ | |||
| 31 | + | } | |||
| 32 | + | }) | |||
| 33 | + | } | |||
M refs_fix_test.go +59 -0
| @@ -4,7 +4,9 @@ package git | |||
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 6 | "errors" | |
| 7 | + | "os" | |
| 7 | 8 | "testing" | |
| 9 | + | "time" | |
| 8 | 10 | ) | |
| 9 | 11 | ||
| 10 | 12 | func TestCorruptRootRefIsNotFound(t *testing.T) { | |
| @@ -82,3 +84,60 @@ func TestClosedRepoRefs(t *testing.T) { | |||
| 82 | 84 | t.Errorf("Refs yielded %d times, want one error", n) | |
| 83 | 85 | } | |
| 84 | 86 | } | |
| 87 | + | ||
| 88 | + | func TestLookupRefID(t *testing.T) { | |
| 89 | + | f := newFx(t) | |
| 90 | + | f.write("a", []byte("a"), 0o644) | |
| 91 | + | head := mustID(t, f.commit("one")) | |
| 92 | + | f.git("tag", "light") | |
| 93 | + | f.git("update-ref", "refs/heads/packed", head.String()) | |
| 94 | + | f.git("pack-refs", "--all") | |
| 95 | + | f.git("update-ref", "refs/heads/loose", head.String()) | |
| 96 | + | f.git("symbolic-ref", "refs/heads/sym", "refs/heads/loose") | |
| 97 | + | // packed-refs written just now is inside the racy window and is parsed | |
| 98 | + | // again at each lookup; the steady state is the one to measure. | |
| 99 | + | old := time.Now().Add(-time.Hour) | |
| 100 | + | if err := os.Chtimes(f.dir+"/packed-refs", old, old); err != nil { | |
| 101 | + | t.Fatal(err) | |
| 102 | + | } | |
| 103 | + | r := f.open() | |
| 104 | + | for _, name := range []string{"HEAD", "refs/heads/loose", "refs/heads/packed", "refs/tags/light", "refs/heads/sym"} { | |
| 105 | + | id, ok, err := r.LookupRefID(name) | |
| 106 | + | if err != nil || !ok || id != head { | |
| 107 | + | t.Errorf("LookupRefID(%q) = %v, %v, %v", name, id, ok, err) | |
| 108 | + | } | |
| 109 | + | if want, err := r.ResolveRef(name); err != nil || want != id { | |
| 110 | + | t.Errorf("ResolveRef(%q) = %v, %v", name, want, err) | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | // A miss is not an error, whatever the reason the name does not exist: | |
| 114 | + | // no such file, a directory in its place, a file where a directory is | |
| 115 | + | // expected, an invalid name, a name outside refs/. | |
| 116 | + | for _, name := range []string{"refs/heads/none", "refs/heads", "refs/heads/loose/under", "refs/tags/none", "refs/heads/a..b", "../config", "", "refs/heads/x.lock", "orig_head"} { | |
| 117 | + | id, ok, err := r.LookupRefID(name) | |
| 118 | + | if err != nil || ok || !id.IsZero() { | |
| 119 | + | t.Errorf("LookupRefID(%q) = %v, %v, %v, want a quiet miss", name, id, ok, err) | |
| 120 | + | } | |
| 121 | + | if _, err := r.ResolveRef(name); !errors.Is(err, ErrNotFound) { | |
| 122 | + | t.Errorf("ResolveRef(%q) = %v, want ErrNotFound", name, err) | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | if raceEnabled { | |
| 126 | + | return | |
| 127 | + | } | |
| 128 | + | for _, name := range []string{"refs/heads/none", "refs/tags/none", "refs/remotes/origin/none"} { | |
| 129 | + | if got := testing.AllocsPerRun(50, func() { _, _, _ = r.LookupRefID(name) }); got != 0 { | |
| 130 | + | t.Errorf("LookupRefID(%q) allocates %v on a miss, want 0", name, got) | |
| 131 | + | } | |
| 132 | + | } | |
| 133 | + | if got := testing.AllocsPerRun(50, func() { _, _, _ = r.LookupRefID("refs/heads/loose") }); got != 0 { | |
| 134 | + | t.Errorf("LookupRefID of a loose ref allocates %v, want 0", got) | |
| 135 | + | } | |
| 136 | + | if got := testing.AllocsPerRun(50, func() { _, _, _ = r.LookupRefID("refs/heads/packed") }); got != 0 { | |
| 137 | + | t.Errorf("LookupRefID of a packed ref allocates %v, want 0", got) | |
| 138 | + | } | |
| 139 | + | _ = r.Close() | |
| 140 | + | if _, _, err := r.LookupRefID("refs/heads/loose"); !errors.Is(err, ErrClosed) { | |
| 141 | + | t.Errorf("closed: %v", err) | |
| 142 | + | } | |
| 143 | + | } | |
| @@ -4,7 +4,9 @@ package git | |||||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "errors" | 6 | "errors" | ||
| 7 | + | "os" | |||
| 7 | "testing" | 8 | "testing" | ||
| 9 | + | "time" | |||
| 8 | ) | 10 | ) | ||
| 9 | 11 | ||||
| 10 | func TestCorruptRootRefIsNotFound(t *testing.T) { | 12 | func TestCorruptRootRefIsNotFound(t *testing.T) { | ||
| @@ -82,3 +84,60 @@ func TestClosedRepoRefs(t *testing.T) { | |||||
| 82 | t.Errorf("Refs yielded %d times, want one error", n) | 84 | t.Errorf("Refs yielded %d times, want one error", n) | ||
| 83 | } | 85 | } | ||
| 84 | } | 86 | } | ||
| 87 | + | ||||
| 88 | + | func TestLookupRefID(t *testing.T) { | |||
| 89 | + | f := newFx(t) | |||
| 90 | + | f.write("a", []byte("a"), 0o644) | |||
| 91 | + | head := mustID(t, f.commit("one")) | |||
| 92 | + | f.git("tag", "light") | |||
| 93 | + | f.git("update-ref", "refs/heads/packed", head.String()) | |||
| 94 | + | f.git("pack-refs", "--all") | |||
| 95 | + | f.git("update-ref", "refs/heads/loose", head.String()) | |||
| 96 | + | f.git("symbolic-ref", "refs/heads/sym", "refs/heads/loose") | |||
| 97 | + | // packed-refs written just now is inside the racy window and is parsed | |||
| 98 | + | // again at each lookup; the steady state is the one to measure. | |||
| 99 | + | old := time.Now().Add(-time.Hour) | |||
| 100 | + | if err := os.Chtimes(f.dir+"/packed-refs", old, old); err != nil { | |||
| 101 | + | t.Fatal(err) | |||
| 102 | + | } | |||
| 103 | + | r := f.open() | |||
| 104 | + | for _, name := range []string{"HEAD", "refs/heads/loose", "refs/heads/packed", "refs/tags/light", "refs/heads/sym"} { | |||
| 105 | + | id, ok, err := r.LookupRefID(name) | |||
| 106 | + | if err != nil || !ok || id != head { | |||
| 107 | + | t.Errorf("LookupRefID(%q) = %v, %v, %v", name, id, ok, err) | |||
| 108 | + | } | |||
| 109 | + | if want, err := r.ResolveRef(name); err != nil || want != id { | |||
| 110 | + | t.Errorf("ResolveRef(%q) = %v, %v", name, want, err) | |||
| 111 | + | } | |||
| 112 | + | } | |||
| 113 | + | // A miss is not an error, whatever the reason the name does not exist: | |||
| 114 | + | // no such file, a directory in its place, a file where a directory is | |||
| 115 | + | // expected, an invalid name, a name outside refs/. | |||
| 116 | + | for _, name := range []string{"refs/heads/none", "refs/heads", "refs/heads/loose/under", "refs/tags/none", "refs/heads/a..b", "../config", "", "refs/heads/x.lock", "orig_head"} { | |||
| 117 | + | id, ok, err := r.LookupRefID(name) | |||
| 118 | + | if err != nil || ok || !id.IsZero() { | |||
| 119 | + | t.Errorf("LookupRefID(%q) = %v, %v, %v, want a quiet miss", name, id, ok, err) | |||
| 120 | + | } | |||
| 121 | + | if _, err := r.ResolveRef(name); !errors.Is(err, ErrNotFound) { | |||
| 122 | + | t.Errorf("ResolveRef(%q) = %v, want ErrNotFound", name, err) | |||
| 123 | + | } | |||
| 124 | + | } | |||
| 125 | + | if raceEnabled { | |||
| 126 | + | return | |||
| 127 | + | } | |||
| 128 | + | for _, name := range []string{"refs/heads/none", "refs/tags/none", "refs/remotes/origin/none"} { | |||
| 129 | + | if got := testing.AllocsPerRun(50, func() { _, _, _ = r.LookupRefID(name) }); got != 0 { | |||
| 130 | + | t.Errorf("LookupRefID(%q) allocates %v on a miss, want 0", name, got) | |||
| 131 | + | } | |||
| 132 | + | } | |||
| 133 | + | if got := testing.AllocsPerRun(50, func() { _, _, _ = r.LookupRefID("refs/heads/loose") }); got != 0 { | |||
| 134 | + | t.Errorf("LookupRefID of a loose ref allocates %v, want 0", got) | |||
| 135 | + | } | |||
| 136 | + | if got := testing.AllocsPerRun(50, func() { _, _, _ = r.LookupRefID("refs/heads/packed") }); got != 0 { | |||
| 137 | + | t.Errorf("LookupRefID of a packed ref allocates %v, want 0", got) | |||
| 138 | + | } | |||
| 139 | + | _ = r.Close() | |||
| 140 | + | if _, _, err := r.LookupRefID("refs/heads/loose"); !errors.Is(err, ErrClosed) { | |||
| 141 | + | t.Errorf("closed: %v", err) | |||
| 142 | + | } | |||
| 143 | + | } | |||
M stat.go +26 -1
| @@ -2,7 +2,12 @@ | |||
| 2 | 2 | ||
| 3 | 3 | package git | |
| 4 | 4 | ||
| 5 | - | import "time" | |
| 5 | + | import ( | |
| 6 | + | "errors" | |
| 7 | + | "io" | |
| 8 | + | "syscall" | |
| 9 | + | "time" | |
| 10 | + | ) | |
| 6 | 11 | ||
| 7 | 12 | // fileStat is the part of a stat result that change detection uses. | |
| 8 | 13 | type fileStat struct { | |
| @@ -48,3 +53,23 @@ func (p *statPath) sig() (fileSig, error) { | |||
| 48 | 53 | } | |
| 49 | 54 | return fileSig{exists: true, size: st.size, mtime: st.mtime, ino: st.ino, read: read}, nil | |
| 50 | 55 | } | |
| 56 | + | ||
| 57 | + | // readFull reads the file into buf until buf is full or the file ends, and | |
| 58 | + | // returns the number of bytes. A directory reads as an empty, missing file: | |
| 59 | + | // dir is true. | |
| 60 | + | func (f rawFile) readFull(buf []byte) (n int, dir bool, err error) { | |
| 61 | + | for n < len(buf) { | |
| 62 | + | m, err := f.read(buf[n:]) | |
| 63 | + | n += m | |
| 64 | + | if err != nil { | |
| 65 | + | if errors.Is(err, io.EOF) { | |
| 66 | + | break | |
| 67 | + | } | |
| 68 | + | if errors.Is(err, syscall.EISDIR) { | |
| 69 | + | return 0, true, nil | |
| 70 | + | } | |
| 71 | + | return n, false, err | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | return n, false, nil | |
| 75 | + | } | |
| @@ -2,7 +2,12 @@ | |||||
| 2 | 2 | ||||
| 3 | package git | 3 | package git | ||
| 4 | 4 | ||||
| 5 | - | import "time" | 5 | + | import ( |
| 6 | + | "errors" | |||
| 7 | + | "io" | |||
| 8 | + | "syscall" | |||
| 9 | + | "time" | |||
| 10 | + | ) | |||
| 6 | 11 | ||||
| 7 | // fileStat is the part of a stat result that change detection uses. | 12 | // fileStat is the part of a stat result that change detection uses. | ||
| 8 | type fileStat struct { | 13 | type fileStat struct { | ||
| @@ -48,3 +53,23 @@ func (p *statPath) sig() (fileSig, error) { | |||||
| 48 | } | 53 | } | ||
| 49 | return fileSig{exists: true, size: st.size, mtime: st.mtime, ino: st.ino, read: read}, nil | 54 | return fileSig{exists: true, size: st.size, mtime: st.mtime, ino: st.ino, read: read}, nil | ||
| 50 | } | 55 | } | ||
| 56 | + | ||||
| 57 | + | // readFull reads the file into buf until buf is full or the file ends, and | |||
| 58 | + | // returns the number of bytes. A directory reads as an empty, missing file: | |||
| 59 | + | // dir is true. | |||
| 60 | + | func (f rawFile) readFull(buf []byte) (n int, dir bool, err error) { | |||
| 61 | + | for n < len(buf) { | |||
| 62 | + | m, err := f.read(buf[n:]) | |||
| 63 | + | n += m | |||
| 64 | + | if err != nil { | |||
| 65 | + | if errors.Is(err, io.EOF) { | |||
| 66 | + | break | |||
| 67 | + | } | |||
| 68 | + | if errors.Is(err, syscall.EISDIR) { | |||
| 69 | + | return 0, true, nil | |||
| 70 | + | } | |||
| 71 | + | return n, false, err | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | return n, false, nil | |||
| 75 | + | } | |||
M stat_linux.go +21 -0
| @@ -35,3 +35,24 @@ func statNul(nul []byte, lstat bool, st *fileStat) error { | |||
| 35 | 35 | } | |
| 36 | 36 | return fillStat(st, &sys, nul, err) | |
| 37 | 37 | } | |
| 38 | + | ||
| 39 | + | const ( | |
| 40 | + | oRdonly = 0 | |
| 41 | + | oCloexec = 0x80000 | |
| 42 | + | ) | |
| 43 | + | ||
| 44 | + | // openNul opens the file read-only with openat(AT_FDCWD, path, flags), the | |
| 45 | + | // system call syscall.Open makes, from a path that is already NUL-terminated. | |
| 46 | + | func openNul(nul []byte) (rawFile, error) { | |
| 47 | + | atFDCWD := -100 | |
| 48 | + | for { | |
| 49 | + | fd, _, errno := syscall.Syscall6(sysOpenat, uintptr(atFDCWD), uintptr(unsafe.Pointer(&nul[0])), oRdonly|oCloexec, 0, 0, 0) | |
| 50 | + | if errno == syscall.EINTR { | |
| 51 | + | continue | |
| 52 | + | } | |
| 53 | + | if errno != 0 { | |
| 54 | + | return rawFile{}, errno | |
| 55 | + | } | |
| 56 | + | return rawFile{int(fd)}, nil | |
| 57 | + | } | |
| 58 | + | } | |
| @@ -35,3 +35,24 @@ func statNul(nul []byte, lstat bool, st *fileStat) error { | |||||
| 35 | } | 35 | } | ||
| 36 | return fillStat(st, &sys, nul, err) | 36 | return fillStat(st, &sys, nul, err) | ||
| 37 | } | 37 | } | ||
| 38 | + | ||||
| 39 | + | const ( | |||
| 40 | + | oRdonly = 0 | |||
| 41 | + | oCloexec = 0x80000 | |||
| 42 | + | ) | |||
| 43 | + | ||||
| 44 | + | // openNul opens the file read-only with openat(AT_FDCWD, path, flags), the | |||
| 45 | + | // system call syscall.Open makes, from a path that is already NUL-terminated. | |||
| 46 | + | func openNul(nul []byte) (rawFile, error) { | |||
| 47 | + | atFDCWD := -100 | |||
| 48 | + | for { | |||
| 49 | + | fd, _, errno := syscall.Syscall6(sysOpenat, uintptr(atFDCWD), uintptr(unsafe.Pointer(&nul[0])), oRdonly|oCloexec, 0, 0, 0) | |||
| 50 | + | if errno == syscall.EINTR { | |||
| 51 | + | continue | |||
| 52 | + | } | |||
| 53 | + | if errno != 0 { | |||
| 54 | + | return rawFile{}, errno | |||
| 55 | + | } | |||
| 56 | + | return rawFile{int(fd)}, nil | |||
| 57 | + | } | |||
| 58 | + | } | |||
M stat_linux_amd64.go +2 -0
| @@ -7,3 +7,5 @@ package git | |||
| 7 | 7 | import "syscall" | |
| 8 | 8 | ||
| 9 | 9 | const sysFstatat = syscall.SYS_NEWFSTATAT | |
| 10 | + | ||
| 11 | + | const sysOpenat = syscall.SYS_OPENAT | |
| @@ -7,3 +7,5 @@ package git | |||||
| 7 | import "syscall" | 7 | import "syscall" | ||
| 8 | 8 | ||||
| 9 | const sysFstatat = syscall.SYS_NEWFSTATAT | 9 | const sysFstatat = syscall.SYS_NEWFSTATAT | ||
| 10 | + | ||||
| 11 | + | const sysOpenat = syscall.SYS_OPENAT | |||
M stat_linux_arm64.go +2 -0
| @@ -7,3 +7,5 @@ package git | |||
| 7 | 7 | import "syscall" | |
| 8 | 8 | ||
| 9 | 9 | const sysFstatat = syscall.SYS_FSTATAT | |
| 10 | + | ||
| 11 | + | const sysOpenat = syscall.SYS_OPENAT | |
| @@ -7,3 +7,5 @@ package git | |||||
| 7 | import "syscall" | 7 | import "syscall" | ||
| 8 | 8 | ||||
| 9 | const sysFstatat = syscall.SYS_FSTATAT | 9 | const sysFstatat = syscall.SYS_FSTATAT | ||
| 10 | + | ||||
| 11 | + | const sysOpenat = syscall.SYS_OPENAT | |||
M stat_other.go +28 -1
| @@ -4,7 +4,11 @@ | |||
| 4 | 4 | ||
| 5 | 5 | package git | |
| 6 | 6 | ||
| 7 | - | import "os" | |
| 7 | + | import ( | |
| 8 | + | "errors" | |
| 9 | + | "io" | |
| 10 | + | "os" | |
| 11 | + | ) | |
| 8 | 12 | ||
| 9 | 13 | // statNul is os.Stat or os.Lstat where there is no stat system call to make | |
| 10 | 14 | // directly. | |
| @@ -27,3 +31,26 @@ func statNul(nul []byte, lstat bool, st *fileStat) error { | |||
| 27 | 31 | *st = fileStat{exists: true, isDir: fi.IsDir(), size: fi.Size(), mtime: fi.ModTime().UnixNano(), ino: fileIno(fi)} | |
| 28 | 32 | return nil | |
| 29 | 33 | } | |
| 34 | + | ||
| 35 | + | // rawFile is an open file. | |
| 36 | + | type rawFile struct{ f *os.File } | |
| 37 | + | ||
| 38 | + | func openNul(nul []byte) (rawFile, error) { | |
| 39 | + | f, err := os.Open(string(nul[:len(nul)-1])) | |
| 40 | + | return rawFile{f}, err | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // read returns io.EOF at the end of the file. | |
| 44 | + | func (f rawFile) read(p []byte) (int, error) { | |
| 45 | + | n, err := f.f.Read(p) | |
| 46 | + | if n > 0 && errors.Is(err, io.EOF) { | |
| 47 | + | err = nil | |
| 48 | + | } | |
| 49 | + | return n, err | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func (f rawFile) close() { _ = f.f.Close() } | |
| 53 | + | ||
| 54 | + | func isNotExistErrno(err error) bool { return isNotExist(err) } | |
| 55 | + | ||
| 56 | + | func openError(_ []byte, err error) error { return err } | |
| @@ -4,7 +4,11 @@ | |||||
| 4 | 4 | ||||
| 5 | package git | 5 | package git | ||
| 6 | 6 | ||||
| 7 | - | import "os" | 7 | + | import ( |
| 8 | + | "errors" | |||
| 9 | + | "io" | |||
| 10 | + | "os" | |||
| 11 | + | ) | |||
| 8 | 12 | ||||
| 9 | // statNul is os.Stat or os.Lstat where there is no stat system call to make | 13 | // statNul is os.Stat or os.Lstat where there is no stat system call to make | ||
| 10 | // directly. | 14 | // directly. | ||
| @@ -27,3 +31,26 @@ func statNul(nul []byte, lstat bool, st *fileStat) error { | |||||
| 27 | *st = fileStat{exists: true, isDir: fi.IsDir(), size: fi.Size(), mtime: fi.ModTime().UnixNano(), ino: fileIno(fi)} | 31 | *st = fileStat{exists: true, isDir: fi.IsDir(), size: fi.Size(), mtime: fi.ModTime().UnixNano(), ino: fileIno(fi)} | ||
| 28 | return nil | 32 | return nil | ||
| 29 | } | 33 | } | ||
| 34 | + | ||||
| 35 | + | // rawFile is an open file. | |||
| 36 | + | type rawFile struct{ f *os.File } | |||
| 37 | + | ||||
| 38 | + | func openNul(nul []byte) (rawFile, error) { | |||
| 39 | + | f, err := os.Open(string(nul[:len(nul)-1])) | |||
| 40 | + | return rawFile{f}, err | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // read returns io.EOF at the end of the file. | |||
| 44 | + | func (f rawFile) read(p []byte) (int, error) { | |||
| 45 | + | n, err := f.f.Read(p) | |||
| 46 | + | if n > 0 && errors.Is(err, io.EOF) { | |||
| 47 | + | err = nil | |||
| 48 | + | } | |||
| 49 | + | return n, err | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func (f rawFile) close() { _ = f.f.Close() } | |||
| 53 | + | ||||
| 54 | + | func isNotExistErrno(err error) bool { return isNotExist(err) } | |||
| 55 | + | ||||
| 56 | + | func openError(_ []byte, err error) error { return err } | |||
M stat_portable.go +16 -0
| @@ -26,3 +26,19 @@ func statNul(nul []byte, lstat bool, st *fileStat) error { | |||
| 26 | 26 | } | |
| 27 | 27 | return fillStat(st, &sys, nul, err) | |
| 28 | 28 | } | |
| 29 | + | ||
| 30 | + | // openNul opens the file read-only. It allocates the C string of the path, | |
| 31 | + | // which stat_linux.go does not. | |
| 32 | + | func openNul(nul []byte) (rawFile, error) { | |
| 33 | + | path := string(nul[:len(nul)-1]) | |
| 34 | + | for { | |
| 35 | + | fd, err := syscall.Open(path, syscall.O_RDONLY|syscall.O_CLOEXEC, 0) | |
| 36 | + | if err == syscall.EINTR { | |
| 37 | + | continue | |
| 38 | + | } | |
| 39 | + | if err != nil { | |
| 40 | + | return rawFile{}, err | |
| 41 | + | } | |
| 42 | + | return rawFile{fd}, nil | |
| 43 | + | } | |
| 44 | + | } | |
| @@ -26,3 +26,19 @@ func statNul(nul []byte, lstat bool, st *fileStat) error { | |||||
| 26 | } | 26 | } | ||
| 27 | return fillStat(st, &sys, nul, err) | 27 | return fillStat(st, &sys, nul, err) | ||
| 28 | } | 28 | } | ||
| 29 | + | ||||
| 30 | + | // openNul opens the file read-only. It allocates the C string of the path, | |||
| 31 | + | // which stat_linux.go does not. | |||
| 32 | + | func openNul(nul []byte) (rawFile, error) { | |||
| 33 | + | path := string(nul[:len(nul)-1]) | |||
| 34 | + | for { | |||
| 35 | + | fd, err := syscall.Open(path, syscall.O_RDONLY|syscall.O_CLOEXEC, 0) | |||
| 36 | + | if err == syscall.EINTR { | |||
| 37 | + | continue | |||
| 38 | + | } | |||
| 39 | + | if err != nil { | |||
| 40 | + | return rawFile{}, err | |||
| 41 | + | } | |||
| 42 | + | return rawFile{fd}, nil | |||
| 43 | + | } | |||
| 44 | + | } | |||
M stat_unix.go +31 -0
| @@ -5,6 +5,7 @@ | |||
| 5 | 5 | package git | |
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | + | "io" | |
| 8 | 9 | "os" | |
| 9 | 10 | "syscall" | |
| 10 | 11 | ) | |
| @@ -28,3 +29,33 @@ func fillStat(st *fileStat, sys *syscall.Stat_t, nul []byte, err error) error { | |||
| 28 | 29 | } | |
| 29 | 30 | return nil | |
| 30 | 31 | } | |
| 32 | + | ||
| 33 | + | // rawFile is an open file descriptor. | |
| 34 | + | type rawFile struct{ fd int } | |
| 35 | + | ||
| 36 | + | // read returns io.EOF at the end of the file. | |
| 37 | + | func (f rawFile) read(p []byte) (int, error) { | |
| 38 | + | for { | |
| 39 | + | n, err := syscall.Read(f.fd, p) | |
| 40 | + | if err == syscall.EINTR { | |
| 41 | + | continue | |
| 42 | + | } | |
| 43 | + | if err != nil { | |
| 44 | + | return 0, err | |
| 45 | + | } | |
| 46 | + | if n == 0 { | |
| 47 | + | return 0, io.EOF | |
| 48 | + | } | |
| 49 | + | return n, nil | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | func (f rawFile) close() { _ = syscall.Close(f.fd) } | |
| 54 | + | ||
| 55 | + | // isNotExistErrno reports whether err, from openNul, says that the file or | |
| 56 | + | // a directory on the way is absent. | |
| 57 | + | func isNotExistErrno(err error) bool { return err == syscall.ENOENT || err == syscall.ENOTDIR } | |
| 58 | + | ||
| 59 | + | func openError(nul []byte, err error) error { | |
| 60 | + | return &os.PathError{Op: "open", Path: string(nul[:len(nul)-1]), Err: err} | |
| 61 | + | } | |
| @@ -5,6 +5,7 @@ | |||||
| 5 | package git | 5 | package git | ||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | + | "io" | |||
| 8 | "os" | 9 | "os" | ||
| 9 | "syscall" | 10 | "syscall" | ||
| 10 | ) | 11 | ) | ||
| @@ -28,3 +29,33 @@ func fillStat(st *fileStat, sys *syscall.Stat_t, nul []byte, err error) error { | |||||
| 28 | } | 29 | } | ||
| 29 | return nil | 30 | return nil | ||
| 30 | } | 31 | } | ||
| 32 | + | ||||
| 33 | + | // rawFile is an open file descriptor. | |||
| 34 | + | type rawFile struct{ fd int } | |||
| 35 | + | ||||
| 36 | + | // read returns io.EOF at the end of the file. | |||
| 37 | + | func (f rawFile) read(p []byte) (int, error) { | |||
| 38 | + | for { | |||
| 39 | + | n, err := syscall.Read(f.fd, p) | |||
| 40 | + | if err == syscall.EINTR { | |||
| 41 | + | continue | |||
| 42 | + | } | |||
| 43 | + | if err != nil { | |||
| 44 | + | return 0, err | |||
| 45 | + | } | |||
| 46 | + | if n == 0 { | |||
| 47 | + | return 0, io.EOF | |||
| 48 | + | } | |||
| 49 | + | return n, nil | |||
| 50 | + | } | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | func (f rawFile) close() { _ = syscall.Close(f.fd) } | |||
| 54 | + | ||||
| 55 | + | // isNotExistErrno reports whether err, from openNul, says that the file or | |||
| 56 | + | // a directory on the way is absent. | |||
| 57 | + | func isNotExistErrno(err error) bool { return err == syscall.ENOENT || err == syscall.ENOTDIR } | |||
| 58 | + | ||||
| 59 | + | func openError(nul []byte, err error) error { | |||
| 60 | + | return &os.PathError{Op: "open", Path: string(nul[:len(nul)-1]), Err: err} | |||
| 61 | + | } | |||