detached bcb6fbd

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +258 -24
M refs.go +40 -22
@@ -3,6 +3,7 @@
33 package git
44
55 import (
6+ "bytes"
67 "errors"
78 "fmt"
89 "io"
@@ -227,28 +228,32 @@ type looseRef struct {
227228 // readLooseRef reads a loose ref. found is false when there is no such file
228229 // (or it is a directory).
229230 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)
231240 if err != nil {
232- if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) {
241+ if isNotExistErrno(err) {
233242 return lr, false, nil
234243 }
235- return lr, false, err
244+ return lr, false, openError(nul, err)
236245 }
237- defer func() { _ = f.Close() }()
246+ defer file.close()
238247 pb := getBuf()
239248 defer putBuf(pb)
240249 pb.b = grow(pb.b, maxRefFile+1)
241250 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)
246252 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
252257 }
253258 if n > maxRefFile {
254259 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) {
261266 }
262267
263268 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))
269275 }
270- return looseRef{symref: target}, nil
276+ return looseRef{symref: t}, nil
271277 }
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') {
274280 return looseRef{id: id}, nil
275281 }
276282 }
277- return looseRef{}, corrupt("bad ref content %q", truncate(s))
283+ return looseRef{}, corrupt("bad ref content %q", truncate(string(b)))
278284 }
279285
280286 // 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) {
405411 return ref.ID, nil
406412 }
407413
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+
408426 // LookupRef is like ResolveRef but returns the whole Ref.
409427 func (r *Repo) LookupRef(name string) (Ref, error) {
410428 ref, ok, err := r.lookupRef(name)
@@ -3,6 +3,7 @@
3 package git3 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 file228 // 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, nil242 return lr, false, nil
234 }243 }
235- return lr, false, err244+ 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.b250 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.PathError253+ 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, nil255+ 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}, nil276+ 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}, nil280 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 such286 // 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, nil411 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
44
55 import (
66 "errors"
7+ "os"
78 "testing"
9+ "time"
810 )
911
1012 func TestCorruptRootRefIsNotFound(t *testing.T) {
@@ -82,3 +84,60 @@ func TestClosedRepoRefs(t *testing.T) {
8284 t.Errorf("Refs yielded %d times, want one error", n)
8385 }
8486 }
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 @@
22
33 package git
44
5-import "time"
5+import (
6+ "errors"
7+ "io"
8+ "syscall"
9+ "time"
10+)
611
712 // fileStat is the part of a stat result that change detection uses.
813 type fileStat struct {
@@ -48,3 +53,23 @@ func (p *statPath) sig() (fileSig, error) {
4853 }
4954 return fileSig{exists: true, size: st.size, mtime: st.mtime, ino: st.ino, read: read}, nil
5055 }
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 git3 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}, nil54 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 {
3535 }
3636 return fillStat(st, &sys, nul, err)
3737 }
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
77 import "syscall"
88
99 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_NEWFSTATAT9 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
77 import "syscall"
88
99 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_FSTATAT9 const sysFstatat = syscall.SYS_FSTATAT
10+
11+const sysOpenat = syscall.SYS_OPENAT
M stat_other.go +28 -1
@@ -4,7 +4,11 @@
44
55 package git
66
7-import "os"
7+import (
8+ "errors"
9+ "io"
10+ "os"
11+)
812
913 // statNul is os.Stat or os.Lstat where there is no stat system call to make
1014 // directly.
@@ -27,3 +31,26 @@ func statNul(nul []byte, lstat bool, st *fileStat) error {
2731 *st = fileStat{exists: true, isDir: fi.IsDir(), size: fi.Size(), mtime: fi.ModTime().UnixNano(), ino: fileIno(fi)}
2832 return nil
2933 }
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 git5 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 make13 // 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 nil32 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 {
2626 }
2727 return fillStat(st, &sys, nul, err)
2828 }
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 @@
55 package git
66
77 import (
8+ "io"
89 "os"
910 "syscall"
1011 )
@@ -28,3 +29,33 @@ func fillStat(st *fileStat, sys *syscall.Stat_t, nul []byte, err error) error {
2829 }
2930 return nil
3031 }
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 git5 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 nil30 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 2

↑↓ moveenter openesc close