detached 4d153b9

feat(git): read refs, loose and packed objects, trees, blobs and commits

Open a bare repository or a .git directory, read-only and safe for
concurrent use. Refs (loose over packed, peeled lines, symbolic HEAD,
unborn branch), loose objects, pack v2 with mmap, fan-out lookup, 64-bit
offsets, ofs and ref deltas with a byte-bounded LRU of delta bases and a
depth bound, prefix resolution with ErrAmbiguous, commit, tree and tag
parsers that allocate nothing per entry, streaming blobs, and Refresh
with refcounted packs so a concurrent repack never breaks a read.

Unsupported layouts fail Open with ErrUnsupported and the reason.
commit
4d153b9d35cbf7448239f45f50dffacefa8417ab
author
brice · · 2026-10-10 20:35 UTC
parent
e62a0a1
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
21 files · +3539 -0
A blob.go +214 -0
@@ -0,0 +1,214 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bytes"
7+ "errors"
8+ "io"
9+)
10+
11+// OpenBlob opens the blob id for reading and returns its size. Undeltified
12+// blobs stream from the memory-mapped pack or the loose file, inflated as they
13+// are read, so a large file is never held in memory. A deltified blob is
14+// rebuilt in a pooled buffer first and is bounded by WithMaxObjectSize
15+// (ErrTooLarge). The caller must close the reader, which also releases the
16+// pack it reads from. Reads detect truncated or oversized data and return
17+// ErrCorrupt.
18+func (r *Repo) OpenBlob(id ObjectID) (int64, io.ReadCloser, error) {
19+ f, err := r.finder()
20+ if err != nil {
21+ return 0, nil, err
22+ }
23+ defer f.close()
24+ for {
25+ p, off, err := f.pack(&id)
26+ if err != nil {
27+ return 0, nil, err
28+ }
29+ if p != nil {
30+ size, rc, err := r.openPackedBlob(p, off, id)
31+ if err != nil {
32+ return 0, nil, wrapPack(p.name, err)
33+ }
34+ return size, rc, nil
35+ }
36+ size, rc, err := r.openLooseBlob(&id)
37+ if err == nil {
38+ return size, rc, nil
39+ }
40+ if !errors.Is(err, ErrNotFound) {
41+ return 0, nil, err
42+ }
43+ if !f.again() {
44+ return 0, nil, notFound("object %s", id)
45+ }
46+ }
47+}
48+
49+func (r *Repo) openPackedBlob(p *pack, off uint64, id ObjectID) (int64, io.ReadCloser, error) {
50+ h, err := parseEntryHeader(p.data, off)
51+ if err != nil {
52+ return 0, nil, err
53+ }
54+ if h.isDelta() {
55+ b := getBuf()
56+ typ, data, err := r.readPacked(p, off, b.b)
57+ if err != nil {
58+ putBuf(b)
59+ return 0, nil, err
60+ }
61+ b.b = data
62+ if typ != TypeBlob {
63+ putBuf(b)
64+ return 0, nil, wrongType(id, typ, TypeBlob)
65+ }
66+ return int64(len(data)), &memBlob{buf: b, r: bytes.NewReader(data)}, nil
67+ }
68+ if Type(h.typ) != TypeBlob {
69+ return 0, nil, wrongType(id, Type(h.typ), TypeBlob)
70+ }
71+ if h.size > 1<<62 || h.size/1032 > uint64(len(p.stream(&h))) {
72+ return 0, nil, corrupt("blob of %d bytes at %d", h.size, off)
73+ }
74+ m, err := getMem(p.stream(&h))
75+ if err != nil {
76+ return 0, nil, err
77+ }
78+ p.retain()
79+ return int64(h.size), &streamBlob{src: m, pack: p, left: int64(h.size), zr: m.zr}, nil
80+}
81+
82+func (r *Repo) openLooseBlob(id *ObjectID) (int64, io.ReadCloser, error) {
83+ l, err := r.openLoose(id)
84+ if err != nil {
85+ return 0, nil, err
86+ }
87+ if l.typ != TypeBlob {
88+ l.close()
89+ return 0, nil, wrongType(*id, l.typ, TypeBlob)
90+ }
91+ if l.size > 1<<62 || uint64(l.nlea) > l.size {
92+ l.close()
93+ return 0, nil, corrupt("loose object %s: size %d", id.String(), l.size)
94+ }
95+ return int64(l.size), &streamBlob{loose: l, left: int64(l.size), zr: l.fi.zr, lead: l.lead[:l.nlea]}, nil
96+}
97+
98+// memBlob serves a blob rebuilt in memory.
99+type memBlob struct {
100+ buf *buffer
101+ r *bytes.Reader
102+}
103+
104+func (m *memBlob) Read(p []byte) (int, error) {
105+ if m.r == nil {
106+ return 0, io.ErrClosedPipe
107+ }
108+ return m.r.Read(p)
109+}
110+
111+func (m *memBlob) WriteTo(w io.Writer) (int64, error) {
112+ if m.r == nil {
113+ return 0, io.ErrClosedPipe
114+ }
115+ return m.r.WriteTo(w)
116+}
117+
118+func (m *memBlob) Close() error {
119+ if m.r != nil {
120+ m.r = nil
121+ putBuf(m.buf)
122+ m.buf = nil
123+ }
124+ return nil
125+}
126+
127+// streamBlob inflates a blob while it is read.
128+type streamBlob struct {
129+ src *memInflater // packed
130+ pack *pack
131+ loose *looseObject // loose
132+ zr io.Reader
133+ lead []byte // loose: content already read with the header
134+ left int64 // bytes still to return
135+ done bool // end of stream verified
136+ err error
137+ probe [1]byte
138+}
139+
140+func (s *streamBlob) Read(p []byte) (int, error) {
141+ if s.zr == nil {
142+ return 0, io.ErrClosedPipe
143+ }
144+ if s.err != nil {
145+ return 0, s.err
146+ }
147+ if len(p) == 0 {
148+ return 0, nil
149+ }
150+ if s.left == 0 {
151+ return 0, s.finish()
152+ }
153+ if int64(len(p)) > s.left {
154+ p = p[:s.left]
155+ }
156+ n := 0
157+ if len(s.lead) > 0 {
158+ n = copy(p, s.lead)
159+ s.lead = s.lead[n:]
160+ s.left -= int64(n)
161+ return n, nil
162+ }
163+ n, err := s.zr.Read(p)
164+ s.left -= int64(n)
165+ if err != nil && !errors.Is(err, io.EOF) {
166+ s.err = corrupt("blob: %v", err)
167+ return n, s.err
168+ }
169+ if errors.Is(err, io.EOF) && s.left > 0 {
170+ s.err = corrupt("blob shorter than its declared size")
171+ return n, s.err
172+ }
173+ if n > 0 {
174+ return n, nil
175+ }
176+ if s.left == 0 {
177+ return 0, s.finish()
178+ }
179+ return 0, nil
180+}
181+
182+// finish checks that the stream ends exactly here and that its checksum is
183+// right, then reports EOF.
184+func (s *streamBlob) finish() error {
185+ if !s.done {
186+ n, err := s.zr.Read(s.probe[:])
187+ switch {
188+ case n != 0:
189+ s.err = corrupt("blob longer than its declared size")
190+ case !errors.Is(err, io.EOF):
191+ s.err = corrupt("blob: %v", err)
192+ }
193+ s.done = true
194+ }
195+ if s.err != nil {
196+ return s.err
197+ }
198+ return io.EOF
199+}
200+
201+func (s *streamBlob) Close() error {
202+ if s.zr == nil {
203+ return nil
204+ }
205+ s.zr = nil
206+ if s.src != nil {
207+ putMem(s.src)
208+ s.pack.release()
209+ }
210+ if s.loose != nil {
211+ s.loose.close()
212+ }
213+ return nil
214+}
@@ -0,0 +1,214 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bytes"
7+ "errors"
8+ "io"
9+)
10+
11+// OpenBlob opens the blob id for reading and returns its size. Undeltified
12+// blobs stream from the memory-mapped pack or the loose file, inflated as they
13+// are read, so a large file is never held in memory. A deltified blob is
14+// rebuilt in a pooled buffer first and is bounded by WithMaxObjectSize
15+// (ErrTooLarge). The caller must close the reader, which also releases the
16+// pack it reads from. Reads detect truncated or oversized data and return
17+// ErrCorrupt.
18+func (r *Repo) OpenBlob(id ObjectID) (int64, io.ReadCloser, error) {
19+ f, err := r.finder()
20+ if err != nil {
21+ return 0, nil, err
22+ }
23+ defer f.close()
24+ for {
25+ p, off, err := f.pack(&id)
26+ if err != nil {
27+ return 0, nil, err
28+ }
29+ if p != nil {
30+ size, rc, err := r.openPackedBlob(p, off, id)
31+ if err != nil {
32+ return 0, nil, wrapPack(p.name, err)
33+ }
34+ return size, rc, nil
35+ }
36+ size, rc, err := r.openLooseBlob(&id)
37+ if err == nil {
38+ return size, rc, nil
39+ }
40+ if !errors.Is(err, ErrNotFound) {
41+ return 0, nil, err
42+ }
43+ if !f.again() {
44+ return 0, nil, notFound("object %s", id)
45+ }
46+ }
47+}
48+
49+func (r *Repo) openPackedBlob(p *pack, off uint64, id ObjectID) (int64, io.ReadCloser, error) {
50+ h, err := parseEntryHeader(p.data, off)
51+ if err != nil {
52+ return 0, nil, err
53+ }
54+ if h.isDelta() {
55+ b := getBuf()
56+ typ, data, err := r.readPacked(p, off, b.b)
57+ if err != nil {
58+ putBuf(b)
59+ return 0, nil, err
60+ }
61+ b.b = data
62+ if typ != TypeBlob {
63+ putBuf(b)
64+ return 0, nil, wrongType(id, typ, TypeBlob)
65+ }
66+ return int64(len(data)), &memBlob{buf: b, r: bytes.NewReader(data)}, nil
67+ }
68+ if Type(h.typ) != TypeBlob {
69+ return 0, nil, wrongType(id, Type(h.typ), TypeBlob)
70+ }
71+ if h.size > 1<<62 || h.size/1032 > uint64(len(p.stream(&h))) {
72+ return 0, nil, corrupt("blob of %d bytes at %d", h.size, off)
73+ }
74+ m, err := getMem(p.stream(&h))
75+ if err != nil {
76+ return 0, nil, err
77+ }
78+ p.retain()
79+ return int64(h.size), &streamBlob{src: m, pack: p, left: int64(h.size), zr: m.zr}, nil
80+}
81+
82+func (r *Repo) openLooseBlob(id *ObjectID) (int64, io.ReadCloser, error) {
83+ l, err := r.openLoose(id)
84+ if err != nil {
85+ return 0, nil, err
86+ }
87+ if l.typ != TypeBlob {
88+ l.close()
89+ return 0, nil, wrongType(*id, l.typ, TypeBlob)
90+ }
91+ if l.size > 1<<62 || uint64(l.nlea) > l.size {
92+ l.close()
93+ return 0, nil, corrupt("loose object %s: size %d", id.String(), l.size)
94+ }
95+ return int64(l.size), &streamBlob{loose: l, left: int64(l.size), zr: l.fi.zr, lead: l.lead[:l.nlea]}, nil
96+}
97+
98+// memBlob serves a blob rebuilt in memory.
99+type memBlob struct {
100+ buf *buffer
101+ r *bytes.Reader
102+}
103+
104+func (m *memBlob) Read(p []byte) (int, error) {
105+ if m.r == nil {
106+ return 0, io.ErrClosedPipe
107+ }
108+ return m.r.Read(p)
109+}
110+
111+func (m *memBlob) WriteTo(w io.Writer) (int64, error) {
112+ if m.r == nil {
113+ return 0, io.ErrClosedPipe
114+ }
115+ return m.r.WriteTo(w)
116+}
117+
118+func (m *memBlob) Close() error {
119+ if m.r != nil {
120+ m.r = nil
121+ putBuf(m.buf)
122+ m.buf = nil
123+ }
124+ return nil
125+}
126+
127+// streamBlob inflates a blob while it is read.
128+type streamBlob struct {
129+ src *memInflater // packed
130+ pack *pack
131+ loose *looseObject // loose
132+ zr io.Reader
133+ lead []byte // loose: content already read with the header
134+ left int64 // bytes still to return
135+ done bool // end of stream verified
136+ err error
137+ probe [1]byte
138+}
139+
140+func (s *streamBlob) Read(p []byte) (int, error) {
141+ if s.zr == nil {
142+ return 0, io.ErrClosedPipe
143+ }
144+ if s.err != nil {
145+ return 0, s.err
146+ }
147+ if len(p) == 0 {
148+ return 0, nil
149+ }
150+ if s.left == 0 {
151+ return 0, s.finish()
152+ }
153+ if int64(len(p)) > s.left {
154+ p = p[:s.left]
155+ }
156+ n := 0
157+ if len(s.lead) > 0 {
158+ n = copy(p, s.lead)
159+ s.lead = s.lead[n:]
160+ s.left -= int64(n)
161+ return n, nil
162+ }
163+ n, err := s.zr.Read(p)
164+ s.left -= int64(n)
165+ if err != nil && !errors.Is(err, io.EOF) {
166+ s.err = corrupt("blob: %v", err)
167+ return n, s.err
168+ }
169+ if errors.Is(err, io.EOF) && s.left > 0 {
170+ s.err = corrupt("blob shorter than its declared size")
171+ return n, s.err
172+ }
173+ if n > 0 {
174+ return n, nil
175+ }
176+ if s.left == 0 {
177+ return 0, s.finish()
178+ }
179+ return 0, nil
180+}
181+
182+// finish checks that the stream ends exactly here and that its checksum is
183+// right, then reports EOF.
184+func (s *streamBlob) finish() error {
185+ if !s.done {
186+ n, err := s.zr.Read(s.probe[:])
187+ switch {
188+ case n != 0:
189+ s.err = corrupt("blob longer than its declared size")
190+ case !errors.Is(err, io.EOF):
191+ s.err = corrupt("blob: %v", err)
192+ }
193+ s.done = true
194+ }
195+ if s.err != nil {
196+ return s.err
197+ }
198+ return io.EOF
199+}
200+
201+func (s *streamBlob) Close() error {
202+ if s.zr == nil {
203+ return nil
204+ }
205+ s.zr = nil
206+ if s.src != nil {
207+ putMem(s.src)
208+ s.pack.release()
209+ }
210+ if s.loose != nil {
211+ s.loose.close()
212+ }
213+ return nil
214+}
A cache.go +130 -0
@@ -0,0 +1,130 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import "sync"
6+
7+// cacheShards is the number of independently locked LRU lists.
8+const cacheShards = 8
9+
10+type cacheKey struct {
11+ pack uint32
12+ off uint64
13+}
14+
15+type cacheEntry struct {
16+ key cacheKey
17+ typ Type
18+ data []byte // immutable once cached
19+ prev, next *cacheEntry
20+}
21+
22+// deltaCache is an LRU cache of delta bases bounded in bytes. Cached slices
23+// are never modified or recycled, so a reader may keep using one after it is
24+// evicted.
25+type deltaCache struct {
26+ shards [cacheShards]cacheShard
27+}
28+
29+type cacheShard struct {
30+ mu sync.Mutex
31+ m map[cacheKey]*cacheEntry
32+ head, tail *cacheEntry // head is the most recently used
33+ bytes int64
34+ budget int64
35+}
36+
37+func newDeltaCache(budget int64) *deltaCache {
38+ c := &deltaCache{}
39+ for i := range c.shards {
40+ c.shards[i].budget = budget / cacheShards
41+ c.shards[i].m = make(map[cacheKey]*cacheEntry)
42+ }
43+ return c
44+}
45+
46+func (c *deltaCache) shard(k cacheKey) *cacheShard {
47+ h := uint64(k.pack)*0x9e3779b97f4a7c15 ^ k.off*0xff51afd7ed558ccd
48+ return &c.shards[(h>>32)%cacheShards]
49+}
50+
51+func (c *deltaCache) get(k cacheKey) (Type, []byte, bool) {
52+ s := c.shard(k)
53+ if s.budget <= 0 {
54+ return 0, nil, false
55+ }
56+ s.mu.Lock()
57+ e := s.m[k]
58+ if e == nil {
59+ s.mu.Unlock()
60+ return 0, nil, false
61+ }
62+ s.moveFront(e)
63+ typ, data := e.typ, e.data
64+ s.mu.Unlock()
65+ return typ, data, true
66+}
67+
68+// fits reports whether an object of n bytes can be cached.
69+func (c *deltaCache) fits(n int) bool {
70+ return int64(n)+64 <= c.shards[0].budget
71+}
72+
73+// putOwned stores data, which the caller must not modify afterwards.
74+func (c *deltaCache) putOwned(k cacheKey, typ Type, data []byte) {
75+ s := c.shard(k)
76+ n := int64(len(data)) + 64
77+ if n > s.budget {
78+ return
79+ }
80+ cp := data
81+ s.mu.Lock()
82+ defer s.mu.Unlock()
83+ if e := s.m[k]; e != nil {
84+ s.moveFront(e)
85+ return
86+ }
87+ e := &cacheEntry{key: k, typ: typ, data: cp}
88+ s.m[k] = e
89+ s.pushFront(e)
90+ s.bytes += n
91+ for s.bytes > s.budget && s.tail != nil {
92+ old := s.tail
93+ s.unlink(old)
94+ delete(s.m, old.key)
95+ s.bytes -= int64(len(old.data)) + 64
96+ }
97+}
98+
99+func (s *cacheShard) unlink(e *cacheEntry) {
100+ if e.prev != nil {
101+ e.prev.next = e.next
102+ } else {
103+ s.head = e.next
104+ }
105+ if e.next != nil {
106+ e.next.prev = e.prev
107+ } else {
108+ s.tail = e.prev
109+ }
110+ e.prev, e.next = nil, nil
111+}
112+
113+func (s *cacheShard) pushFront(e *cacheEntry) {
114+ e.next = s.head
115+ if s.head != nil {
116+ s.head.prev = e
117+ }
118+ s.head = e
119+ if s.tail == nil {
120+ s.tail = e
121+ }
122+}
123+
124+func (s *cacheShard) moveFront(e *cacheEntry) {
125+ if s.head == e {
126+ return
127+ }
128+ s.unlink(e)
129+ s.pushFront(e)
130+}
@@ -0,0 +1,130 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import "sync"
6+
7+// cacheShards is the number of independently locked LRU lists.
8+const cacheShards = 8
9+
10+type cacheKey struct {
11+ pack uint32
12+ off uint64
13+}
14+
15+type cacheEntry struct {
16+ key cacheKey
17+ typ Type
18+ data []byte // immutable once cached
19+ prev, next *cacheEntry
20+}
21+
22+// deltaCache is an LRU cache of delta bases bounded in bytes. Cached slices
23+// are never modified or recycled, so a reader may keep using one after it is
24+// evicted.
25+type deltaCache struct {
26+ shards [cacheShards]cacheShard
27+}
28+
29+type cacheShard struct {
30+ mu sync.Mutex
31+ m map[cacheKey]*cacheEntry
32+ head, tail *cacheEntry // head is the most recently used
33+ bytes int64
34+ budget int64
35+}
36+
37+func newDeltaCache(budget int64) *deltaCache {
38+ c := &deltaCache{}
39+ for i := range c.shards {
40+ c.shards[i].budget = budget / cacheShards
41+ c.shards[i].m = make(map[cacheKey]*cacheEntry)
42+ }
43+ return c
44+}
45+
46+func (c *deltaCache) shard(k cacheKey) *cacheShard {
47+ h := uint64(k.pack)*0x9e3779b97f4a7c15 ^ k.off*0xff51afd7ed558ccd
48+ return &c.shards[(h>>32)%cacheShards]
49+}
50+
51+func (c *deltaCache) get(k cacheKey) (Type, []byte, bool) {
52+ s := c.shard(k)
53+ if s.budget <= 0 {
54+ return 0, nil, false
55+ }
56+ s.mu.Lock()
57+ e := s.m[k]
58+ if e == nil {
59+ s.mu.Unlock()
60+ return 0, nil, false
61+ }
62+ s.moveFront(e)
63+ typ, data := e.typ, e.data
64+ s.mu.Unlock()
65+ return typ, data, true
66+}
67+
68+// fits reports whether an object of n bytes can be cached.
69+func (c *deltaCache) fits(n int) bool {
70+ return int64(n)+64 <= c.shards[0].budget
71+}
72+
73+// putOwned stores data, which the caller must not modify afterwards.
74+func (c *deltaCache) putOwned(k cacheKey, typ Type, data []byte) {
75+ s := c.shard(k)
76+ n := int64(len(data)) + 64
77+ if n > s.budget {
78+ return
79+ }
80+ cp := data
81+ s.mu.Lock()
82+ defer s.mu.Unlock()
83+ if e := s.m[k]; e != nil {
84+ s.moveFront(e)
85+ return
86+ }
87+ e := &cacheEntry{key: k, typ: typ, data: cp}
88+ s.m[k] = e
89+ s.pushFront(e)
90+ s.bytes += n
91+ for s.bytes > s.budget && s.tail != nil {
92+ old := s.tail
93+ s.unlink(old)
94+ delete(s.m, old.key)
95+ s.bytes -= int64(len(old.data)) + 64
96+ }
97+}
98+
99+func (s *cacheShard) unlink(e *cacheEntry) {
100+ if e.prev != nil {
101+ e.prev.next = e.next
102+ } else {
103+ s.head = e.next
104+ }
105+ if e.next != nil {
106+ e.next.prev = e.prev
107+ } else {
108+ s.tail = e.prev
109+ }
110+ e.prev, e.next = nil, nil
111+}
112+
113+func (s *cacheShard) pushFront(e *cacheEntry) {
114+ e.next = s.head
115+ if s.head != nil {
116+ s.head.prev = e
117+ }
118+ s.head = e
119+ if s.tail == nil {
120+ s.tail = e
121+ }
122+}
123+
124+func (s *cacheShard) moveFront(e *cacheEntry) {
125+ if s.head == e {
126+ return
127+ }
128+ s.unlink(e)
129+ s.pushFront(e)
130+}
A commit.go +231 -0
@@ -0,0 +1,231 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bytes"
7+ "time"
8+)
9+
10+// Signature is the author or committer line of a commit or the tagger of a
11+// tag. Name and Email point into the buffer of the object they come from.
12+type Signature struct {
13+ Name []byte
14+ Email []byte
15+ Time int64 // seconds since the Unix epoch
16+ Offset int // time zone, in seconds east of UTC
17+}
18+
19+// Date returns the time of the signature, in its own time zone. It allocates.
20+func (s Signature) Date() time.Time {
21+ t := time.Unix(s.Time, 0)
22+ if s.Offset == 0 {
23+ return t.UTC()
24+ }
25+ return t.In(time.FixedZone("", s.Offset))
26+}
27+
28+// Commit is a parsed commit. A Commit is meant to be reused: ReadCommit and
29+// ParseCommit overwrite it and reuse its storage, so a hot loop allocates
30+// nothing after the first few commits. Every []byte field is valid until the
31+// next call that fills this Commit.
32+type Commit struct {
33+ ID ObjectID // set by ReadCommit
34+ Tree ObjectID
35+ Parents []ObjectID
36+ Author Signature
37+ Committer Signature
38+ // Encoding is the value of the encoding header, nil when absent. The
39+ // other headers (gpgsig, mergetag...) are skipped.
40+ Encoding []byte
41+ // Message is everything after the blank line that ends the headers,
42+ // as raw bytes: it may not be valid UTF-8.
43+ Message []byte
44+
45+ buf []byte
46+}
47+
48+// ReadCommit reads and parses the commit id into c.
49+func (r *Repo) ReadCommit(id ObjectID, c *Commit) error {
50+ typ, data, err := r.ReadObject(id, c.buf)
51+ if err != nil {
52+ return err
53+ }
54+ c.buf = data
55+ if typ != TypeCommit {
56+ return wrongType(id, typ, TypeCommit)
57+ }
58+ if err := ParseCommit(data, c); err != nil {
59+ return wrapObj(id, err)
60+ }
61+ c.ID = id
62+ return nil
63+}
64+
65+// ParseCommit parses the content of a commit object into c. The fields of c
66+// point into data, which must stay unchanged while they are used. It returns
67+// ErrCorrupt if the commit has no valid tree or a malformed parent.
68+func ParseCommit(data []byte, c *Commit) error {
69+ c.ID = ObjectID{}
70+ c.Parents = c.Parents[:0]
71+ c.Author, c.Committer = Signature{}, Signature{}
72+ c.Encoding, c.Message = nil, nil
73+ haveTree := false
74+ for len(data) > 0 {
75+ if data[0] == '\n' {
76+ c.Message = data[1:]
77+ break
78+ }
79+ var line []byte
80+ if i := bytes.IndexByte(data, '\n'); i >= 0 {
81+ line, data = data[:i], data[i+1:]
82+ } else {
83+ line, data = data, nil
84+ }
85+ if line[0] == ' ' {
86+ continue // continuation of a multi-line header such as gpgsig
87+ }
88+ key, val, _ := bytes.Cut(line, []byte{' '})
89+ switch string(key) {
90+ case "tree":
91+ id, ok := parseIDBytes(val)
92+ if !ok || haveTree {
93+ return corrupt("commit: bad tree header")
94+ }
95+ c.Tree, haveTree = id, true
96+ case "parent":
97+ id, ok := parseIDBytes(val)
98+ if !ok {
99+ return corrupt("commit: bad parent header")
100+ }
101+ c.Parents = append(c.Parents, id)
102+ case "author":
103+ c.Author = parseSignature(val)
104+ case "committer":
105+ c.Committer = parseSignature(val)
106+ case "encoding":
107+ c.Encoding = val
108+ }
109+ }
110+ if !haveTree {
111+ return corrupt("commit: no tree")
112+ }
113+ return nil
114+}
115+
116+// parseSignature parses "Name <email> <seconds> <+hhmm>". It is as lenient
117+// as Git: what cannot be parsed is left zero.
118+func parseSignature(b []byte) Signature {
119+ var s Signature
120+ lt := bytes.IndexByte(b, '<')
121+ gt := bytes.LastIndexByte(b, '>')
122+ if lt < 0 || gt < lt {
123+ s.Name = bytes.TrimSpace(b)
124+ return s
125+ }
126+ s.Name = bytes.TrimRight(b[:lt], " ")
127+ s.Email = b[lt+1 : gt]
128+ rest := bytes.TrimLeft(b[gt+1:], " ")
129+ secs, rest, _ := bytes.Cut(rest, []byte{' '})
130+ if t, ok := parseInt(secs); ok {
131+ s.Time = t
132+ if tz := bytes.TrimSpace(rest); len(tz) == 5 && (tz[0] == '+' || tz[0] == '-') {
133+ hh, ok1 := parseInt(tz[1:3])
134+ mm, ok2 := parseInt(tz[3:5])
135+ if ok1 && ok2 {
136+ off := int(hh*3600 + mm*60)
137+ if tz[0] == '-' {
138+ off = -off
139+ }
140+ s.Offset = off
141+ }
142+ }
143+ }
144+ return s
145+}
146+
147+// parseInt parses a decimal integer of up to 18 digits.
148+func parseInt(b []byte) (int64, bool) {
149+ if len(b) == 0 || len(b) > 18 {
150+ return 0, false
151+ }
152+ var v int64
153+ for _, c := range b {
154+ if c < '0' || c > '9' {
155+ return 0, false
156+ }
157+ v = v*10 + int64(c-'0')
158+ }
159+ return v, true
160+}
161+
162+// Tag is a parsed annotated tag. Name, Tagger and Message point into the
163+// data that was parsed.
164+type Tag struct {
165+ Object ObjectID
166+ Type Type // the type of Object
167+ Name []byte
168+ Tagger Signature
169+ Message []byte
170+}
171+
172+// ReadTag reads and parses the annotated tag id into t. The byte fields of t
173+// are copies, owned by the caller.
174+func (r *Repo) ReadTag(id ObjectID, t *Tag) error {
175+ b, err := r.readTyped(id, TypeTag)
176+ if err != nil {
177+ return err
178+ }
179+ defer putBuf(b)
180+ if err := ParseTag(b.b, t); err != nil {
181+ return wrapObj(id, err)
182+ }
183+ // The pooled buffer goes back: detach the fields.
184+ t.Name = bytes.Clone(t.Name)
185+ t.Message = bytes.Clone(t.Message)
186+ t.Tagger.Name = bytes.Clone(t.Tagger.Name)
187+ t.Tagger.Email = bytes.Clone(t.Tagger.Email)
188+ return nil
189+}
190+
191+// ParseTag parses the content of a tag object into t, whose byte fields point
192+// into data. It returns ErrCorrupt if the object or its type is missing.
193+func ParseTag(data []byte, t *Tag) error {
194+ *t = Tag{}
195+ haveObj, haveType := false, false
196+ for len(data) > 0 {
197+ if data[0] == '\n' {
198+ t.Message = data[1:]
199+ break
200+ }
201+ var line []byte
202+ if i := bytes.IndexByte(data, '\n'); i >= 0 {
203+ line, data = data[:i], data[i+1:]
204+ } else {
205+ line, data = data, nil
206+ }
207+ key, val, _ := bytes.Cut(line, []byte{' '})
208+ switch string(key) {
209+ case "object":
210+ id, ok := parseIDBytes(val)
211+ if !ok {
212+ return corrupt("tag: bad object header")
213+ }
214+ t.Object, haveObj = id, true
215+ case "type":
216+ typ, ok := parseTypeName(val)
217+ if !ok {
218+ return corrupt("tag: bad type header")
219+ }
220+ t.Type, haveType = typ, true
221+ case "tag":
222+ t.Name = val
223+ case "tagger":
224+ t.Tagger = parseSignature(val)
225+ }
226+ }
227+ if !haveObj || !haveType {
228+ return corrupt("tag: missing object or type")
229+ }
230+ return nil
231+}
@@ -0,0 +1,231 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bytes"
7+ "time"
8+)
9+
10+// Signature is the author or committer line of a commit or the tagger of a
11+// tag. Name and Email point into the buffer of the object they come from.
12+type Signature struct {
13+ Name []byte
14+ Email []byte
15+ Time int64 // seconds since the Unix epoch
16+ Offset int // time zone, in seconds east of UTC
17+}
18+
19+// Date returns the time of the signature, in its own time zone. It allocates.
20+func (s Signature) Date() time.Time {
21+ t := time.Unix(s.Time, 0)
22+ if s.Offset == 0 {
23+ return t.UTC()
24+ }
25+ return t.In(time.FixedZone("", s.Offset))
26+}
27+
28+// Commit is a parsed commit. A Commit is meant to be reused: ReadCommit and
29+// ParseCommit overwrite it and reuse its storage, so a hot loop allocates
30+// nothing after the first few commits. Every []byte field is valid until the
31+// next call that fills this Commit.
32+type Commit struct {
33+ ID ObjectID // set by ReadCommit
34+ Tree ObjectID
35+ Parents []ObjectID
36+ Author Signature
37+ Committer Signature
38+ // Encoding is the value of the encoding header, nil when absent. The
39+ // other headers (gpgsig, mergetag...) are skipped.
40+ Encoding []byte
41+ // Message is everything after the blank line that ends the headers,
42+ // as raw bytes: it may not be valid UTF-8.
43+ Message []byte
44+
45+ buf []byte
46+}
47+
48+// ReadCommit reads and parses the commit id into c.
49+func (r *Repo) ReadCommit(id ObjectID, c *Commit) error {
50+ typ, data, err := r.ReadObject(id, c.buf)
51+ if err != nil {
52+ return err
53+ }
54+ c.buf = data
55+ if typ != TypeCommit {
56+ return wrongType(id, typ, TypeCommit)
57+ }
58+ if err := ParseCommit(data, c); err != nil {
59+ return wrapObj(id, err)
60+ }
61+ c.ID = id
62+ return nil
63+}
64+
65+// ParseCommit parses the content of a commit object into c. The fields of c
66+// point into data, which must stay unchanged while they are used. It returns
67+// ErrCorrupt if the commit has no valid tree or a malformed parent.
68+func ParseCommit(data []byte, c *Commit) error {
69+ c.ID = ObjectID{}
70+ c.Parents = c.Parents[:0]
71+ c.Author, c.Committer = Signature{}, Signature{}
72+ c.Encoding, c.Message = nil, nil
73+ haveTree := false
74+ for len(data) > 0 {
75+ if data[0] == '\n' {
76+ c.Message = data[1:]
77+ break
78+ }
79+ var line []byte
80+ if i := bytes.IndexByte(data, '\n'); i >= 0 {
81+ line, data = data[:i], data[i+1:]
82+ } else {
83+ line, data = data, nil
84+ }
85+ if line[0] == ' ' {
86+ continue // continuation of a multi-line header such as gpgsig
87+ }
88+ key, val, _ := bytes.Cut(line, []byte{' '})
89+ switch string(key) {
90+ case "tree":
91+ id, ok := parseIDBytes(val)
92+ if !ok || haveTree {
93+ return corrupt("commit: bad tree header")
94+ }
95+ c.Tree, haveTree = id, true
96+ case "parent":
97+ id, ok := parseIDBytes(val)
98+ if !ok {
99+ return corrupt("commit: bad parent header")
100+ }
101+ c.Parents = append(c.Parents, id)
102+ case "author":
103+ c.Author = parseSignature(val)
104+ case "committer":
105+ c.Committer = parseSignature(val)
106+ case "encoding":
107+ c.Encoding = val
108+ }
109+ }
110+ if !haveTree {
111+ return corrupt("commit: no tree")
112+ }
113+ return nil
114+}
115+
116+// parseSignature parses "Name <email> <seconds> <+hhmm>". It is as lenient
117+// as Git: what cannot be parsed is left zero.
118+func parseSignature(b []byte) Signature {
119+ var s Signature
120+ lt := bytes.IndexByte(b, '<')
121+ gt := bytes.LastIndexByte(b, '>')
122+ if lt < 0 || gt < lt {
123+ s.Name = bytes.TrimSpace(b)
124+ return s
125+ }
126+ s.Name = bytes.TrimRight(b[:lt], " ")
127+ s.Email = b[lt+1 : gt]
128+ rest := bytes.TrimLeft(b[gt+1:], " ")
129+ secs, rest, _ := bytes.Cut(rest, []byte{' '})
130+ if t, ok := parseInt(secs); ok {
131+ s.Time = t
132+ if tz := bytes.TrimSpace(rest); len(tz) == 5 && (tz[0] == '+' || tz[0] == '-') {
133+ hh, ok1 := parseInt(tz[1:3])
134+ mm, ok2 := parseInt(tz[3:5])
135+ if ok1 && ok2 {
136+ off := int(hh*3600 + mm*60)
137+ if tz[0] == '-' {
138+ off = -off
139+ }
140+ s.Offset = off
141+ }
142+ }
143+ }
144+ return s
145+}
146+
147+// parseInt parses a decimal integer of up to 18 digits.
148+func parseInt(b []byte) (int64, bool) {
149+ if len(b) == 0 || len(b) > 18 {
150+ return 0, false
151+ }
152+ var v int64
153+ for _, c := range b {
154+ if c < '0' || c > '9' {
155+ return 0, false
156+ }
157+ v = v*10 + int64(c-'0')
158+ }
159+ return v, true
160+}
161+
162+// Tag is a parsed annotated tag. Name, Tagger and Message point into the
163+// data that was parsed.
164+type Tag struct {
165+ Object ObjectID
166+ Type Type // the type of Object
167+ Name []byte
168+ Tagger Signature
169+ Message []byte
170+}
171+
172+// ReadTag reads and parses the annotated tag id into t. The byte fields of t
173+// are copies, owned by the caller.
174+func (r *Repo) ReadTag(id ObjectID, t *Tag) error {
175+ b, err := r.readTyped(id, TypeTag)
176+ if err != nil {
177+ return err
178+ }
179+ defer putBuf(b)
180+ if err := ParseTag(b.b, t); err != nil {
181+ return wrapObj(id, err)
182+ }
183+ // The pooled buffer goes back: detach the fields.
184+ t.Name = bytes.Clone(t.Name)
185+ t.Message = bytes.Clone(t.Message)
186+ t.Tagger.Name = bytes.Clone(t.Tagger.Name)
187+ t.Tagger.Email = bytes.Clone(t.Tagger.Email)
188+ return nil
189+}
190+
191+// ParseTag parses the content of a tag object into t, whose byte fields point
192+// into data. It returns ErrCorrupt if the object or its type is missing.
193+func ParseTag(data []byte, t *Tag) error {
194+ *t = Tag{}
195+ haveObj, haveType := false, false
196+ for len(data) > 0 {
197+ if data[0] == '\n' {
198+ t.Message = data[1:]
199+ break
200+ }
201+ var line []byte
202+ if i := bytes.IndexByte(data, '\n'); i >= 0 {
203+ line, data = data[:i], data[i+1:]
204+ } else {
205+ line, data = data, nil
206+ }
207+ key, val, _ := bytes.Cut(line, []byte{' '})
208+ switch string(key) {
209+ case "object":
210+ id, ok := parseIDBytes(val)
211+ if !ok {
212+ return corrupt("tag: bad object header")
213+ }
214+ t.Object, haveObj = id, true
215+ case "type":
216+ typ, ok := parseTypeName(val)
217+ if !ok {
218+ return corrupt("tag: bad type header")
219+ }
220+ t.Type, haveType = typ, true
221+ case "tag":
222+ t.Name = val
223+ case "tagger":
224+ t.Tagger = parseSignature(val)
225+ }
226+ }
227+ if !haveObj || !haveType {
228+ return corrupt("tag: missing object or type")
229+ }
230+ return nil
231+}
A config.go +116 -0
@@ -0,0 +1,116 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bytes"
7+ "errors"
8+ "io/fs"
9+ "os"
10+ "strconv"
11+ "strings"
12+)
13+
14+// maxConfigSize bounds the config file that is read.
15+const maxConfigSize = 1 << 20
16+
17+// checkConfig reads the few settings that decide whether the repository is
18+// readable: core.repositoryformatversion and the extensions.* keys. Other
19+// settings, includes and conditional includes are ignored.
20+func checkConfig(path string) error {
21+ data, err := os.ReadFile(path)
22+ if errors.Is(err, fs.ErrNotExist) {
23+ return nil
24+ }
25+ if err != nil {
26+ return err
27+ }
28+ if len(data) > maxConfigSize {
29+ return unsupported("config file larger than %d bytes", maxConfigSize)
30+ }
31+ return checkConfigData(data)
32+}
33+
34+func checkConfigData(data []byte) error {
35+ section := ""
36+ version := int64(0)
37+ for len(data) > 0 {
38+ var line []byte
39+ line, data, _ = bytes.Cut(data, []byte{'\n'})
40+ line = bytes.TrimSpace(line)
41+ if len(line) == 0 || line[0] == '#' || line[0] == ';' {
42+ continue
43+ }
44+ if line[0] == '[' {
45+ end := bytes.IndexByte(line, ']')
46+ if end < 0 {
47+ return corrupt("config: bad section line %q", truncate(string(line)))
48+ }
49+ head := string(bytes.TrimSpace(line[1:end]))
50+ // "[core]", "[remote \"origin\"]", "[branch.main]"
51+ name, _, _ := strings.Cut(head, " ")
52+ name, _, _ = strings.Cut(name, ".")
53+ section = strings.ToLower(name)
54+ line = bytes.TrimSpace(line[end+1:])
55+ if len(line) == 0 || line[0] == '#' || line[0] == ';' {
56+ continue
57+ }
58+ // "[core] bare = true" on one line.
59+ }
60+ keyb, valb, _ := bytes.Cut(line, []byte{'='})
61+ key := strings.ToLower(string(bytes.TrimSpace(keyb)))
62+ val := configValue(valb)
63+ switch section {
64+ case "core":
65+ if key == "repositoryformatversion" {
66+ v, err := strconv.ParseInt(val, 10, 64)
67+ if err != nil {
68+ return corrupt("config: core.repositoryformatversion %q", truncate(val))
69+ }
70+ version = v
71+ }
72+ case "extensions":
73+ switch key {
74+ case "objectformat":
75+ if !strings.EqualFold(val, "sha1") {
76+ return unsupported("extensions.objectformat = %s", truncate(val))
77+ }
78+ case "refstorage":
79+ if !strings.EqualFold(val, "files") {
80+ return unsupported("extensions.refstorage = %s", truncate(val))
81+ }
82+ case "noop", "worktreeconfig":
83+ // No effect on reading a bare repository.
84+ default:
85+ return unsupported("extensions.%s", truncate(key))
86+ }
87+ }
88+ }
89+ if version != 0 && version != 1 {
90+ return unsupported("core.repositoryformatversion = %d", version)
91+ }
92+ return nil
93+}
94+
95+// configValue returns the unquoted value of a config line, without a trailing
96+// comment.
97+func configValue(b []byte) string {
98+ b = bytes.TrimSpace(b)
99+ var sb strings.Builder
100+ inQuote := false
101+ for i := 0; i < len(b); i++ {
102+ c := b[i]
103+ switch {
104+ case c == '\\' && i+1 < len(b):
105+ i++
106+ sb.WriteByte(b[i])
107+ case c == '"':
108+ inQuote = !inQuote
109+ case !inQuote && (c == '#' || c == ';'):
110+ return strings.TrimSpace(sb.String())
111+ default:
112+ sb.WriteByte(c)
113+ }
114+ }
115+ return strings.TrimSpace(sb.String())
116+}
@@ -0,0 +1,116 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bytes"
7+ "errors"
8+ "io/fs"
9+ "os"
10+ "strconv"
11+ "strings"
12+)
13+
14+// maxConfigSize bounds the config file that is read.
15+const maxConfigSize = 1 << 20
16+
17+// checkConfig reads the few settings that decide whether the repository is
18+// readable: core.repositoryformatversion and the extensions.* keys. Other
19+// settings, includes and conditional includes are ignored.
20+func checkConfig(path string) error {
21+ data, err := os.ReadFile(path)
22+ if errors.Is(err, fs.ErrNotExist) {
23+ return nil
24+ }
25+ if err != nil {
26+ return err
27+ }
28+ if len(data) > maxConfigSize {
29+ return unsupported("config file larger than %d bytes", maxConfigSize)
30+ }
31+ return checkConfigData(data)
32+}
33+
34+func checkConfigData(data []byte) error {
35+ section := ""
36+ version := int64(0)
37+ for len(data) > 0 {
38+ var line []byte
39+ line, data, _ = bytes.Cut(data, []byte{'\n'})
40+ line = bytes.TrimSpace(line)
41+ if len(line) == 0 || line[0] == '#' || line[0] == ';' {
42+ continue
43+ }
44+ if line[0] == '[' {
45+ end := bytes.IndexByte(line, ']')
46+ if end < 0 {
47+ return corrupt("config: bad section line %q", truncate(string(line)))
48+ }
49+ head := string(bytes.TrimSpace(line[1:end]))
50+ // "[core]", "[remote \"origin\"]", "[branch.main]"
51+ name, _, _ := strings.Cut(head, " ")
52+ name, _, _ = strings.Cut(name, ".")
53+ section = strings.ToLower(name)
54+ line = bytes.TrimSpace(line[end+1:])
55+ if len(line) == 0 || line[0] == '#' || line[0] == ';' {
56+ continue
57+ }
58+ // "[core] bare = true" on one line.
59+ }
60+ keyb, valb, _ := bytes.Cut(line, []byte{'='})
61+ key := strings.ToLower(string(bytes.TrimSpace(keyb)))
62+ val := configValue(valb)
63+ switch section {
64+ case "core":
65+ if key == "repositoryformatversion" {
66+ v, err := strconv.ParseInt(val, 10, 64)
67+ if err != nil {
68+ return corrupt("config: core.repositoryformatversion %q", truncate(val))
69+ }
70+ version = v
71+ }
72+ case "extensions":
73+ switch key {
74+ case "objectformat":
75+ if !strings.EqualFold(val, "sha1") {
76+ return unsupported("extensions.objectformat = %s", truncate(val))
77+ }
78+ case "refstorage":
79+ if !strings.EqualFold(val, "files") {
80+ return unsupported("extensions.refstorage = %s", truncate(val))
81+ }
82+ case "noop", "worktreeconfig":
83+ // No effect on reading a bare repository.
84+ default:
85+ return unsupported("extensions.%s", truncate(key))
86+ }
87+ }
88+ }
89+ if version != 0 && version != 1 {
90+ return unsupported("core.repositoryformatversion = %d", version)
91+ }
92+ return nil
93+}
94+
95+// configValue returns the unquoted value of a config line, without a trailing
96+// comment.
97+func configValue(b []byte) string {
98+ b = bytes.TrimSpace(b)
99+ var sb strings.Builder
100+ inQuote := false
101+ for i := 0; i < len(b); i++ {
102+ c := b[i]
103+ switch {
104+ case c == '\\' && i+1 < len(b):
105+ i++
106+ sb.WriteByte(b[i])
107+ case c == '"':
108+ inQuote = !inQuote
109+ case !inQuote && (c == '#' || c == ';'):
110+ return strings.TrimSpace(sb.String())
111+ default:
112+ sb.WriteByte(c)
113+ }
114+ }
115+ return strings.TrimSpace(sb.String())
116+}
A delta.go +117 -0
@@ -0,0 +1,117 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+// deltaVarint reads a little-endian base-128 size of a delta header.
6+func deltaVarint(d []byte, p int) (v uint64, next int, ok bool) {
7+ var shift uint
8+ for p < len(d) {
9+ c := d[p]
10+ p++
11+ if shift > 56 {
12+ return 0, 0, false
13+ }
14+ v |= uint64(c&0x7f) << shift
15+ shift += 7
16+ if c&0x80 == 0 {
17+ return v, p, true
18+ }
19+ }
20+ return 0, 0, false
21+}
22+
23+// deltaHeader parses the base size and the result size of a delta.
24+func deltaHeader(d []byte) (baseSize, resultSize uint64, body int, err error) {
25+ var ok bool
26+ baseSize, p, ok := deltaVarint(d, 0)
27+ if !ok {
28+ return 0, 0, 0, corrupt("delta: bad base size")
29+ }
30+ resultSize, p, ok = deltaVarint(d, p)
31+ if !ok {
32+ return 0, 0, 0, corrupt("delta: bad result size")
33+ }
34+ return baseSize, resultSize, p, nil
35+}
36+
37+// applyDelta applies delta to base and writes the result into dst, whose
38+// length is the result size announced by the delta header (checked by the
39+// caller with deltaHeader). Every copy and insert is bounds checked.
40+func applyDelta(dst, base, delta []byte, body int) error {
41+ out := 0
42+ p := body
43+ for p < len(delta) {
44+ op := delta[p]
45+ p++
46+ switch {
47+ case op&0x80 != 0:
48+ var off, size uint64
49+ for i := uint(0); i < 4; i++ {
50+ if op&(1<<i) != 0 {
51+ if p >= len(delta) {
52+ return corrupt("delta: truncated copy offset")
53+ }
54+ off |= uint64(delta[p]) << (8 * i)
55+ p++
56+ }
57+ }
58+ for i := uint(0); i < 3; i++ {
59+ if op&(0x10<<i) != 0 {
60+ if p >= len(delta) {
61+ return corrupt("delta: truncated copy size")
62+ }
63+ size |= uint64(delta[p]) << (8 * i)
64+ p++
65+ }
66+ }
67+ if size == 0 {
68+ size = 0x10000
69+ }
70+ if off > uint64(len(base)) || size > uint64(len(base))-off {
71+ return corrupt("delta: copy %d+%d outside base of %d", off, size, len(base))
72+ }
73+ if size > uint64(len(dst)-out) {
74+ return corrupt("delta: result longer than announced")
75+ }
76+ copy(dst[out:], base[off:off+size])
77+ out += int(size)
78+ case op != 0:
79+ n := int(op)
80+ if n > len(delta)-p {
81+ return corrupt("delta: truncated insert")
82+ }
83+ if n > len(dst)-out {
84+ return corrupt("delta: result longer than announced")
85+ }
86+ copy(dst[out:], delta[p:p+n])
87+ p += n
88+ out += n
89+ default:
90+ return corrupt("delta: reserved opcode 0")
91+ }
92+ }
93+ if out != len(dst) {
94+ return corrupt("delta: result of %d bytes, %d announced", out, len(dst))
95+ }
96+ return nil
97+}
98+
99+// patch applies a delta to base and returns the result, which is at most
100+// limit bytes. It is the allocating form of applyDelta, for tests and fuzzing.
101+func patch(base, delta []byte, limit int64) ([]byte, error) {
102+ bs, rs, body, err := deltaHeader(delta)
103+ if err != nil {
104+ return nil, err
105+ }
106+ if bs != uint64(len(base)) {
107+ return nil, corrupt("delta: base is %d bytes, delta expects %d", len(base), bs)
108+ }
109+ if rs > uint64(limit) {
110+ return nil, ErrTooLarge
111+ }
112+ dst := make([]byte, rs)
113+ if err := applyDelta(dst, base, delta, body); err != nil {
114+ return nil, err
115+ }
116+ return dst, nil
117+}
@@ -0,0 +1,117 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+// deltaVarint reads a little-endian base-128 size of a delta header.
6+func deltaVarint(d []byte, p int) (v uint64, next int, ok bool) {
7+ var shift uint
8+ for p < len(d) {
9+ c := d[p]
10+ p++
11+ if shift > 56 {
12+ return 0, 0, false
13+ }
14+ v |= uint64(c&0x7f) << shift
15+ shift += 7
16+ if c&0x80 == 0 {
17+ return v, p, true
18+ }
19+ }
20+ return 0, 0, false
21+}
22+
23+// deltaHeader parses the base size and the result size of a delta.
24+func deltaHeader(d []byte) (baseSize, resultSize uint64, body int, err error) {
25+ var ok bool
26+ baseSize, p, ok := deltaVarint(d, 0)
27+ if !ok {
28+ return 0, 0, 0, corrupt("delta: bad base size")
29+ }
30+ resultSize, p, ok = deltaVarint(d, p)
31+ if !ok {
32+ return 0, 0, 0, corrupt("delta: bad result size")
33+ }
34+ return baseSize, resultSize, p, nil
35+}
36+
37+// applyDelta applies delta to base and writes the result into dst, whose
38+// length is the result size announced by the delta header (checked by the
39+// caller with deltaHeader). Every copy and insert is bounds checked.
40+func applyDelta(dst, base, delta []byte, body int) error {
41+ out := 0
42+ p := body
43+ for p < len(delta) {
44+ op := delta[p]
45+ p++
46+ switch {
47+ case op&0x80 != 0:
48+ var off, size uint64
49+ for i := uint(0); i < 4; i++ {
50+ if op&(1<<i) != 0 {
51+ if p >= len(delta) {
52+ return corrupt("delta: truncated copy offset")
53+ }
54+ off |= uint64(delta[p]) << (8 * i)
55+ p++
56+ }
57+ }
58+ for i := uint(0); i < 3; i++ {
59+ if op&(0x10<<i) != 0 {
60+ if p >= len(delta) {
61+ return corrupt("delta: truncated copy size")
62+ }
63+ size |= uint64(delta[p]) << (8 * i)
64+ p++
65+ }
66+ }
67+ if size == 0 {
68+ size = 0x10000
69+ }
70+ if off > uint64(len(base)) || size > uint64(len(base))-off {
71+ return corrupt("delta: copy %d+%d outside base of %d", off, size, len(base))
72+ }
73+ if size > uint64(len(dst)-out) {
74+ return corrupt("delta: result longer than announced")
75+ }
76+ copy(dst[out:], base[off:off+size])
77+ out += int(size)
78+ case op != 0:
79+ n := int(op)
80+ if n > len(delta)-p {
81+ return corrupt("delta: truncated insert")
82+ }
83+ if n > len(dst)-out {
84+ return corrupt("delta: result longer than announced")
85+ }
86+ copy(dst[out:], delta[p:p+n])
87+ p += n
88+ out += n
89+ default:
90+ return corrupt("delta: reserved opcode 0")
91+ }
92+ }
93+ if out != len(dst) {
94+ return corrupt("delta: result of %d bytes, %d announced", out, len(dst))
95+ }
96+ return nil
97+}
98+
99+// patch applies a delta to base and returns the result, which is at most
100+// limit bytes. It is the allocating form of applyDelta, for tests and fuzzing.
101+func patch(base, delta []byte, limit int64) ([]byte, error) {
102+ bs, rs, body, err := deltaHeader(delta)
103+ if err != nil {
104+ return nil, err
105+ }
106+ if bs != uint64(len(base)) {
107+ return nil, corrupt("delta: base is %d bytes, delta expects %d", len(base), bs)
108+ }
109+ if rs > uint64(limit) {
110+ return nil, ErrTooLarge
111+ }
112+ dst := make([]byte, rs)
113+ if err := applyDelta(dst, base, delta, body); err != nil {
114+ return nil, err
115+ }
116+ return dst, nil
117+}
A errors.go +46 -0
@@ -0,0 +1,46 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "errors"
7+ "fmt"
8+)
9+
10+// Errors returned by the package. They are wrapped with context, so test them
11+// with errors.Is.
12+var (
13+ // ErrNotFound reports a missing object, ref or path, or a directory that
14+ // is not a Git repository.
15+ ErrNotFound = errors.New("git: not found")
16+ // ErrUnsupported reports a repository feature the package does not read:
17+ // the error is wrapped with the reason.
18+ ErrUnsupported = errors.New("git: unsupported repository")
19+ // ErrAmbiguous reports an abbreviated object id that matches several
20+ // objects.
21+ ErrAmbiguous = errors.New("git: ambiguous object id")
22+ // ErrCorrupt reports malformed repository data. Reading never panics on
23+ // such data.
24+ ErrCorrupt = errors.New("git: corrupt data")
25+ // ErrInvalidID reports a malformed object id or abbreviation.
26+ ErrInvalidID = errors.New("git: invalid object id")
27+ // ErrWrongType reports an object that does not have the expected type.
28+ ErrWrongType = errors.New("git: wrong object type")
29+ // ErrTooLarge reports an object bigger than the configured bound
30+ // (see WithMaxObjectSize).
31+ ErrTooLarge = errors.New("git: object too large")
32+ // ErrClosed reports the use of a closed Repo.
33+ ErrClosed = errors.New("git: repository closed")
34+)
35+
36+func corrupt(format string, a ...any) error {
37+ return fmt.Errorf("%w: %s", ErrCorrupt, fmt.Sprintf(format, a...))
38+}
39+
40+func unsupported(format string, a ...any) error {
41+ return fmt.Errorf("%w: %s", ErrUnsupported, fmt.Sprintf(format, a...))
42+}
43+
44+func notFound(format string, a ...any) error {
45+ return fmt.Errorf("%w: %s", ErrNotFound, fmt.Sprintf(format, a...))
46+}
@@ -0,0 +1,46 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "errors"
7+ "fmt"
8+)
9+
10+// Errors returned by the package. They are wrapped with context, so test them
11+// with errors.Is.
12+var (
13+ // ErrNotFound reports a missing object, ref or path, or a directory that
14+ // is not a Git repository.
15+ ErrNotFound = errors.New("git: not found")
16+ // ErrUnsupported reports a repository feature the package does not read:
17+ // the error is wrapped with the reason.
18+ ErrUnsupported = errors.New("git: unsupported repository")
19+ // ErrAmbiguous reports an abbreviated object id that matches several
20+ // objects.
21+ ErrAmbiguous = errors.New("git: ambiguous object id")
22+ // ErrCorrupt reports malformed repository data. Reading never panics on
23+ // such data.
24+ ErrCorrupt = errors.New("git: corrupt data")
25+ // ErrInvalidID reports a malformed object id or abbreviation.
26+ ErrInvalidID = errors.New("git: invalid object id")
27+ // ErrWrongType reports an object that does not have the expected type.
28+ ErrWrongType = errors.New("git: wrong object type")
29+ // ErrTooLarge reports an object bigger than the configured bound
30+ // (see WithMaxObjectSize).
31+ ErrTooLarge = errors.New("git: object too large")
32+ // ErrClosed reports the use of a closed Repo.
33+ ErrClosed = errors.New("git: repository closed")
34+)
35+
36+func corrupt(format string, a ...any) error {
37+ return fmt.Errorf("%w: %s", ErrCorrupt, fmt.Sprintf(format, a...))
38+}
39+
40+func unsupported(format string, a ...any) error {
41+ return fmt.Errorf("%w: %s", ErrUnsupported, fmt.Sprintf(format, a...))
42+}
43+
44+func notFound(format string, a ...any) error {
45+ return fmt.Errorf("%w: %s", ErrNotFound, fmt.Sprintf(format, a...))
46+}
A id.go +81 -0
@@ -0,0 +1,81 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import "fmt"
6+
7+// ObjectID is a SHA-1 object id.
8+type ObjectID [20]byte
9+
10+const hexDigits = "0123456789abcdef"
11+
12+// oidHexVal maps a hex digit to its value, or 0xff.
13+var oidHexVal = func() (t [256]byte) {
14+ for i := range t {
15+ t[i] = 0xff
16+ }
17+ for i := 0; i < 10; i++ {
18+ t['0'+i] = byte(i)
19+ }
20+ for i := 0; i < 6; i++ {
21+ t['a'+i] = byte(10 + i)
22+ t['A'+i] = byte(10 + i)
23+ }
24+ return t
25+}()
26+
27+// ParseID parses 40 hexadecimal digits.
28+func ParseID(s string) (ObjectID, error) {
29+ var id ObjectID
30+ if len(s) != 40 {
31+ return id, fmt.Errorf("%w: %q has %d characters, want 40", ErrInvalidID, truncate(s), len(s))
32+ }
33+ for i := 0; i < 20; i++ {
34+ hi, lo := oidHexVal[s[2*i]], oidHexVal[s[2*i+1]]
35+ if hi|lo > 0x0f {
36+ return ObjectID{}, fmt.Errorf("%w: %q is not hexadecimal", ErrInvalidID, truncate(s))
37+ }
38+ id[i] = hi<<4 | lo
39+ }
40+ return id, nil
41+}
42+
43+// parseIDBytes parses 40 hexadecimal digits.
44+func parseIDBytes(b []byte) (id ObjectID, ok bool) {
45+ if len(b) != 40 {
46+ return id, false
47+ }
48+ for i := 0; i < 20; i++ {
49+ hi, lo := oidHexVal[b[2*i]], oidHexVal[b[2*i+1]]
50+ if hi|lo > 0x0f {
51+ return ObjectID{}, false
52+ }
53+ id[i] = hi<<4 | lo
54+ }
55+ return id, true
56+}
57+
58+// String returns the 40 lowercase hexadecimal digits of the id.
59+func (id ObjectID) String() string {
60+ var b [40]byte
61+ return string(id.AppendHex(b[:0]))
62+}
63+
64+// AppendHex appends the 40 lowercase hexadecimal digits of the id to dst.
65+// It does not allocate when dst has room.
66+func (id ObjectID) AppendHex(dst []byte) []byte {
67+ for _, c := range id {
68+ dst = append(dst, hexDigits[c>>4], hexDigits[c&0x0f])
69+ }
70+ return dst
71+}
72+
73+// IsZero reports whether the id is all zeros (the null id).
74+func (id ObjectID) IsZero() bool { return id == ObjectID{} }
75+
76+func truncate(s string) string {
77+ if len(s) > 64 {
78+ return s[:64] + "..."
79+ }
80+ return s
81+}
@@ -0,0 +1,81 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import "fmt"
6+
7+// ObjectID is a SHA-1 object id.
8+type ObjectID [20]byte
9+
10+const hexDigits = "0123456789abcdef"
11+
12+// oidHexVal maps a hex digit to its value, or 0xff.
13+var oidHexVal = func() (t [256]byte) {
14+ for i := range t {
15+ t[i] = 0xff
16+ }
17+ for i := 0; i < 10; i++ {
18+ t['0'+i] = byte(i)
19+ }
20+ for i := 0; i < 6; i++ {
21+ t['a'+i] = byte(10 + i)
22+ t['A'+i] = byte(10 + i)
23+ }
24+ return t
25+}()
26+
27+// ParseID parses 40 hexadecimal digits.
28+func ParseID(s string) (ObjectID, error) {
29+ var id ObjectID
30+ if len(s) != 40 {
31+ return id, fmt.Errorf("%w: %q has %d characters, want 40", ErrInvalidID, truncate(s), len(s))
32+ }
33+ for i := 0; i < 20; i++ {
34+ hi, lo := oidHexVal[s[2*i]], oidHexVal[s[2*i+1]]
35+ if hi|lo > 0x0f {
36+ return ObjectID{}, fmt.Errorf("%w: %q is not hexadecimal", ErrInvalidID, truncate(s))
37+ }
38+ id[i] = hi<<4 | lo
39+ }
40+ return id, nil
41+}
42+
43+// parseIDBytes parses 40 hexadecimal digits.
44+func parseIDBytes(b []byte) (id ObjectID, ok bool) {
45+ if len(b) != 40 {
46+ return id, false
47+ }
48+ for i := 0; i < 20; i++ {
49+ hi, lo := oidHexVal[b[2*i]], oidHexVal[b[2*i+1]]
50+ if hi|lo > 0x0f {
51+ return ObjectID{}, false
52+ }
53+ id[i] = hi<<4 | lo
54+ }
55+ return id, true
56+}
57+
58+// String returns the 40 lowercase hexadecimal digits of the id.
59+func (id ObjectID) String() string {
60+ var b [40]byte
61+ return string(id.AppendHex(b[:0]))
62+}
63+
64+// AppendHex appends the 40 lowercase hexadecimal digits of the id to dst.
65+// It does not allocate when dst has room.
66+func (id ObjectID) AppendHex(dst []byte) []byte {
67+ for _, c := range id {
68+ dst = append(dst, hexDigits[c>>4], hexDigits[c&0x0f])
69+ }
70+ return dst
71+}
72+
73+// IsZero reports whether the id is all zeros (the null id).
74+func (id ObjectID) IsZero() bool { return id == ObjectID{} }
75+
76+func truncate(s string) string {
77+ if len(s) > 64 {
78+ return s[:64] + "..."
79+ }
80+ return s
81+}
A idx.go +164 -0
@@ -0,0 +1,164 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bytes"
7+ "encoding/binary"
8+)
9+
10+// idxFile is a validated, read-only pack index (version 2) held in memory.
11+type idxFile struct {
12+ data []byte
13+ n uint32
14+ fanout [256]uint32
15+ names []byte // n*20 sorted object ids
16+ offs []byte // n*4 offsets
17+ large []byte // 8 byte offsets
18+ packHash []byte // 20 bytes: the checksum of the pack file
19+}
20+
21+const (
22+ idxHeaderLen = 8
23+ idxFanoutLen = 256 * 4
24+ idxTrailer = 40
25+)
26+
27+var idxMagic = []byte{0xff, 't', 'O', 'c'}
28+
29+// parseIdx validates the layout of a v2 index: sizes, fan-out monotony and
30+// the large offset table. It does not verify the checksums.
31+func parseIdx(data []byte) (*idxFile, error) {
32+ if len(data) < idxHeaderLen+idxFanoutLen+idxTrailer {
33+ return nil, corrupt("idx: file too short (%d bytes)", len(data))
34+ }
35+ if !bytes.Equal(data[:4], idxMagic) {
36+ return nil, unsupported("idx: version 1 index")
37+ }
38+ if v := binary.BigEndian.Uint32(data[4:8]); v != 2 {
39+ return nil, unsupported("idx: version %d", v)
40+ }
41+ x := &idxFile{data: data}
42+ prev := uint32(0)
43+ for i := range x.fanout {
44+ v := binary.BigEndian.Uint32(data[idxHeaderLen+4*i:])
45+ if v < prev {
46+ return nil, corrupt("idx: fan-out decreases at %#02x", i)
47+ }
48+ x.fanout[i] = v
49+ prev = v
50+ }
51+ x.n = prev
52+ n := uint64(x.n)
53+ fixed := uint64(idxHeaderLen+idxFanoutLen) + n*28 + idxTrailer
54+ if uint64(len(data)) < fixed {
55+ return nil, corrupt("idx: %d objects do not fit in %d bytes", n, len(data))
56+ }
57+ extra := uint64(len(data)) - fixed
58+ if extra%8 != 0 {
59+ return nil, corrupt("idx: large offset table of %d bytes", extra)
60+ }
61+ p := uint64(idxHeaderLen + idxFanoutLen)
62+ x.names = data[p : p+n*20]
63+ p += n * 20
64+ p += n * 4 // crc32
65+ x.offs = data[p : p+n*4]
66+ p += n * 4
67+ x.large = data[p : p+extra]
68+ p += extra
69+ x.packHash = data[p : p+20]
70+ return x, nil
71+}
72+
73+// id returns the id at position i.
74+func (x *idxFile) id(i uint32) []byte { return x.names[uint64(i)*20 : uint64(i)*20+20] }
75+
76+// find returns the position of id.
77+func (x *idxFile) find(id *ObjectID) (uint32, bool) {
78+ lo, hi := x.bounds(id[0])
79+ pos := x.lowerBound(id[:], lo, hi)
80+ if pos < hi && bytes.Equal(x.id(pos), id[:]) {
81+ return pos, true
82+ }
83+ return 0, false
84+}
85+
86+// bounds returns the range of positions whose first byte is b.
87+func (x *idxFile) bounds(b byte) (lo, hi uint32) {
88+ if b > 0 {
89+ lo = x.fanout[b-1]
90+ }
91+ return lo, x.fanout[b]
92+}
93+
94+// lowerBound returns the first position in [lo, hi) whose id is not less
95+// than key, comparing len(key) bytes.
96+func (x *idxFile) lowerBound(key []byte, lo, hi uint32) uint32 {
97+ for lo < hi {
98+ mid := lo + (hi-lo)/2
99+ if bytes.Compare(x.id(mid)[:len(key)], key) < 0 {
100+ lo = mid + 1
101+ } else {
102+ hi = mid
103+ }
104+ }
105+ return lo
106+}
107+
108+// offset returns the offset in the pack of the object at position i.
109+func (x *idxFile) offset(i uint32) (uint64, error) {
110+ v := binary.BigEndian.Uint32(x.offs[uint64(i)*4:])
111+ if v&0x80000000 == 0 {
112+ return uint64(v), nil
113+ }
114+ k := uint64(v & 0x7fffffff)
115+ if (k+1)*8 > uint64(len(x.large)) {
116+ return 0, corrupt("idx: large offset %d out of %d", k, len(x.large)/8)
117+ }
118+ return binary.BigEndian.Uint64(x.large[k*8:]), nil
119+}
120+
121+// prefixMatches calls fn with each id that starts with the nibble prefix
122+// (nibbles hex digits, packed two per byte in key), stopping when fn returns
123+// false.
124+func (x *idxFile) prefixMatches(key []byte, nibbles int, fn func(id ObjectID) bool) {
125+ var lo, hi uint32
126+ var first, last byte
127+ whole := nibbles / 2
128+ first, last = key[0], key[0]
129+ if nibbles == 1 {
130+ first, last = key[0]&0xf0, key[0]|0x0f
131+ }
132+ lo, _ = x.bounds(first)
133+ _, hi = x.bounds(last)
134+ var pad [20]byte
135+ copy(pad[:], key[:(nibbles+1)/2])
136+ if nibbles%2 == 1 {
137+ pad[whole] &= 0xf0
138+ }
139+ pos := x.lowerBound(pad[:(nibbles+1)/2], lo, hi)
140+ for ; pos < hi; pos++ {
141+ id := x.id(pos)
142+ if !hasNibblePrefix(id, key, nibbles) {
143+ return
144+ }
145+ var o ObjectID
146+ copy(o[:], id)
147+ if !fn(o) {
148+ return
149+ }
150+ }
151+}
152+
153+// hasNibblePrefix reports whether id starts with the first n hex digits
154+// encoded in key.
155+func hasNibblePrefix(id, key []byte, n int) bool {
156+ whole := n / 2
157+ if !bytes.Equal(id[:whole], key[:whole]) {
158+ return false
159+ }
160+ if n%2 == 1 {
161+ return id[whole]&0xf0 == key[whole]&0xf0
162+ }
163+ return true
164+}
@@ -0,0 +1,164 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bytes"
7+ "encoding/binary"
8+)
9+
10+// idxFile is a validated, read-only pack index (version 2) held in memory.
11+type idxFile struct {
12+ data []byte
13+ n uint32
14+ fanout [256]uint32
15+ names []byte // n*20 sorted object ids
16+ offs []byte // n*4 offsets
17+ large []byte // 8 byte offsets
18+ packHash []byte // 20 bytes: the checksum of the pack file
19+}
20+
21+const (
22+ idxHeaderLen = 8
23+ idxFanoutLen = 256 * 4
24+ idxTrailer = 40
25+)
26+
27+var idxMagic = []byte{0xff, 't', 'O', 'c'}
28+
29+// parseIdx validates the layout of a v2 index: sizes, fan-out monotony and
30+// the large offset table. It does not verify the checksums.
31+func parseIdx(data []byte) (*idxFile, error) {
32+ if len(data) < idxHeaderLen+idxFanoutLen+idxTrailer {
33+ return nil, corrupt("idx: file too short (%d bytes)", len(data))
34+ }
35+ if !bytes.Equal(data[:4], idxMagic) {
36+ return nil, unsupported("idx: version 1 index")
37+ }
38+ if v := binary.BigEndian.Uint32(data[4:8]); v != 2 {
39+ return nil, unsupported("idx: version %d", v)
40+ }
41+ x := &idxFile{data: data}
42+ prev := uint32(0)
43+ for i := range x.fanout {
44+ v := binary.BigEndian.Uint32(data[idxHeaderLen+4*i:])
45+ if v < prev {
46+ return nil, corrupt("idx: fan-out decreases at %#02x", i)
47+ }
48+ x.fanout[i] = v
49+ prev = v
50+ }
51+ x.n = prev
52+ n := uint64(x.n)
53+ fixed := uint64(idxHeaderLen+idxFanoutLen) + n*28 + idxTrailer
54+ if uint64(len(data)) < fixed {
55+ return nil, corrupt("idx: %d objects do not fit in %d bytes", n, len(data))
56+ }
57+ extra := uint64(len(data)) - fixed
58+ if extra%8 != 0 {
59+ return nil, corrupt("idx: large offset table of %d bytes", extra)
60+ }
61+ p := uint64(idxHeaderLen + idxFanoutLen)
62+ x.names = data[p : p+n*20]
63+ p += n * 20
64+ p += n * 4 // crc32
65+ x.offs = data[p : p+n*4]
66+ p += n * 4
67+ x.large = data[p : p+extra]
68+ p += extra
69+ x.packHash = data[p : p+20]
70+ return x, nil
71+}
72+
73+// id returns the id at position i.
74+func (x *idxFile) id(i uint32) []byte { return x.names[uint64(i)*20 : uint64(i)*20+20] }
75+
76+// find returns the position of id.
77+func (x *idxFile) find(id *ObjectID) (uint32, bool) {
78+ lo, hi := x.bounds(id[0])
79+ pos := x.lowerBound(id[:], lo, hi)
80+ if pos < hi && bytes.Equal(x.id(pos), id[:]) {
81+ return pos, true
82+ }
83+ return 0, false
84+}
85+
86+// bounds returns the range of positions whose first byte is b.
87+func (x *idxFile) bounds(b byte) (lo, hi uint32) {
88+ if b > 0 {
89+ lo = x.fanout[b-1]
90+ }
91+ return lo, x.fanout[b]
92+}
93+
94+// lowerBound returns the first position in [lo, hi) whose id is not less
95+// than key, comparing len(key) bytes.
96+func (x *idxFile) lowerBound(key []byte, lo, hi uint32) uint32 {
97+ for lo < hi {
98+ mid := lo + (hi-lo)/2
99+ if bytes.Compare(x.id(mid)[:len(key)], key) < 0 {
100+ lo = mid + 1
101+ } else {
102+ hi = mid
103+ }
104+ }
105+ return lo
106+}
107+
108+// offset returns the offset in the pack of the object at position i.
109+func (x *idxFile) offset(i uint32) (uint64, error) {
110+ v := binary.BigEndian.Uint32(x.offs[uint64(i)*4:])
111+ if v&0x80000000 == 0 {
112+ return uint64(v), nil
113+ }
114+ k := uint64(v & 0x7fffffff)
115+ if (k+1)*8 > uint64(len(x.large)) {
116+ return 0, corrupt("idx: large offset %d out of %d", k, len(x.large)/8)
117+ }
118+ return binary.BigEndian.Uint64(x.large[k*8:]), nil
119+}
120+
121+// prefixMatches calls fn with each id that starts with the nibble prefix
122+// (nibbles hex digits, packed two per byte in key), stopping when fn returns
123+// false.
124+func (x *idxFile) prefixMatches(key []byte, nibbles int, fn func(id ObjectID) bool) {
125+ var lo, hi uint32
126+ var first, last byte
127+ whole := nibbles / 2
128+ first, last = key[0], key[0]
129+ if nibbles == 1 {
130+ first, last = key[0]&0xf0, key[0]|0x0f
131+ }
132+ lo, _ = x.bounds(first)
133+ _, hi = x.bounds(last)
134+ var pad [20]byte
135+ copy(pad[:], key[:(nibbles+1)/2])
136+ if nibbles%2 == 1 {
137+ pad[whole] &= 0xf0
138+ }
139+ pos := x.lowerBound(pad[:(nibbles+1)/2], lo, hi)
140+ for ; pos < hi; pos++ {
141+ id := x.id(pos)
142+ if !hasNibblePrefix(id, key, nibbles) {
143+ return
144+ }
145+ var o ObjectID
146+ copy(o[:], id)
147+ if !fn(o) {
148+ return
149+ }
150+ }
151+}
152+
153+// hasNibblePrefix reports whether id starts with the first n hex digits
154+// encoded in key.
155+func hasNibblePrefix(id, key []byte, n int) bool {
156+ whole := n / 2
157+ if !bytes.Equal(id[:whole], key[:whole]) {
158+ return false
159+ }
160+ if n%2 == 1 {
161+ return id[whole]&0xf0 == key[whole]&0xf0
162+ }
163+ return true
164+}
A inflate.go +118 -0
@@ -0,0 +1,118 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bufio"
7+ "bytes"
8+ "compress/zlib"
9+ "errors"
10+ "io"
11+ "sync"
12+)
13+
14+// buffer is a pooled byte slice. Pooling the pointer keeps Put free of
15+// allocations.
16+type buffer struct{ b []byte }
17+
18+// maxPooledBuffer is the largest buffer that goes back to the pool, so a
19+// single big blob does not pin memory.
20+const maxPooledBuffer = 4 << 20
21+
22+var bufPool = sync.Pool{New: func() any { return &buffer{b: make([]byte, 0, 4096)} }}
23+
24+func getBuf() *buffer { return bufPool.Get().(*buffer) }
25+
26+func putBuf(b *buffer) {
27+ if b == nil || cap(b.b) > maxPooledBuffer {
28+ return
29+ }
30+ b.b = b.b[:0]
31+ bufPool.Put(b)
32+}
33+
34+// grow returns b with length n, reusing its capacity when possible. The
35+// content is not preserved.
36+func grow(b []byte, n int) []byte {
37+ if cap(b) >= n {
38+ return b[:n]
39+ }
40+ return make([]byte, n, n+n/4)
41+}
42+
43+// memInflater inflates a zlib stream held in memory.
44+type memInflater struct {
45+ zr io.ReadCloser
46+ br bytes.Reader
47+ scratch [maxLooseHeader]byte // for reads through the io.Reader interface, which escape
48+}
49+
50+var memPool = sync.Pool{New: func() any { return new(memInflater) }}
51+
52+// getMem returns an inflater positioned at the start of src.
53+func getMem(src []byte) (*memInflater, error) {
54+ m := memPool.Get().(*memInflater)
55+ m.br.Reset(src)
56+ var err error
57+ if m.zr == nil {
58+ m.zr, err = zlib.NewReader(&m.br)
59+ } else {
60+ err = m.zr.(zlib.Resetter).Reset(&m.br, nil)
61+ }
62+ if err != nil {
63+ putMem(m)
64+ return nil, corrupt("zlib header: %v", err)
65+ }
66+ return m, nil
67+}
68+
69+func putMem(m *memInflater) {
70+ m.br.Reset(nil)
71+ memPool.Put(m)
72+}
73+
74+// fileInflater inflates a zlib stream read from a file.
75+type fileInflater struct {
76+ zr io.ReadCloser
77+ br *bufio.Reader
78+ scratch [maxLooseHeader]byte
79+}
80+
81+var filePool = sync.Pool{New: func() any { return &fileInflater{br: bufio.NewReaderSize(nil, 8192)} }}
82+
83+func getFile(r io.Reader) (*fileInflater, error) {
84+ f := filePool.Get().(*fileInflater)
85+ f.br.Reset(r)
86+ var err error
87+ if f.zr == nil {
88+ f.zr, err = zlib.NewReader(f.br)
89+ } else {
90+ err = f.zr.(zlib.Resetter).Reset(f.br, nil)
91+ }
92+ if err != nil {
93+ putFile(f)
94+ return nil, corrupt("zlib header: %v", err)
95+ }
96+ return f, nil
97+}
98+
99+func putFile(f *fileInflater) {
100+ f.br.Reset(nil)
101+ filePool.Put(f)
102+}
103+
104+// inflateExact fills dst from zr and checks that the stream ends right
105+// after, so a stream shorter or longer than declared is corrupt.
106+func inflateExact(zr io.Reader, dst []byte, probe []byte) error {
107+ if _, err := io.ReadFull(zr, dst); err != nil {
108+ return corrupt("inflate: %v", err)
109+ }
110+ n, err := zr.Read(probe[:1])
111+ if n != 0 {
112+ return corrupt("inflate: object longer than its declared size")
113+ }
114+ if !errors.Is(err, io.EOF) {
115+ return corrupt("inflate: %v", err)
116+ }
117+ return nil
118+}
@@ -0,0 +1,118 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bufio"
7+ "bytes"
8+ "compress/zlib"
9+ "errors"
10+ "io"
11+ "sync"
12+)
13+
14+// buffer is a pooled byte slice. Pooling the pointer keeps Put free of
15+// allocations.
16+type buffer struct{ b []byte }
17+
18+// maxPooledBuffer is the largest buffer that goes back to the pool, so a
19+// single big blob does not pin memory.
20+const maxPooledBuffer = 4 << 20
21+
22+var bufPool = sync.Pool{New: func() any { return &buffer{b: make([]byte, 0, 4096)} }}
23+
24+func getBuf() *buffer { return bufPool.Get().(*buffer) }
25+
26+func putBuf(b *buffer) {
27+ if b == nil || cap(b.b) > maxPooledBuffer {
28+ return
29+ }
30+ b.b = b.b[:0]
31+ bufPool.Put(b)
32+}
33+
34+// grow returns b with length n, reusing its capacity when possible. The
35+// content is not preserved.
36+func grow(b []byte, n int) []byte {
37+ if cap(b) >= n {
38+ return b[:n]
39+ }
40+ return make([]byte, n, n+n/4)
41+}
42+
43+// memInflater inflates a zlib stream held in memory.
44+type memInflater struct {
45+ zr io.ReadCloser
46+ br bytes.Reader
47+ scratch [maxLooseHeader]byte // for reads through the io.Reader interface, which escape
48+}
49+
50+var memPool = sync.Pool{New: func() any { return new(memInflater) }}
51+
52+// getMem returns an inflater positioned at the start of src.
53+func getMem(src []byte) (*memInflater, error) {
54+ m := memPool.Get().(*memInflater)
55+ m.br.Reset(src)
56+ var err error
57+ if m.zr == nil {
58+ m.zr, err = zlib.NewReader(&m.br)
59+ } else {
60+ err = m.zr.(zlib.Resetter).Reset(&m.br, nil)
61+ }
62+ if err != nil {
63+ putMem(m)
64+ return nil, corrupt("zlib header: %v", err)
65+ }
66+ return m, nil
67+}
68+
69+func putMem(m *memInflater) {
70+ m.br.Reset(nil)
71+ memPool.Put(m)
72+}
73+
74+// fileInflater inflates a zlib stream read from a file.
75+type fileInflater struct {
76+ zr io.ReadCloser
77+ br *bufio.Reader
78+ scratch [maxLooseHeader]byte
79+}
80+
81+var filePool = sync.Pool{New: func() any { return &fileInflater{br: bufio.NewReaderSize(nil, 8192)} }}
82+
83+func getFile(r io.Reader) (*fileInflater, error) {
84+ f := filePool.Get().(*fileInflater)
85+ f.br.Reset(r)
86+ var err error
87+ if f.zr == nil {
88+ f.zr, err = zlib.NewReader(f.br)
89+ } else {
90+ err = f.zr.(zlib.Resetter).Reset(f.br, nil)
91+ }
92+ if err != nil {
93+ putFile(f)
94+ return nil, corrupt("zlib header: %v", err)
95+ }
96+ return f, nil
97+}
98+
99+func putFile(f *fileInflater) {
100+ f.br.Reset(nil)
101+ filePool.Put(f)
102+}
103+
104+// inflateExact fills dst from zr and checks that the stream ends right
105+// after, so a stream shorter or longer than declared is corrupt.
106+func inflateExact(zr io.Reader, dst []byte, probe []byte) error {
107+ if _, err := io.ReadFull(zr, dst); err != nil {
108+ return corrupt("inflate: %v", err)
109+ }
110+ n, err := zr.Read(probe[:1])
111+ if n != 0 {
112+ return corrupt("inflate: object longer than its declared size")
113+ }
114+ if !errors.Is(err, io.EOF) {
115+ return corrupt("inflate: %v", err)
116+ }
117+ return nil
118+}
A loose.go +276 -0
@@ -0,0 +1,276 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "errors"
7+ "io"
8+ "io/fs"
9+ "os"
10+ "strings"
11+ "syscall"
12+)
13+
14+// maxLooseHeader bounds "<type> <size>\x00" of a loose object.
15+const maxLooseHeader = 32
16+
17+func (r *Repo) loosePath(id *ObjectID) string {
18+ var hex [40]byte
19+ id.AppendHex(hex[:0])
20+ var arr [256]byte
21+ b := append(arr[:0], r.dir...)
22+ b = append(b, "/objects/"...)
23+ b = append(b, hex[:2]...)
24+ b = append(b, '/')
25+ b = append(b, hex[2:]...)
26+ return string(b)
27+}
28+
29+// looseObject is an opened loose object whose header was read.
30+type looseObject struct {
31+ f *os.File
32+ fi *fileInflater
33+ typ Type
34+ size uint64
35+ lead [maxLooseHeader]byte // content bytes read along with the header
36+ nlea int
37+ disk int64 // size of the file
38+}
39+
40+// openLoose opens a loose object and reads its header. It returns
41+// ErrNotFound when the object is not loose.
42+func (r *Repo) openLoose(id *ObjectID) (*looseObject, error) {
43+ f, err := os.Open(r.loosePath(id))
44+ if err != nil {
45+ if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) {
46+ return nil, ErrNotFound
47+ }
48+ return nil, err
49+ }
50+ l := &looseObject{f: f}
51+ if l.fi, err = getFile(f); err != nil {
52+ _ = f.Close()
53+ return nil, wrapLoose(id, err)
54+ }
55+ if err := l.readHeader(); err != nil {
56+ l.close()
57+ return nil, wrapLoose(id, err)
58+ }
59+ // A small file cannot hold a big object (deflate expands at most 1032
60+ // times): check it before allocating for the content. The stat is only
61+ // worth its cost for big objects.
62+ if l.size > statThreshold {
63+ st, err := f.Stat()
64+ if err != nil {
65+ l.close()
66+ return nil, err
67+ }
68+ l.disk = st.Size()
69+ if l.size/1032 > uint64(l.disk) {
70+ l.close()
71+ return nil, corrupt("loose object %s: %d bytes in a file of %d", id.String(), l.size, l.disk)
72+ }
73+ }
74+ return l, nil
75+}
76+
77+// statThreshold is the object size over which the size of the loose file is
78+// checked against the size the header announces.
79+const statThreshold = 1 << 16
80+
81+func wrapLoose(id *ObjectID, err error) error {
82+ if errors.Is(err, ErrCorrupt) {
83+ return corrupt("loose object %s: %v", id.String(), err)
84+ }
85+ return err
86+}
87+
88+func (l *looseObject) close() {
89+ if l.fi != nil {
90+ putFile(l.fi)
91+ l.fi = nil
92+ }
93+ if l.f != nil {
94+ _ = l.f.Close()
95+ l.f = nil
96+ }
97+}
98+
99+func (l *looseObject) readHeader() error {
100+ tmp := l.fi.scratch[:]
101+ have := 0
102+ nul := -1
103+ for nul < 0 {
104+ if have == len(tmp) {
105+ return corrupt("header too long")
106+ }
107+ n, err := l.fi.zr.Read(tmp[have:])
108+ for i := have; i < have+n; i++ {
109+ if tmp[i] == 0 {
110+ nul = i
111+ break
112+ }
113+ }
114+ have += n
115+ if nul < 0 && err != nil {
116+ return corrupt("truncated header: %v", err)
117+ }
118+ }
119+ typ, size, err := parseObjectHeader(tmp[:nul])
120+ if err != nil {
121+ return err
122+ }
123+ l.typ, l.size = typ, size
124+ l.nlea = copy(l.lead[:], tmp[nul+1:have])
125+ return nil
126+}
127+
128+// parseObjectHeader parses "<type> <decimal size>".
129+func parseObjectHeader(h []byte) (Type, uint64, error) {
130+ sp := -1
131+ for i, c := range h {
132+ if c == ' ' {
133+ sp = i
134+ break
135+ }
136+ }
137+ if sp < 0 {
138+ return 0, 0, corrupt("bad object header")
139+ }
140+ typ, ok := parseTypeName(h[:sp])
141+ if !ok {
142+ return 0, 0, corrupt("unknown object type %q", truncate(string(h[:sp])))
143+ }
144+ digits := h[sp+1:]
145+ if len(digits) == 0 || len(digits) > 18 {
146+ return 0, 0, corrupt("bad object size")
147+ }
148+ var size uint64
149+ for _, c := range digits {
150+ if c < '0' || c > '9' {
151+ return 0, 0, corrupt("bad object size")
152+ }
153+ size = size*10 + uint64(c-'0')
154+ }
155+ return typ, size, nil
156+}
157+
158+// readLoose reads a whole loose object into dst.
159+func (r *Repo) readLoose(id *ObjectID, dst []byte) (Type, []byte, error) {
160+ l, err := r.openLoose(id)
161+ if err != nil {
162+ return 0, nil, err
163+ }
164+ defer l.close()
165+ if l.size > uint64(r.opts.maxObject) {
166+ return 0, nil, tooLarge(l.size, r.opts.maxObject)
167+ }
168+ if uint64(l.nlea) > l.size {
169+ return 0, nil, corrupt("loose object %s: longer than its header says", id.String())
170+ }
171+ dst = grow(dst, int(l.size))
172+ copy(dst, l.lead[:l.nlea])
173+ if err := inflateExact(l.fi.zr, dst[l.nlea:], l.fi.scratch[:]); err != nil {
174+ return 0, nil, wrapLoose(id, err)
175+ }
176+ return l.typ, dst, nil
177+}
178+
179+// headerPrefix is how much of a loose file is given to the inflater to find
180+// the header. Go's flate decodes a whole 32 KiB window before returning the
181+// first byte, so feeding it the whole file would inflate most small objects
182+// for nothing. The first block header and a few literals fit in 1 KiB.
183+const headerPrefix = 1024
184+
185+// looseHeader reads the type and size of a loose object, inflating as little
186+// as possible.
187+func (r *Repo) looseHeader(id *ObjectID) (Type, int64, error) {
188+ f, err := os.Open(r.loosePath(id))
189+ if err != nil {
190+ if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) {
191+ return 0, 0, ErrNotFound
192+ }
193+ return 0, 0, err
194+ }
195+ pb := getBuf()
196+ pb.b = grow(pb.b, headerPrefix)
197+ n, rerr := io.ReadFull(f, pb.b)
198+ _ = f.Close()
199+ if rerr != nil && !errors.Is(rerr, io.ErrUnexpectedEOF) && !errors.Is(rerr, io.EOF) {
200+ putBuf(pb)
201+ return 0, 0, rerr
202+ }
203+ typ, size, ok := headerFromPrefix(pb.b[:n])
204+ putBuf(pb)
205+ if ok {
206+ return typ, int64(size), nil
207+ }
208+ // Unusual or damaged: take the full path, which reports precise errors.
209+ l, err := r.openLoose(id)
210+ if err != nil {
211+ return 0, 0, err
212+ }
213+ defer l.close()
214+ return l.typ, int64(l.size), nil
215+}
216+
217+// headerFromPrefix parses the object header from the beginning of a
218+// compressed loose object. ok is false if the header cannot be found there.
219+func headerFromPrefix(src []byte) (typ Type, size uint64, ok bool) {
220+ m, err := getMem(src)
221+ if err != nil {
222+ return 0, 0, false
223+ }
224+ defer putMem(m)
225+ have := 0
226+ for have < len(m.scratch) {
227+ n, err := m.zr.Read(m.scratch[have:])
228+ for i := have; i < have+n; i++ {
229+ if m.scratch[i] == 0 {
230+ typ, size, perr := parseObjectHeader(m.scratch[:i])
231+ return typ, size, perr == nil
232+ }
233+ }
234+ have += n
235+ if err != nil {
236+ return 0, 0, false
237+ }
238+ }
239+ return 0, 0, false
240+}
241+
242+// looseWithPrefix calls fn for the loose objects whose id starts with the
243+// hex digits in hex (at least 2 of them), until fn returns false.
244+func (r *Repo) looseWithPrefix(hex string, fn func(ObjectID) bool) error {
245+ dir := r.dir + "/objects/" + hex[:2]
246+ f, err := os.Open(dir)
247+ if err != nil {
248+ if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) {
249+ return nil
250+ }
251+ return err
252+ }
253+ defer func() { _ = f.Close() }()
254+ rest := hex[2:]
255+ for {
256+ names, err := f.Readdirnames(256)
257+ for _, n := range names {
258+ if len(n) != 38 || !strings.HasPrefix(n, rest) {
259+ continue
260+ }
261+ id, ok := parseIDBytes([]byte(hex[:2] + n))
262+ if !ok {
263+ continue // "tmp_obj_*" and other files
264+ }
265+ if !fn(id) {
266+ return nil
267+ }
268+ }
269+ if errors.Is(err, io.EOF) {
270+ return nil
271+ }
272+ if err != nil {
273+ return err
274+ }
275+ }
276+}
@@ -0,0 +1,276 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "errors"
7+ "io"
8+ "io/fs"
9+ "os"
10+ "strings"
11+ "syscall"
12+)
13+
14+// maxLooseHeader bounds "<type> <size>\x00" of a loose object.
15+const maxLooseHeader = 32
16+
17+func (r *Repo) loosePath(id *ObjectID) string {
18+ var hex [40]byte
19+ id.AppendHex(hex[:0])
20+ var arr [256]byte
21+ b := append(arr[:0], r.dir...)
22+ b = append(b, "/objects/"...)
23+ b = append(b, hex[:2]...)
24+ b = append(b, '/')
25+ b = append(b, hex[2:]...)
26+ return string(b)
27+}
28+
29+// looseObject is an opened loose object whose header was read.
30+type looseObject struct {
31+ f *os.File
32+ fi *fileInflater
33+ typ Type
34+ size uint64
35+ lead [maxLooseHeader]byte // content bytes read along with the header
36+ nlea int
37+ disk int64 // size of the file
38+}
39+
40+// openLoose opens a loose object and reads its header. It returns
41+// ErrNotFound when the object is not loose.
42+func (r *Repo) openLoose(id *ObjectID) (*looseObject, error) {
43+ f, err := os.Open(r.loosePath(id))
44+ if err != nil {
45+ if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) {
46+ return nil, ErrNotFound
47+ }
48+ return nil, err
49+ }
50+ l := &looseObject{f: f}
51+ if l.fi, err = getFile(f); err != nil {
52+ _ = f.Close()
53+ return nil, wrapLoose(id, err)
54+ }
55+ if err := l.readHeader(); err != nil {
56+ l.close()
57+ return nil, wrapLoose(id, err)
58+ }
59+ // A small file cannot hold a big object (deflate expands at most 1032
60+ // times): check it before allocating for the content. The stat is only
61+ // worth its cost for big objects.
62+ if l.size > statThreshold {
63+ st, err := f.Stat()
64+ if err != nil {
65+ l.close()
66+ return nil, err
67+ }
68+ l.disk = st.Size()
69+ if l.size/1032 > uint64(l.disk) {
70+ l.close()
71+ return nil, corrupt("loose object %s: %d bytes in a file of %d", id.String(), l.size, l.disk)
72+ }
73+ }
74+ return l, nil
75+}
76+
77+// statThreshold is the object size over which the size of the loose file is
78+// checked against the size the header announces.
79+const statThreshold = 1 << 16
80+
81+func wrapLoose(id *ObjectID, err error) error {
82+ if errors.Is(err, ErrCorrupt) {
83+ return corrupt("loose object %s: %v", id.String(), err)
84+ }
85+ return err
86+}
87+
88+func (l *looseObject) close() {
89+ if l.fi != nil {
90+ putFile(l.fi)
91+ l.fi = nil
92+ }
93+ if l.f != nil {
94+ _ = l.f.Close()
95+ l.f = nil
96+ }
97+}
98+
99+func (l *looseObject) readHeader() error {
100+ tmp := l.fi.scratch[:]
101+ have := 0
102+ nul := -1
103+ for nul < 0 {
104+ if have == len(tmp) {
105+ return corrupt("header too long")
106+ }
107+ n, err := l.fi.zr.Read(tmp[have:])
108+ for i := have; i < have+n; i++ {
109+ if tmp[i] == 0 {
110+ nul = i
111+ break
112+ }
113+ }
114+ have += n
115+ if nul < 0 && err != nil {
116+ return corrupt("truncated header: %v", err)
117+ }
118+ }
119+ typ, size, err := parseObjectHeader(tmp[:nul])
120+ if err != nil {
121+ return err
122+ }
123+ l.typ, l.size = typ, size
124+ l.nlea = copy(l.lead[:], tmp[nul+1:have])
125+ return nil
126+}
127+
128+// parseObjectHeader parses "<type> <decimal size>".
129+func parseObjectHeader(h []byte) (Type, uint64, error) {
130+ sp := -1
131+ for i, c := range h {
132+ if c == ' ' {
133+ sp = i
134+ break
135+ }
136+ }
137+ if sp < 0 {
138+ return 0, 0, corrupt("bad object header")
139+ }
140+ typ, ok := parseTypeName(h[:sp])
141+ if !ok {
142+ return 0, 0, corrupt("unknown object type %q", truncate(string(h[:sp])))
143+ }
144+ digits := h[sp+1:]
145+ if len(digits) == 0 || len(digits) > 18 {
146+ return 0, 0, corrupt("bad object size")
147+ }
148+ var size uint64
149+ for _, c := range digits {
150+ if c < '0' || c > '9' {
151+ return 0, 0, corrupt("bad object size")
152+ }
153+ size = size*10 + uint64(c-'0')
154+ }
155+ return typ, size, nil
156+}
157+
158+// readLoose reads a whole loose object into dst.
159+func (r *Repo) readLoose(id *ObjectID, dst []byte) (Type, []byte, error) {
160+ l, err := r.openLoose(id)
161+ if err != nil {
162+ return 0, nil, err
163+ }
164+ defer l.close()
165+ if l.size > uint64(r.opts.maxObject) {
166+ return 0, nil, tooLarge(l.size, r.opts.maxObject)
167+ }
168+ if uint64(l.nlea) > l.size {
169+ return 0, nil, corrupt("loose object %s: longer than its header says", id.String())
170+ }
171+ dst = grow(dst, int(l.size))
172+ copy(dst, l.lead[:l.nlea])
173+ if err := inflateExact(l.fi.zr, dst[l.nlea:], l.fi.scratch[:]); err != nil {
174+ return 0, nil, wrapLoose(id, err)
175+ }
176+ return l.typ, dst, nil
177+}
178+
179+// headerPrefix is how much of a loose file is given to the inflater to find
180+// the header. Go's flate decodes a whole 32 KiB window before returning the
181+// first byte, so feeding it the whole file would inflate most small objects
182+// for nothing. The first block header and a few literals fit in 1 KiB.
183+const headerPrefix = 1024
184+
185+// looseHeader reads the type and size of a loose object, inflating as little
186+// as possible.
187+func (r *Repo) looseHeader(id *ObjectID) (Type, int64, error) {
188+ f, err := os.Open(r.loosePath(id))
189+ if err != nil {
190+ if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) {
191+ return 0, 0, ErrNotFound
192+ }
193+ return 0, 0, err
194+ }
195+ pb := getBuf()
196+ pb.b = grow(pb.b, headerPrefix)
197+ n, rerr := io.ReadFull(f, pb.b)
198+ _ = f.Close()
199+ if rerr != nil && !errors.Is(rerr, io.ErrUnexpectedEOF) && !errors.Is(rerr, io.EOF) {
200+ putBuf(pb)
201+ return 0, 0, rerr
202+ }
203+ typ, size, ok := headerFromPrefix(pb.b[:n])
204+ putBuf(pb)
205+ if ok {
206+ return typ, int64(size), nil
207+ }
208+ // Unusual or damaged: take the full path, which reports precise errors.
209+ l, err := r.openLoose(id)
210+ if err != nil {
211+ return 0, 0, err
212+ }
213+ defer l.close()
214+ return l.typ, int64(l.size), nil
215+}
216+
217+// headerFromPrefix parses the object header from the beginning of a
218+// compressed loose object. ok is false if the header cannot be found there.
219+func headerFromPrefix(src []byte) (typ Type, size uint64, ok bool) {
220+ m, err := getMem(src)
221+ if err != nil {
222+ return 0, 0, false
223+ }
224+ defer putMem(m)
225+ have := 0
226+ for have < len(m.scratch) {
227+ n, err := m.zr.Read(m.scratch[have:])
228+ for i := have; i < have+n; i++ {
229+ if m.scratch[i] == 0 {
230+ typ, size, perr := parseObjectHeader(m.scratch[:i])
231+ return typ, size, perr == nil
232+ }
233+ }
234+ have += n
235+ if err != nil {
236+ return 0, 0, false
237+ }
238+ }
239+ return 0, 0, false
240+}
241+
242+// looseWithPrefix calls fn for the loose objects whose id starts with the
243+// hex digits in hex (at least 2 of them), until fn returns false.
244+func (r *Repo) looseWithPrefix(hex string, fn func(ObjectID) bool) error {
245+ dir := r.dir + "/objects/" + hex[:2]
246+ f, err := os.Open(dir)
247+ if err != nil {
248+ if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) {
249+ return nil
250+ }
251+ return err
252+ }
253+ defer func() { _ = f.Close() }()
254+ rest := hex[2:]
255+ for {
256+ names, err := f.Readdirnames(256)
257+ for _, n := range names {
258+ if len(n) != 38 || !strings.HasPrefix(n, rest) {
259+ continue
260+ }
261+ id, ok := parseIDBytes([]byte(hex[:2] + n))
262+ if !ok {
263+ continue // "tmp_obj_*" and other files
264+ }
265+ if !fn(id) {
266+ return nil
267+ }
268+ }
269+ if errors.Is(err, io.EOF) {
270+ return nil
271+ }
272+ if err != nil {
273+ return err
274+ }
275+ }
276+}
A mmap_linux.go +11 -0
@@ -0,0 +1,11 @@
1+// SPDX-License-Identifier: MIT
2+
3+//go:build linux
4+
5+package git
6+
7+import "syscall"
8+
9+// adviseRandom tells the kernel that lookups are random accesses, so it
10+// should not read ahead. The advice is best effort.
11+func adviseRandom(data []byte) { _ = syscall.Madvise(data, syscall.MADV_RANDOM) }
@@ -0,0 +1,11 @@
1+// SPDX-License-Identifier: MIT
2+
3+//go:build linux
4+
5+package git
6+
7+import "syscall"
8+
9+// adviseRandom tells the kernel that lookups are random accesses, so it
10+// should not read ahead. The advice is best effort.
11+func adviseRandom(data []byte) { _ = syscall.Madvise(data, syscall.MADV_RANDOM) }
A mmap_notlinux.go +7 -0
@@ -0,0 +1,7 @@
1+// SPDX-License-Identifier: MIT
2+
3+//go:build darwin || freebsd
4+
5+package git
6+
7+func adviseRandom([]byte) {}
@@ -0,0 +1,7 @@
1+// SPDX-License-Identifier: MIT
2+
3+//go:build darwin || freebsd
4+
5+package git
6+
7+func adviseRandom([]byte) {}
A mmap_other.go +24 -0
@@ -0,0 +1,24 @@
1+// SPDX-License-Identifier: MIT
2+
3+//go:build !(linux || darwin || freebsd)
4+
5+package git
6+
7+import (
8+ "io"
9+ "os"
10+)
11+
12+// mapFile reads a whole file in memory, where mmap is not available.
13+func mapFile(f *os.File, size int64) ([]byte, func() error, error) {
14+ if size <= 0 || int64(int(size)) != size {
15+ return nil, nil, corrupt("%s: unusable size %d", f.Name(), size)
16+ }
17+ data := make([]byte, size)
18+ if _, err := io.ReadFull(f, data); err != nil {
19+ return nil, nil, err
20+ }
21+ return data, func() error { return nil }, nil
22+}
23+
24+func fileIno(os.FileInfo) uint64 { return 0 }
@@ -0,0 +1,24 @@
1+// SPDX-License-Identifier: MIT
2+
3+//go:build !(linux || darwin || freebsd)
4+
5+package git
6+
7+import (
8+ "io"
9+ "os"
10+)
11+
12+// mapFile reads a whole file in memory, where mmap is not available.
13+func mapFile(f *os.File, size int64) ([]byte, func() error, error) {
14+ if size <= 0 || int64(int(size)) != size {
15+ return nil, nil, corrupt("%s: unusable size %d", f.Name(), size)
16+ }
17+ data := make([]byte, size)
18+ if _, err := io.ReadFull(f, data); err != nil {
19+ return nil, nil, err
20+ }
21+ return data, func() error { return nil }, nil
22+}
23+
24+func fileIno(os.FileInfo) uint64 { return 0 }
A mmap_unix.go +31 -0
@@ -0,0 +1,31 @@
1+// SPDX-License-Identifier: MIT
2+
3+//go:build linux || darwin || freebsd
4+
5+package git
6+
7+import (
8+ "os"
9+ "syscall"
10+)
11+
12+// mapFile maps a whole file read-only. The returned function unmaps it.
13+func mapFile(f *os.File, size int64) ([]byte, func() error, error) {
14+ if size <= 0 || int64(int(size)) != size {
15+ return nil, nil, corrupt("%s: unusable size %d", f.Name(), size)
16+ }
17+ data, err := syscall.Mmap(int(f.Fd()), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED)
18+ if err != nil {
19+ return nil, nil, &os.PathError{Op: "mmap", Path: f.Name(), Err: err}
20+ }
21+ adviseRandom(data)
22+ return data, func() error { return syscall.Munmap(data) }, nil
23+}
24+
25+// fileIno returns the inode number of a stat result, 0 when unknown.
26+func fileIno(fi os.FileInfo) uint64 {
27+ if st, ok := fi.Sys().(*syscall.Stat_t); ok {
28+ return uint64(st.Ino) //nolint:unconvert // the type differs per OS
29+ }
30+ return 0
31+}
@@ -0,0 +1,31 @@
1+// SPDX-License-Identifier: MIT
2+
3+//go:build linux || darwin || freebsd
4+
5+package git
6+
7+import (
8+ "os"
9+ "syscall"
10+)
11+
12+// mapFile maps a whole file read-only. The returned function unmaps it.
13+func mapFile(f *os.File, size int64) ([]byte, func() error, error) {
14+ if size <= 0 || int64(int(size)) != size {
15+ return nil, nil, corrupt("%s: unusable size %d", f.Name(), size)
16+ }
17+ data, err := syscall.Mmap(int(f.Fd()), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED)
18+ if err != nil {
19+ return nil, nil, &os.PathError{Op: "mmap", Path: f.Name(), Err: err}
20+ }
21+ adviseRandom(data)
22+ return data, func() error { return syscall.Munmap(data) }, nil
23+}
24+
25+// fileIno returns the inode number of a stat result, 0 when unknown.
26+func fileIno(fi os.FileInfo) uint64 {
27+ if st, ok := fi.Sys().(*syscall.Stat_t); ok {
28+ return uint64(st.Ino) //nolint:unconvert // the type differs per OS
29+ }
30+ return 0
31+}
A object.go +287 -0
@@ -0,0 +1,287 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "strings"
9+)
10+
11+// finder looks an object up in a snapshot of the packs and, when it is
12+// missing, once more after reloading the pack list: a repack or a push may
13+// have added the pack that holds it.
14+type finder struct {
15+ r *Repo
16+ s *snapshot
17+ retried bool
18+}
19+
20+func (r *Repo) finder() (finder, error) {
21+ s, err := r.acquire()
22+ return finder{r: r, s: s}, err
23+}
24+
25+func (f *finder) close() { f.s.release() }
26+
27+func (f *finder) pack(id *ObjectID) (*pack, uint64, error) { return f.s.find(id) }
28+
29+// again reloads the pack list if it changed and reports whether the lookup
30+// is worth repeating, that is whether the current snapshot differs from the
31+// one this finder used. Another goroutine may already have installed the new
32+// list, so the answer of reloadPacks alone is not enough. It returns true at
33+// most once.
34+func (f *finder) again() bool {
35+ if f.retried {
36+ return false
37+ }
38+ f.retried = true
39+ if _, err := f.r.reloadPacks(false); err != nil {
40+ return false
41+ }
42+ s, err := f.r.acquire()
43+ if err != nil {
44+ return false
45+ }
46+ if s == f.s {
47+ s.release()
48+ return false
49+ }
50+ f.s.release()
51+ f.s = s
52+ return true
53+}
54+
55+// HasObject reports whether the object exists.
56+func (r *Repo) HasObject(id ObjectID) bool {
57+ f, err := r.finder()
58+ if err != nil {
59+ return false
60+ }
61+ defer f.close()
62+ for {
63+ if p, _, err := f.pack(&id); err != nil || p != nil {
64+ return err == nil
65+ }
66+ if exists(r.loosePath(&id)) {
67+ return true
68+ }
69+ if !f.again() {
70+ return false
71+ }
72+ }
73+}
74+
75+// ObjectHeader returns the type and the size of an object without reading its
76+// content. For a deltified object the size is the result size announced by
77+// the delta header. It returns ErrNotFound if the object does not exist.
78+func (r *Repo) ObjectHeader(id ObjectID) (Type, int64, error) {
79+ f, err := r.finder()
80+ if err != nil {
81+ return 0, 0, err
82+ }
83+ defer f.close()
84+ for {
85+ p, off, err := f.pack(&id)
86+ if err != nil {
87+ return 0, 0, err
88+ }
89+ if p != nil {
90+ typ, size, err := p.header(off)
91+ if err != nil {
92+ return 0, 0, wrapPack(p.name, err)
93+ }
94+ return typ, size, nil
95+ }
96+ typ, size, err := r.looseHeader(&id)
97+ if err == nil {
98+ return typ, size, nil
99+ }
100+ if !errors.Is(err, ErrNotFound) {
101+ return 0, 0, err
102+ }
103+ if !f.again() {
104+ return 0, 0, notFound("object %s", id.String())
105+ }
106+ }
107+}
108+
109+// ReadObject reads the whole content of an object. The content is appended
110+// to dst[:0], which is grown if needed; pass the previous result to reuse its
111+// storage. Objects over the WithMaxObjectSize bound fail with ErrTooLarge.
112+func (r *Repo) ReadObject(id ObjectID, dst []byte) (Type, []byte, error) {
113+ f, err := r.finder()
114+ if err != nil {
115+ return 0, nil, err
116+ }
117+ defer f.close()
118+ for {
119+ p, off, err := f.pack(&id)
120+ if err != nil {
121+ return 0, nil, err
122+ }
123+ if p != nil {
124+ typ, data, err := r.readPacked(p, off, dst)
125+ if err != nil {
126+ return 0, nil, wrapPack(p.name, err)
127+ }
128+ return typ, data, nil
129+ }
130+ typ, data, err := r.readLoose(&id, dst)
131+ if err == nil {
132+ return typ, data, nil
133+ }
134+ if !errors.Is(err, ErrNotFound) {
135+ return 0, nil, err
136+ }
137+ if !f.again() {
138+ return 0, nil, notFound("object %s", id.String())
139+ }
140+ }
141+}
142+
143+// readTyped reads an object that must have type want into a pooled buffer.
144+// The caller returns the buffer with putBuf.
145+func (r *Repo) readTyped(id ObjectID, want Type) (*buffer, error) {
146+ b := getBuf()
147+ typ, data, err := r.ReadObject(id, b.b)
148+ if err != nil {
149+ putBuf(b)
150+ return nil, err
151+ }
152+ b.b = data
153+ if typ != want {
154+ putBuf(b)
155+ return nil, wrongType(id, typ, want)
156+ }
157+ return b, nil
158+}
159+
160+// Peel follows annotated tags from id until it reaches an object that is not
161+// a tag, and returns that object and its type. Chains of tags longer than 64
162+// are corrupt.
163+func (r *Repo) Peel(id ObjectID) (ObjectID, Type, error) {
164+ for range maxPeel {
165+ typ, _, err := r.ObjectHeader(id)
166+ if err != nil {
167+ return ObjectID{}, 0, err
168+ }
169+ if typ != TypeTag {
170+ return id, typ, nil
171+ }
172+ b, err := r.readTyped(id, TypeTag)
173+ if err != nil {
174+ return ObjectID{}, 0, err
175+ }
176+ var t Tag
177+ err = ParseTag(b.b, &t)
178+ next := t.Object
179+ putBuf(b)
180+ if err != nil {
181+ return ObjectID{}, 0, fmt.Errorf("tag %s: %w", id, err)
182+ }
183+ id = next
184+ }
185+ return ObjectID{}, 0, corrupt("more than %d nested tags", maxPeel)
186+}
187+
188+const maxPeel = 64
189+
190+// PeelToCommit follows annotated tags from id and returns the commit they
191+// designate. It returns ErrWrongType if the result is not a commit (a tag of
192+// a tree, for example).
193+func (r *Repo) PeelToCommit(id ObjectID) (ObjectID, error) {
194+ got, typ, err := r.Peel(id)
195+ if err != nil {
196+ return ObjectID{}, err
197+ }
198+ if typ != TypeCommit {
199+ return ObjectID{}, fmt.Errorf("%w: %s is a %s, not a commit", ErrWrongType, got, typ)
200+ }
201+ return got, nil
202+}
203+
204+// minPrefix is the shortest abbreviation accepted, as in Git.
205+const minPrefix = 4
206+
207+// ResolvePrefix returns the object whose id starts with the hexadecimal
208+// digits in hex (4 to 40, either case). It returns ErrNotFound when none
209+// matches and ErrAmbiguous when several do, whatever their type; see
210+// ResolveCommitPrefix. A full id must exist.
211+func (r *Repo) ResolvePrefix(hex string) (ObjectID, error) {
212+ return r.resolvePrefix(hex, nil)
213+}
214+
215+// ResolveCommitPrefix is like ResolvePrefix but only considers the objects
216+// that designate a commit: commits and tags that peel to a commit. It is what
217+// "git rev-parse <hex>^{commit}" does.
218+func (r *Repo) ResolveCommitPrefix(hex string) (ObjectID, error) {
219+ return r.resolvePrefix(hex, func(id ObjectID) bool {
220+ _, err := r.PeelToCommit(id)
221+ return err == nil
222+ })
223+}
224+
225+func (r *Repo) resolvePrefix(hex string, accept func(ObjectID) bool) (ObjectID, error) {
226+ n := len(hex)
227+ if n < minPrefix || n > 40 {
228+ return ObjectID{}, fmt.Errorf("%w: %q must have %d to 40 hexadecimal digits", ErrInvalidID, truncate(hex), minPrefix)
229+ }
230+ var key [20]byte
231+ for i := 0; i < n; i++ {
232+ v := oidHexVal[hex[i]]
233+ if v > 0x0f {
234+ return ObjectID{}, fmt.Errorf("%w: %q is not hexadecimal", ErrInvalidID, truncate(hex))
235+ }
236+ if i%2 == 0 {
237+ key[i/2] = v << 4
238+ } else {
239+ key[i/2] |= v
240+ }
241+ }
242+ hex = strings.ToLower(hex)
243+
244+ f, err := r.finder()
245+ if err != nil {
246+ return ObjectID{}, err
247+ }
248+ defer f.close()
249+ for {
250+ var found [2]ObjectID
251+ count := 0
252+ add := func(id ObjectID) bool {
253+ if count > 0 && found[0] == id {
254+ return true
255+ }
256+ if count > 1 && found[1] == id {
257+ return true
258+ }
259+ if accept != nil && !accept(id) {
260+ return true
261+ }
262+ found[count] = id
263+ count++
264+ return count < 2
265+ }
266+ for _, p := range f.s.packs {
267+ p.idx.prefixMatches(key[:], n, add)
268+ if count >= 2 {
269+ break
270+ }
271+ }
272+ if count < 2 {
273+ if err := r.looseWithPrefix(hex, add); err != nil {
274+ return ObjectID{}, err
275+ }
276+ }
277+ switch {
278+ case count >= 2:
279+ return ObjectID{}, fmt.Errorf("%w: %s matches several objects", ErrAmbiguous, hex)
280+ case count == 1:
281+ return found[0], nil
282+ }
283+ if !f.again() {
284+ return ObjectID{}, notFound("no object starts with %s", hex)
285+ }
286+ }
287+}
@@ -0,0 +1,287 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "strings"
9+)
10+
11+// finder looks an object up in a snapshot of the packs and, when it is
12+// missing, once more after reloading the pack list: a repack or a push may
13+// have added the pack that holds it.
14+type finder struct {
15+ r *Repo
16+ s *snapshot
17+ retried bool
18+}
19+
20+func (r *Repo) finder() (finder, error) {
21+ s, err := r.acquire()
22+ return finder{r: r, s: s}, err
23+}
24+
25+func (f *finder) close() { f.s.release() }
26+
27+func (f *finder) pack(id *ObjectID) (*pack, uint64, error) { return f.s.find(id) }
28+
29+// again reloads the pack list if it changed and reports whether the lookup
30+// is worth repeating, that is whether the current snapshot differs from the
31+// one this finder used. Another goroutine may already have installed the new
32+// list, so the answer of reloadPacks alone is not enough. It returns true at
33+// most once.
34+func (f *finder) again() bool {
35+ if f.retried {
36+ return false
37+ }
38+ f.retried = true
39+ if _, err := f.r.reloadPacks(false); err != nil {
40+ return false
41+ }
42+ s, err := f.r.acquire()
43+ if err != nil {
44+ return false
45+ }
46+ if s == f.s {
47+ s.release()
48+ return false
49+ }
50+ f.s.release()
51+ f.s = s
52+ return true
53+}
54+
55+// HasObject reports whether the object exists.
56+func (r *Repo) HasObject(id ObjectID) bool {
57+ f, err := r.finder()
58+ if err != nil {
59+ return false
60+ }
61+ defer f.close()
62+ for {
63+ if p, _, err := f.pack(&id); err != nil || p != nil {
64+ return err == nil
65+ }
66+ if exists(r.loosePath(&id)) {
67+ return true
68+ }
69+ if !f.again() {
70+ return false
71+ }
72+ }
73+}
74+
75+// ObjectHeader returns the type and the size of an object without reading its
76+// content. For a deltified object the size is the result size announced by
77+// the delta header. It returns ErrNotFound if the object does not exist.
78+func (r *Repo) ObjectHeader(id ObjectID) (Type, int64, error) {
79+ f, err := r.finder()
80+ if err != nil {
81+ return 0, 0, err
82+ }
83+ defer f.close()
84+ for {
85+ p, off, err := f.pack(&id)
86+ if err != nil {
87+ return 0, 0, err
88+ }
89+ if p != nil {
90+ typ, size, err := p.header(off)
91+ if err != nil {
92+ return 0, 0, wrapPack(p.name, err)
93+ }
94+ return typ, size, nil
95+ }
96+ typ, size, err := r.looseHeader(&id)
97+ if err == nil {
98+ return typ, size, nil
99+ }
100+ if !errors.Is(err, ErrNotFound) {
101+ return 0, 0, err
102+ }
103+ if !f.again() {
104+ return 0, 0, notFound("object %s", id.String())
105+ }
106+ }
107+}
108+
109+// ReadObject reads the whole content of an object. The content is appended
110+// to dst[:0], which is grown if needed; pass the previous result to reuse its
111+// storage. Objects over the WithMaxObjectSize bound fail with ErrTooLarge.
112+func (r *Repo) ReadObject(id ObjectID, dst []byte) (Type, []byte, error) {
113+ f, err := r.finder()
114+ if err != nil {
115+ return 0, nil, err
116+ }
117+ defer f.close()
118+ for {
119+ p, off, err := f.pack(&id)
120+ if err != nil {
121+ return 0, nil, err
122+ }
123+ if p != nil {
124+ typ, data, err := r.readPacked(p, off, dst)
125+ if err != nil {
126+ return 0, nil, wrapPack(p.name, err)
127+ }
128+ return typ, data, nil
129+ }
130+ typ, data, err := r.readLoose(&id, dst)
131+ if err == nil {
132+ return typ, data, nil
133+ }
134+ if !errors.Is(err, ErrNotFound) {
135+ return 0, nil, err
136+ }
137+ if !f.again() {
138+ return 0, nil, notFound("object %s", id.String())
139+ }
140+ }
141+}
142+
143+// readTyped reads an object that must have type want into a pooled buffer.
144+// The caller returns the buffer with putBuf.
145+func (r *Repo) readTyped(id ObjectID, want Type) (*buffer, error) {
146+ b := getBuf()
147+ typ, data, err := r.ReadObject(id, b.b)
148+ if err != nil {
149+ putBuf(b)
150+ return nil, err
151+ }
152+ b.b = data
153+ if typ != want {
154+ putBuf(b)
155+ return nil, wrongType(id, typ, want)
156+ }
157+ return b, nil
158+}
159+
160+// Peel follows annotated tags from id until it reaches an object that is not
161+// a tag, and returns that object and its type. Chains of tags longer than 64
162+// are corrupt.
163+func (r *Repo) Peel(id ObjectID) (ObjectID, Type, error) {
164+ for range maxPeel {
165+ typ, _, err := r.ObjectHeader(id)
166+ if err != nil {
167+ return ObjectID{}, 0, err
168+ }
169+ if typ != TypeTag {
170+ return id, typ, nil
171+ }
172+ b, err := r.readTyped(id, TypeTag)
173+ if err != nil {
174+ return ObjectID{}, 0, err
175+ }
176+ var t Tag
177+ err = ParseTag(b.b, &t)
178+ next := t.Object
179+ putBuf(b)
180+ if err != nil {
181+ return ObjectID{}, 0, fmt.Errorf("tag %s: %w", id, err)
182+ }
183+ id = next
184+ }
185+ return ObjectID{}, 0, corrupt("more than %d nested tags", maxPeel)
186+}
187+
188+const maxPeel = 64
189+
190+// PeelToCommit follows annotated tags from id and returns the commit they
191+// designate. It returns ErrWrongType if the result is not a commit (a tag of
192+// a tree, for example).
193+func (r *Repo) PeelToCommit(id ObjectID) (ObjectID, error) {
194+ got, typ, err := r.Peel(id)
195+ if err != nil {
196+ return ObjectID{}, err
197+ }
198+ if typ != TypeCommit {
199+ return ObjectID{}, fmt.Errorf("%w: %s is a %s, not a commit", ErrWrongType, got, typ)
200+ }
201+ return got, nil
202+}
203+
204+// minPrefix is the shortest abbreviation accepted, as in Git.
205+const minPrefix = 4
206+
207+// ResolvePrefix returns the object whose id starts with the hexadecimal
208+// digits in hex (4 to 40, either case). It returns ErrNotFound when none
209+// matches and ErrAmbiguous when several do, whatever their type; see
210+// ResolveCommitPrefix. A full id must exist.
211+func (r *Repo) ResolvePrefix(hex string) (ObjectID, error) {
212+ return r.resolvePrefix(hex, nil)
213+}
214+
215+// ResolveCommitPrefix is like ResolvePrefix but only considers the objects
216+// that designate a commit: commits and tags that peel to a commit. It is what
217+// "git rev-parse <hex>^{commit}" does.
218+func (r *Repo) ResolveCommitPrefix(hex string) (ObjectID, error) {
219+ return r.resolvePrefix(hex, func(id ObjectID) bool {
220+ _, err := r.PeelToCommit(id)
221+ return err == nil
222+ })
223+}
224+
225+func (r *Repo) resolvePrefix(hex string, accept func(ObjectID) bool) (ObjectID, error) {
226+ n := len(hex)
227+ if n < minPrefix || n > 40 {
228+ return ObjectID{}, fmt.Errorf("%w: %q must have %d to 40 hexadecimal digits", ErrInvalidID, truncate(hex), minPrefix)
229+ }
230+ var key [20]byte
231+ for i := 0; i < n; i++ {
232+ v := oidHexVal[hex[i]]
233+ if v > 0x0f {
234+ return ObjectID{}, fmt.Errorf("%w: %q is not hexadecimal", ErrInvalidID, truncate(hex))
235+ }
236+ if i%2 == 0 {
237+ key[i/2] = v << 4
238+ } else {
239+ key[i/2] |= v
240+ }
241+ }
242+ hex = strings.ToLower(hex)
243+
244+ f, err := r.finder()
245+ if err != nil {
246+ return ObjectID{}, err
247+ }
248+ defer f.close()
249+ for {
250+ var found [2]ObjectID
251+ count := 0
252+ add := func(id ObjectID) bool {
253+ if count > 0 && found[0] == id {
254+ return true
255+ }
256+ if count > 1 && found[1] == id {
257+ return true
258+ }
259+ if accept != nil && !accept(id) {
260+ return true
261+ }
262+ found[count] = id
263+ count++
264+ return count < 2
265+ }
266+ for _, p := range f.s.packs {
267+ p.idx.prefixMatches(key[:], n, add)
268+ if count >= 2 {
269+ break
270+ }
271+ }
272+ if count < 2 {
273+ if err := r.looseWithPrefix(hex, add); err != nil {
274+ return ObjectID{}, err
275+ }
276+ }
277+ switch {
278+ case count >= 2:
279+ return ObjectID{}, fmt.Errorf("%w: %s matches several objects", ErrAmbiguous, hex)
280+ case count == 1:
281+ return found[0], nil
282+ }
283+ if !f.again() {
284+ return ObjectID{}, notFound("no object starts with %s", hex)
285+ }
286+ }
287+}
A pack.go +447 -0
@@ -0,0 +1,447 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bytes"
7+ "encoding/binary"
8+ "io"
9+ "os"
10+ "sync/atomic"
11+)
12+
13+const (
14+ packHeaderLen = 12
15+ packTrailerLen = 20
16+
17+ objOfsDelta = 6
18+ objRefDelta = 7
19+)
20+
21+// pack is an open pack file with its index, both mapped read-only. It is
22+// shared by the snapshots that list it and unmapped when the last one is
23+// released.
24+type pack struct {
25+ id uint32
26+ name string // "pack-<hash>", without extension
27+ idx *idxFile
28+ data []byte // the whole .pack file
29+ sig fileSig
30+ unmap [2]func() error
31+ refs atomic.Int32
32+}
33+
34+func (p *pack) retain() { p.refs.Add(1) }
35+
36+func (p *pack) release() {
37+ if p.refs.Add(-1) == 0 {
38+ for _, f := range p.unmap {
39+ if f != nil {
40+ _ = f()
41+ }
42+ }
43+ p.data, p.idx = nil, nil
44+ }
45+}
46+
47+// openPack maps and validates dir/<name>.idx and dir/<name>.pack. It returns
48+// (nil, nil) when a file vanished, which happens while a repack replaces it.
49+func openPack(dir, name string, id uint32, sig fileSig) (*pack, error) {
50+ idxF, err := os.Open(dir + "/" + name + ".idx")
51+ if err != nil {
52+ return nil, ignoreMissing(err)
53+ }
54+ defer func() { _ = idxF.Close() }()
55+ packF, err := os.Open(dir + "/" + name + ".pack")
56+ if err != nil {
57+ return nil, ignoreMissing(err)
58+ }
59+ defer func() { _ = packF.Close() }()
60+ idxStat, err := idxF.Stat()
61+ if err != nil {
62+ return nil, err
63+ }
64+ packStat, err := packF.Stat()
65+ if err != nil {
66+ return nil, err
67+ }
68+ idxData, unmapIdx, err := mapFile(idxF, idxStat.Size())
69+ if err != nil {
70+ return nil, err
71+ }
72+ packData, unmapPack, err := mapFile(packF, packStat.Size())
73+ if err != nil {
74+ _ = unmapIdx()
75+ return nil, err
76+ }
77+ p := &pack{id: id, name: name, data: packData, sig: sig, unmap: [2]func() error{unmapIdx, unmapPack}}
78+ p.refs.Store(1)
79+ if p.idx, err = parseIdx(idxData); err != nil {
80+ p.release()
81+ return nil, wrapPack(name, err)
82+ }
83+ if err := p.validate(); err != nil {
84+ p.release()
85+ return nil, wrapPack(name, err)
86+ }
87+ return p, nil
88+}
89+
90+// validate checks the pack header and ties the pack to its index.
91+func (p *pack) validate() error {
92+ if len(p.data) < packHeaderLen+packTrailerLen {
93+ return corrupt("pack too short (%d bytes)", len(p.data))
94+ }
95+ if string(p.data[:4]) != "PACK" {
96+ return corrupt("bad pack signature")
97+ }
98+ if v := binary.BigEndian.Uint32(p.data[4:8]); v != 2 && v != 3 {
99+ return corrupt("pack version %d", v)
100+ }
101+ if n := binary.BigEndian.Uint32(p.data[8:12]); n != p.idx.n {
102+ return corrupt("pack holds %d objects, index %d", n, p.idx.n)
103+ }
104+ if !bytes.Equal(p.data[len(p.data)-packTrailerLen:], p.idx.packHash) {
105+ return corrupt("pack checksum differs from its index")
106+ }
107+ return nil
108+}
109+
110+func wrapPack(name string, err error) error {
111+ return &packError{name: name, err: err}
112+}
113+
114+type packError struct {
115+ name string
116+ err error
117+}
118+
119+func (e *packError) Error() string { return e.name + ": " + e.err.Error() }
120+func (e *packError) Unwrap() error { return e.err }
121+
122+// entryHdr is a parsed pack entry header.
123+type entryHdr struct {
124+ typ uint8 // 1..4, objOfsDelta or objRefDelta
125+ size uint64 // inflated size of the entry data (the delta for deltas)
126+ data uint64 // offset of the zlib stream
127+ baseOff uint64 // objOfsDelta: absolute offset of the base
128+ baseID ObjectID
129+}
130+
131+func (h *entryHdr) isDelta() bool { return h.typ == objOfsDelta || h.typ == objRefDelta }
132+
133+// parseEntryHeader parses the header of the entry at off in a whole pack
134+// file (header, entries and trailer). It never reads outside data.
135+func parseEntryHeader(data []byte, off uint64) (h entryHdr, err error) {
136+ end := uint64(len(data))
137+ if end < packHeaderLen+packTrailerLen {
138+ return h, corrupt("pack too short")
139+ }
140+ end -= packTrailerLen
141+ if off < packHeaderLen || off >= end {
142+ return h, corrupt("entry offset %d outside pack", off)
143+ }
144+ p := off
145+ c := data[p]
146+ p++
147+ h.typ = (c >> 4) & 7
148+ h.size = uint64(c & 0x0f)
149+ for shift := uint(4); c&0x80 != 0; shift += 7 {
150+ if p >= end || shift > 56 {
151+ return h, corrupt("bad entry size at %d", off)
152+ }
153+ c = data[p]
154+ p++
155+ h.size |= uint64(c&0x7f) << shift
156+ }
157+ switch h.typ {
158+ case uint8(TypeCommit), uint8(TypeTree), uint8(TypeBlob), uint8(TypeTag):
159+ case objOfsDelta:
160+ if p >= end {
161+ return h, corrupt("truncated delta offset at %d", off)
162+ }
163+ c = data[p]
164+ p++
165+ rel := uint64(c & 0x7f)
166+ for c&0x80 != 0 {
167+ if p >= end || rel >= 1<<56 {
168+ return h, corrupt("bad delta offset at %d", off)
169+ }
170+ c = data[p]
171+ p++
172+ rel = (rel+1)<<7 | uint64(c&0x7f)
173+ }
174+ if rel == 0 || rel > off-packHeaderLen {
175+ return h, corrupt("delta base offset %d outside pack at %d", rel, off)
176+ }
177+ h.baseOff = off - rel
178+ case objRefDelta:
179+ if end-p < 20 {
180+ return h, corrupt("truncated delta base id at %d", off)
181+ }
182+ copy(h.baseID[:], data[p:p+20])
183+ p += 20
184+ default:
185+ return h, corrupt("entry type %d at %d", h.typ, off)
186+ }
187+ if p >= end {
188+ return h, corrupt("entry at %d has no data", off)
189+ }
190+ h.data = p
191+ return h, nil
192+}
193+
194+// stream returns the compressed bytes of the entry.
195+func (p *pack) stream(h *entryHdr) []byte {
196+ return p.data[h.data : len(p.data)-packTrailerLen]
197+}
198+
199+// inflateEntry inflates the entry data into dst, which has the exact size
200+// announced by the header.
201+func (p *pack) inflateEntry(h *entryHdr, dst []byte) error {
202+ src := p.stream(h)
203+ // Deflate cannot expand a byte into more than 1032 bytes.
204+ if h.size/1032 > uint64(len(src)) {
205+ return corrupt("entry of %d bytes in %d compressed bytes", h.size, len(src))
206+ }
207+ m, err := getMem(src)
208+ if err != nil {
209+ return err
210+ }
211+ defer putMem(m)
212+ return inflateExact(m.zr, dst, m.scratch[:])
213+}
214+
215+// chainEnd follows the chain of delta headers from off without inflating
216+// anything. It returns the type of the base object.
217+func (p *pack) chainEnd(off uint64, h entryHdr) (Type, error) {
218+ for depth := 0; ; depth++ {
219+ if !h.isDelta() {
220+ return Type(h.typ), nil
221+ }
222+ if depth >= maxDeltaDepth {
223+ return 0, corrupt("delta chain longer than %d", maxDeltaDepth)
224+ }
225+ var err error
226+ off, err = p.baseOffset(&h, off)
227+ if err != nil {
228+ return 0, err
229+ }
230+ if h, err = parseEntryHeader(p.data, off); err != nil {
231+ return 0, err
232+ }
233+ }
234+}
235+
236+// baseOffset returns the offset of the base of a delta entry. A ref-delta
237+// base must be in the same pack, as Git requires.
238+func (p *pack) baseOffset(h *entryHdr, self uint64) (uint64, error) {
239+ if h.typ == objOfsDelta {
240+ return h.baseOff, nil
241+ }
242+ pos, ok := p.idx.find(&h.baseID)
243+ if !ok {
244+ return 0, corrupt("delta base %s not in pack %s", h.baseID.String(), p.name)
245+ }
246+ off, err := p.idx.offset(pos)
247+ if err != nil {
248+ return 0, err
249+ }
250+ if off == self {
251+ return 0, corrupt("delta is its own base at %d", self)
252+ }
253+ return off, nil
254+}
255+
256+// header returns the type and size of the object at off.
257+func (p *pack) header(off uint64) (Type, int64, error) {
258+ h, err := parseEntryHeader(p.data, off)
259+ if err != nil {
260+ return 0, 0, err
261+ }
262+ if !h.isDelta() {
263+ if h.size > 1<<62 {
264+ return 0, 0, corrupt("entry size %d", h.size)
265+ }
266+ return Type(h.typ), int64(h.size), nil
267+ }
268+ typ, err := p.chainEnd(off, h)
269+ if err != nil {
270+ return 0, 0, err
271+ }
272+ // The result size is the second varint of the delta, at its start.
273+ m, err := getMem(p.stream(&h))
274+ if err != nil {
275+ return 0, 0, err
276+ }
277+ defer putMem(m)
278+ n := min(uint64(len(m.scratch)), h.size)
279+ if _, err := io.ReadFull(m.zr, m.scratch[:n]); err != nil {
280+ return 0, 0, corrupt("delta header: %v", err)
281+ }
282+ _, rs, _, err := deltaHeader(m.scratch[:n])
283+ if err != nil {
284+ return 0, 0, err
285+ }
286+ if rs > 1<<62 {
287+ return 0, 0, corrupt("delta result size %d", rs)
288+ }
289+ return typ, int64(rs), nil
290+}
291+
292+// readPacked resolves the object at off into dst (grown as needed) and
293+// returns its type and content.
294+func (r *Repo) readPacked(p *pack, off uint64, dst []byte) (Type, []byte, error) {
295+ h, err := parseEntryHeader(p.data, off)
296+ if err != nil {
297+ return 0, nil, err
298+ }
299+ if !h.isDelta() {
300+ if h.size > uint64(r.opts.maxObject) {
301+ return 0, nil, tooLarge(h.size, r.opts.maxObject)
302+ }
303+ dst = grow(dst, int(h.size))
304+ if err := p.inflateEntry(&h, dst); err != nil {
305+ return 0, nil, err
306+ }
307+ return Type(h.typ), dst, nil
308+ }
309+
310+ // Collect the chain of deltas down to a base that is a plain object or
311+ // a cached result.
312+ type link struct {
313+ hdr entryHdr
314+ off uint64
315+ }
316+ var arr [64]link
317+ chain := arr[:0]
318+ var (
319+ baseTyp Type
320+ base []byte
321+ baseFresh bool // base is a private slice that may be cached as is
322+ scratch [2]*buffer
323+ )
324+ defer func() {
325+ putBuf(scratch[0])
326+ putBuf(scratch[1])
327+ }()
328+ cur, curHdr := off, h
329+ for {
330+ if curHdr.isDelta() {
331+ if len(chain) >= maxDeltaDepth {
332+ return 0, nil, corrupt("delta chain longer than %d", maxDeltaDepth)
333+ }
334+ chain = append(chain, link{curHdr, cur})
335+ next, err := p.baseOffset(&curHdr, cur)
336+ if err != nil {
337+ return 0, nil, err
338+ }
339+ cur = next
340+ if t, data, ok := r.cache.get(cacheKey{p.id, cur}); ok {
341+ baseTyp, base = t, data
342+ break
343+ }
344+ if curHdr, err = parseEntryHeader(p.data, cur); err != nil {
345+ return 0, nil, err
346+ }
347+ continue
348+ }
349+ if curHdr.size > uint64(r.opts.maxObject) {
350+ return 0, nil, tooLarge(curHdr.size, r.opts.maxObject)
351+ }
352+ baseTyp = Type(curHdr.typ)
353+ n := int(curHdr.size)
354+ if r.cache.fits(n) {
355+ base, baseFresh = make([]byte, n), true
356+ } else {
357+ scratch[0] = getBuf()
358+ scratch[0].b = grow(scratch[0].b, n)
359+ base = scratch[0].b
360+ }
361+ if err := p.inflateEntry(&curHdr, base); err != nil {
362+ return 0, nil, err
363+ }
364+ if baseFresh {
365+ r.cache.putOwned(cacheKey{p.id, cur}, baseTyp, base)
366+ }
367+ break
368+ }
369+
370+ delta := getBuf()
371+ defer putBuf(delta)
372+ for i := len(chain) - 1; i >= 0; i-- {
373+ l := chain[i]
374+ if l.hdr.size > uint64(r.opts.maxObject) {
375+ return 0, nil, tooLarge(l.hdr.size, r.opts.maxObject)
376+ }
377+ delta.b = grow(delta.b, int(l.hdr.size))
378+ if err := p.inflateEntry(&l.hdr, delta.b); err != nil {
379+ return 0, nil, err
380+ }
381+ bs, rs, body, err := deltaHeader(delta.b)
382+ if err != nil {
383+ return 0, nil, err
384+ }
385+ if bs != uint64(len(base)) {
386+ return 0, nil, corrupt("delta at %d expects a base of %d bytes, got %d", l.off, bs, len(base))
387+ }
388+ if rs > uint64(r.opts.maxObject) {
389+ return 0, nil, tooLarge(rs, r.opts.maxObject)
390+ }
391+ var out []byte
392+ fresh := false
393+ switch {
394+ case i == 0:
395+ out = grow(dst, int(rs))
396+ case r.cache.fits(int(rs)):
397+ out, fresh = make([]byte, rs), true
398+ default:
399+ // Write into the scratch buffer that does not hold base.
400+ k := 0
401+ if scratch[0] != nil && len(base) > 0 && len(scratch[0].b) > 0 && &scratch[0].b[0] == &base[0] {
402+ k = 1
403+ }
404+ if scratch[k] == nil {
405+ scratch[k] = getBuf()
406+ }
407+ scratch[k].b = grow(scratch[k].b, int(rs))
408+ out = scratch[k].b
409+ }
410+ if err := applyDelta(out, base, delta.b, body); err != nil {
411+ return 0, nil, err
412+ }
413+ if fresh {
414+ r.cache.putOwned(cacheKey{p.id, l.off}, baseTyp, out)
415+ }
416+ base = out
417+ }
418+ return baseTyp, base, nil
419+}
420+
421+func tooLarge(size uint64, limit int64) error {
422+ return &sizeError{size: size, limit: limit}
423+}
424+
425+type sizeError struct {
426+ size uint64
427+ limit int64
428+}
429+
430+func (e *sizeError) Error() string {
431+ return ErrTooLarge.Error() + ": " + itoa(e.size) + " bytes, limit " + itoa(uint64(e.limit))
432+}
433+func (e *sizeError) Unwrap() error { return ErrTooLarge }
434+
435+func itoa(v uint64) string {
436+ var b [20]byte
437+ i := len(b)
438+ for {
439+ i--
440+ b[i] = byte('0' + v%10)
441+ v /= 10
442+ if v == 0 {
443+ break
444+ }
445+ }
446+ return string(b[i:])
447+}
@@ -0,0 +1,447 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bytes"
7+ "encoding/binary"
8+ "io"
9+ "os"
10+ "sync/atomic"
11+)
12+
13+const (
14+ packHeaderLen = 12
15+ packTrailerLen = 20
16+
17+ objOfsDelta = 6
18+ objRefDelta = 7
19+)
20+
21+// pack is an open pack file with its index, both mapped read-only. It is
22+// shared by the snapshots that list it and unmapped when the last one is
23+// released.
24+type pack struct {
25+ id uint32
26+ name string // "pack-<hash>", without extension
27+ idx *idxFile
28+ data []byte // the whole .pack file
29+ sig fileSig
30+ unmap [2]func() error
31+ refs atomic.Int32
32+}
33+
34+func (p *pack) retain() { p.refs.Add(1) }
35+
36+func (p *pack) release() {
37+ if p.refs.Add(-1) == 0 {
38+ for _, f := range p.unmap {
39+ if f != nil {
40+ _ = f()
41+ }
42+ }
43+ p.data, p.idx = nil, nil
44+ }
45+}
46+
47+// openPack maps and validates dir/<name>.idx and dir/<name>.pack. It returns
48+// (nil, nil) when a file vanished, which happens while a repack replaces it.
49+func openPack(dir, name string, id uint32, sig fileSig) (*pack, error) {
50+ idxF, err := os.Open(dir + "/" + name + ".idx")
51+ if err != nil {
52+ return nil, ignoreMissing(err)
53+ }
54+ defer func() { _ = idxF.Close() }()
55+ packF, err := os.Open(dir + "/" + name + ".pack")
56+ if err != nil {
57+ return nil, ignoreMissing(err)
58+ }
59+ defer func() { _ = packF.Close() }()
60+ idxStat, err := idxF.Stat()
61+ if err != nil {
62+ return nil, err
63+ }
64+ packStat, err := packF.Stat()
65+ if err != nil {
66+ return nil, err
67+ }
68+ idxData, unmapIdx, err := mapFile(idxF, idxStat.Size())
69+ if err != nil {
70+ return nil, err
71+ }
72+ packData, unmapPack, err := mapFile(packF, packStat.Size())
73+ if err != nil {
74+ _ = unmapIdx()
75+ return nil, err
76+ }
77+ p := &pack{id: id, name: name, data: packData, sig: sig, unmap: [2]func() error{unmapIdx, unmapPack}}
78+ p.refs.Store(1)
79+ if p.idx, err = parseIdx(idxData); err != nil {
80+ p.release()
81+ return nil, wrapPack(name, err)
82+ }
83+ if err := p.validate(); err != nil {
84+ p.release()
85+ return nil, wrapPack(name, err)
86+ }
87+ return p, nil
88+}
89+
90+// validate checks the pack header and ties the pack to its index.
91+func (p *pack) validate() error {
92+ if len(p.data) < packHeaderLen+packTrailerLen {
93+ return corrupt("pack too short (%d bytes)", len(p.data))
94+ }
95+ if string(p.data[:4]) != "PACK" {
96+ return corrupt("bad pack signature")
97+ }
98+ if v := binary.BigEndian.Uint32(p.data[4:8]); v != 2 && v != 3 {
99+ return corrupt("pack version %d", v)
100+ }
101+ if n := binary.BigEndian.Uint32(p.data[8:12]); n != p.idx.n {
102+ return corrupt("pack holds %d objects, index %d", n, p.idx.n)
103+ }
104+ if !bytes.Equal(p.data[len(p.data)-packTrailerLen:], p.idx.packHash) {
105+ return corrupt("pack checksum differs from its index")
106+ }
107+ return nil
108+}
109+
110+func wrapPack(name string, err error) error {
111+ return &packError{name: name, err: err}
112+}
113+
114+type packError struct {
115+ name string
116+ err error
117+}
118+
119+func (e *packError) Error() string { return e.name + ": " + e.err.Error() }
120+func (e *packError) Unwrap() error { return e.err }
121+
122+// entryHdr is a parsed pack entry header.
123+type entryHdr struct {
124+ typ uint8 // 1..4, objOfsDelta or objRefDelta
125+ size uint64 // inflated size of the entry data (the delta for deltas)
126+ data uint64 // offset of the zlib stream
127+ baseOff uint64 // objOfsDelta: absolute offset of the base
128+ baseID ObjectID
129+}
130+
131+func (h *entryHdr) isDelta() bool { return h.typ == objOfsDelta || h.typ == objRefDelta }
132+
133+// parseEntryHeader parses the header of the entry at off in a whole pack
134+// file (header, entries and trailer). It never reads outside data.
135+func parseEntryHeader(data []byte, off uint64) (h entryHdr, err error) {
136+ end := uint64(len(data))
137+ if end < packHeaderLen+packTrailerLen {
138+ return h, corrupt("pack too short")
139+ }
140+ end -= packTrailerLen
141+ if off < packHeaderLen || off >= end {
142+ return h, corrupt("entry offset %d outside pack", off)
143+ }
144+ p := off
145+ c := data[p]
146+ p++
147+ h.typ = (c >> 4) & 7
148+ h.size = uint64(c & 0x0f)
149+ for shift := uint(4); c&0x80 != 0; shift += 7 {
150+ if p >= end || shift > 56 {
151+ return h, corrupt("bad entry size at %d", off)
152+ }
153+ c = data[p]
154+ p++
155+ h.size |= uint64(c&0x7f) << shift
156+ }
157+ switch h.typ {
158+ case uint8(TypeCommit), uint8(TypeTree), uint8(TypeBlob), uint8(TypeTag):
159+ case objOfsDelta:
160+ if p >= end {
161+ return h, corrupt("truncated delta offset at %d", off)
162+ }
163+ c = data[p]
164+ p++
165+ rel := uint64(c & 0x7f)
166+ for c&0x80 != 0 {
167+ if p >= end || rel >= 1<<56 {
168+ return h, corrupt("bad delta offset at %d", off)
169+ }
170+ c = data[p]
171+ p++
172+ rel = (rel+1)<<7 | uint64(c&0x7f)
173+ }
174+ if rel == 0 || rel > off-packHeaderLen {
175+ return h, corrupt("delta base offset %d outside pack at %d", rel, off)
176+ }
177+ h.baseOff = off - rel
178+ case objRefDelta:
179+ if end-p < 20 {
180+ return h, corrupt("truncated delta base id at %d", off)
181+ }
182+ copy(h.baseID[:], data[p:p+20])
183+ p += 20
184+ default:
185+ return h, corrupt("entry type %d at %d", h.typ, off)
186+ }
187+ if p >= end {
188+ return h, corrupt("entry at %d has no data", off)
189+ }
190+ h.data = p
191+ return h, nil
192+}
193+
194+// stream returns the compressed bytes of the entry.
195+func (p *pack) stream(h *entryHdr) []byte {
196+ return p.data[h.data : len(p.data)-packTrailerLen]
197+}
198+
199+// inflateEntry inflates the entry data into dst, which has the exact size
200+// announced by the header.
201+func (p *pack) inflateEntry(h *entryHdr, dst []byte) error {
202+ src := p.stream(h)
203+ // Deflate cannot expand a byte into more than 1032 bytes.
204+ if h.size/1032 > uint64(len(src)) {
205+ return corrupt("entry of %d bytes in %d compressed bytes", h.size, len(src))
206+ }
207+ m, err := getMem(src)
208+ if err != nil {
209+ return err
210+ }
211+ defer putMem(m)
212+ return inflateExact(m.zr, dst, m.scratch[:])
213+}
214+
215+// chainEnd follows the chain of delta headers from off without inflating
216+// anything. It returns the type of the base object.
217+func (p *pack) chainEnd(off uint64, h entryHdr) (Type, error) {
218+ for depth := 0; ; depth++ {
219+ if !h.isDelta() {
220+ return Type(h.typ), nil
221+ }
222+ if depth >= maxDeltaDepth {
223+ return 0, corrupt("delta chain longer than %d", maxDeltaDepth)
224+ }
225+ var err error
226+ off, err = p.baseOffset(&h, off)
227+ if err != nil {
228+ return 0, err
229+ }
230+ if h, err = parseEntryHeader(p.data, off); err != nil {
231+ return 0, err
232+ }
233+ }
234+}
235+
236+// baseOffset returns the offset of the base of a delta entry. A ref-delta
237+// base must be in the same pack, as Git requires.
238+func (p *pack) baseOffset(h *entryHdr, self uint64) (uint64, error) {
239+ if h.typ == objOfsDelta {
240+ return h.baseOff, nil
241+ }
242+ pos, ok := p.idx.find(&h.baseID)
243+ if !ok {
244+ return 0, corrupt("delta base %s not in pack %s", h.baseID.String(), p.name)
245+ }
246+ off, err := p.idx.offset(pos)
247+ if err != nil {
248+ return 0, err
249+ }
250+ if off == self {
251+ return 0, corrupt("delta is its own base at %d", self)
252+ }
253+ return off, nil
254+}
255+
256+// header returns the type and size of the object at off.
257+func (p *pack) header(off uint64) (Type, int64, error) {
258+ h, err := parseEntryHeader(p.data, off)
259+ if err != nil {
260+ return 0, 0, err
261+ }
262+ if !h.isDelta() {
263+ if h.size > 1<<62 {
264+ return 0, 0, corrupt("entry size %d", h.size)
265+ }
266+ return Type(h.typ), int64(h.size), nil
267+ }
268+ typ, err := p.chainEnd(off, h)
269+ if err != nil {
270+ return 0, 0, err
271+ }
272+ // The result size is the second varint of the delta, at its start.
273+ m, err := getMem(p.stream(&h))
274+ if err != nil {
275+ return 0, 0, err
276+ }
277+ defer putMem(m)
278+ n := min(uint64(len(m.scratch)), h.size)
279+ if _, err := io.ReadFull(m.zr, m.scratch[:n]); err != nil {
280+ return 0, 0, corrupt("delta header: %v", err)
281+ }
282+ _, rs, _, err := deltaHeader(m.scratch[:n])
283+ if err != nil {
284+ return 0, 0, err
285+ }
286+ if rs > 1<<62 {
287+ return 0, 0, corrupt("delta result size %d", rs)
288+ }
289+ return typ, int64(rs), nil
290+}
291+
292+// readPacked resolves the object at off into dst (grown as needed) and
293+// returns its type and content.
294+func (r *Repo) readPacked(p *pack, off uint64, dst []byte) (Type, []byte, error) {
295+ h, err := parseEntryHeader(p.data, off)
296+ if err != nil {
297+ return 0, nil, err
298+ }
299+ if !h.isDelta() {
300+ if h.size > uint64(r.opts.maxObject) {
301+ return 0, nil, tooLarge(h.size, r.opts.maxObject)
302+ }
303+ dst = grow(dst, int(h.size))
304+ if err := p.inflateEntry(&h, dst); err != nil {
305+ return 0, nil, err
306+ }
307+ return Type(h.typ), dst, nil
308+ }
309+
310+ // Collect the chain of deltas down to a base that is a plain object or
311+ // a cached result.
312+ type link struct {
313+ hdr entryHdr
314+ off uint64
315+ }
316+ var arr [64]link
317+ chain := arr[:0]
318+ var (
319+ baseTyp Type
320+ base []byte
321+ baseFresh bool // base is a private slice that may be cached as is
322+ scratch [2]*buffer
323+ )
324+ defer func() {
325+ putBuf(scratch[0])
326+ putBuf(scratch[1])
327+ }()
328+ cur, curHdr := off, h
329+ for {
330+ if curHdr.isDelta() {
331+ if len(chain) >= maxDeltaDepth {
332+ return 0, nil, corrupt("delta chain longer than %d", maxDeltaDepth)
333+ }
334+ chain = append(chain, link{curHdr, cur})
335+ next, err := p.baseOffset(&curHdr, cur)
336+ if err != nil {
337+ return 0, nil, err
338+ }
339+ cur = next
340+ if t, data, ok := r.cache.get(cacheKey{p.id, cur}); ok {
341+ baseTyp, base = t, data
342+ break
343+ }
344+ if curHdr, err = parseEntryHeader(p.data, cur); err != nil {
345+ return 0, nil, err
346+ }
347+ continue
348+ }
349+ if curHdr.size > uint64(r.opts.maxObject) {
350+ return 0, nil, tooLarge(curHdr.size, r.opts.maxObject)
351+ }
352+ baseTyp = Type(curHdr.typ)
353+ n := int(curHdr.size)
354+ if r.cache.fits(n) {
355+ base, baseFresh = make([]byte, n), true
356+ } else {
357+ scratch[0] = getBuf()
358+ scratch[0].b = grow(scratch[0].b, n)
359+ base = scratch[0].b
360+ }
361+ if err := p.inflateEntry(&curHdr, base); err != nil {
362+ return 0, nil, err
363+ }
364+ if baseFresh {
365+ r.cache.putOwned(cacheKey{p.id, cur}, baseTyp, base)
366+ }
367+ break
368+ }
369+
370+ delta := getBuf()
371+ defer putBuf(delta)
372+ for i := len(chain) - 1; i >= 0; i-- {
373+ l := chain[i]
374+ if l.hdr.size > uint64(r.opts.maxObject) {
375+ return 0, nil, tooLarge(l.hdr.size, r.opts.maxObject)
376+ }
377+ delta.b = grow(delta.b, int(l.hdr.size))
378+ if err := p.inflateEntry(&l.hdr, delta.b); err != nil {
379+ return 0, nil, err
380+ }
381+ bs, rs, body, err := deltaHeader(delta.b)
382+ if err != nil {
383+ return 0, nil, err
384+ }
385+ if bs != uint64(len(base)) {
386+ return 0, nil, corrupt("delta at %d expects a base of %d bytes, got %d", l.off, bs, len(base))
387+ }
388+ if rs > uint64(r.opts.maxObject) {
389+ return 0, nil, tooLarge(rs, r.opts.maxObject)
390+ }
391+ var out []byte
392+ fresh := false
393+ switch {
394+ case i == 0:
395+ out = grow(dst, int(rs))
396+ case r.cache.fits(int(rs)):
397+ out, fresh = make([]byte, rs), true
398+ default:
399+ // Write into the scratch buffer that does not hold base.
400+ k := 0
401+ if scratch[0] != nil && len(base) > 0 && len(scratch[0].b) > 0 && &scratch[0].b[0] == &base[0] {
402+ k = 1
403+ }
404+ if scratch[k] == nil {
405+ scratch[k] = getBuf()
406+ }
407+ scratch[k].b = grow(scratch[k].b, int(rs))
408+ out = scratch[k].b
409+ }
410+ if err := applyDelta(out, base, delta.b, body); err != nil {
411+ return 0, nil, err
412+ }
413+ if fresh {
414+ r.cache.putOwned(cacheKey{p.id, l.off}, baseTyp, out)
415+ }
416+ base = out
417+ }
418+ return baseTyp, base, nil
419+}
420+
421+func tooLarge(size uint64, limit int64) error {
422+ return &sizeError{size: size, limit: limit}
423+}
424+
425+type sizeError struct {
426+ size uint64
427+ limit int64
428+}
429+
430+func (e *sizeError) Error() string {
431+ return ErrTooLarge.Error() + ": " + itoa(e.size) + " bytes, limit " + itoa(uint64(e.limit))
432+}
433+func (e *sizeError) Unwrap() error { return ErrTooLarge }
434+
435+func itoa(v uint64) string {
436+ var b [20]byte
437+ i := len(b)
438+ for {
439+ i--
440+ b[i] = byte('0' + v%10)
441+ v /= 10
442+ if v == 0 {
443+ break
444+ }
445+ }
446+ return string(b[i:])
447+}
A refs.go +536 -0
@@ -0,0 +1,536 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "io"
9+ "io/fs"
10+ "iter"
11+ "os"
12+ "slices"
13+ "strings"
14+ "syscall"
15+)
16+
17+const (
18+ // maxRefFile bounds a loose ref file or HEAD.
19+ maxRefFile = 4096
20+ // maxPackedRefs bounds the packed-refs file that is loaded.
21+ maxPackedRefs = 1 << 30
22+ // maxSymrefDepth is the number of symbolic refs followed, as in Git.
23+ maxSymrefDepth = 5
24+)
25+
26+// Ref is a reference.
27+type Ref struct {
28+ // Name is the full name, such as "refs/heads/main".
29+ Name string
30+ // ID is the object the ref designates, after following symbolic refs.
31+ // It may be an annotated tag; see Repo.PeelRef.
32+ ID ObjectID
33+ // Symref is the name of the ref this one points to when it is a symbolic
34+ // ref, empty otherwise.
35+ Symref string
36+ // Peeled is the object that ID designates once annotated tags are
37+ // followed. It is only set when PeeledKnown, which is the case for refs
38+ // read from packed-refs that carry peeled values.
39+ Peeled ObjectID
40+ PeeledKnown bool
41+}
42+
43+// packedRef is an entry of packed-refs.
44+type packedRef struct {
45+ name string
46+ id ObjectID
47+ peeled ObjectID
48+ peeledKnown bool
49+}
50+
51+// packedRefs is an immutable parsed packed-refs file, sorted by name.
52+type packedRefs struct {
53+ sig fileSig
54+ refs []packedRef
55+}
56+
57+func (p *packedRefs) find(name string) (packedRef, bool) {
58+ i, ok := slices.BinarySearchFunc(p.refs, name, func(e packedRef, n string) int {
59+ return strings.Compare(e.name, n)
60+ })
61+ if !ok {
62+ return packedRef{}, false
63+ }
64+ return p.refs[i], true
65+}
66+
67+// loadPacked returns the current content of packed-refs, reparsing the file
68+// when its stat data changed (or always with force).
69+func (r *Repo) loadPacked(force bool) (*packedRefs, error) {
70+ path := r.dir + "/packed-refs"
71+ sig, err := statSig(path)
72+ if err != nil {
73+ return nil, err
74+ }
75+ if cur := r.packed.Load(); !force && cur != nil && cur.sig.same(sig) {
76+ return cur, nil
77+ }
78+ r.pmu.Lock()
79+ defer r.pmu.Unlock()
80+ f, err := os.Open(path)
81+ if errors.Is(err, fs.ErrNotExist) {
82+ p := &packedRefs{}
83+ r.packed.Store(p)
84+ return p, nil
85+ }
86+ if err != nil {
87+ return nil, err
88+ }
89+ defer func() { _ = f.Close() }()
90+ st, err := f.Stat()
91+ if err != nil {
92+ return nil, err
93+ }
94+ if st.Size() > maxPackedRefs {
95+ return nil, unsupported("packed-refs larger than %d bytes", maxPackedRefs)
96+ }
97+ data := make([]byte, st.Size())
98+ if _, err := io.ReadFull(f, data); err != nil {
99+ return nil, err
100+ }
101+ p, err := parsePackedRefs(string(data))
102+ if err != nil {
103+ return nil, err
104+ }
105+ if i, _ := slices.BinarySearchFunc(p.refs, "refs/replace/", func(e packedRef, n string) int {
106+ return strings.Compare(e.name, n)
107+ }); i < len(p.refs) && strings.HasPrefix(p.refs[i].name, "refs/replace/") {
108+ return nil, unsupported("replace refs (refs/replace in packed-refs)")
109+ }
110+ p.sig = sigOf(st)
111+ r.packed.Store(p)
112+ return p, nil
113+}
114+
115+// parsePackedRefs parses a packed-refs file. The ref names are substrings of
116+// data.
117+func parsePackedRefs(data string) (*packedRefs, error) {
118+ p := &packedRefs{}
119+ fullyPeeled, peeled := false, false
120+ first := true
121+ for len(data) > 0 {
122+ var line string
123+ line, data, _ = strings.Cut(data, "\n")
124+ line = strings.TrimSuffix(line, "\r")
125+ if line == "" {
126+ continue
127+ }
128+ switch line[0] {
129+ case '#':
130+ if first && strings.HasPrefix(line, "# pack-refs with:") {
131+ for _, tr := range strings.Fields(line[len("# pack-refs with:"):]) {
132+ switch tr {
133+ case "fully-peeled":
134+ fullyPeeled = true
135+ case "peeled":
136+ peeled = true
137+ }
138+ }
139+ }
140+ first = false
141+ continue
142+ case '^':
143+ if len(p.refs) == 0 {
144+ return nil, corrupt("packed-refs: peeled line without a ref")
145+ }
146+ id, ok := parseIDBytes([]byte(line[1:]))
147+ if !ok {
148+ return nil, corrupt("packed-refs: bad peeled line")
149+ }
150+ last := &p.refs[len(p.refs)-1]
151+ last.peeled, last.peeledKnown = id, true
152+ continue
153+ }
154+ first = false
155+ hex, name, ok := strings.Cut(line, " ")
156+ if !ok || len(hex) != 40 || name == "" {
157+ return nil, corrupt("packed-refs: bad line %q", truncate(line))
158+ }
159+ id, ok := parseIDBytes([]byte(hex))
160+ if !ok {
161+ return nil, corrupt("packed-refs: bad id in line %q", truncate(line))
162+ }
163+ e := packedRef{name: name, id: id}
164+ // Without a ^ line, a ref is known not to be a tag when the file
165+ // is fully peeled (or, for the old "peeled" trait, outside tags).
166+ if fullyPeeled || (peeled && strings.HasPrefix(name, "refs/tags/")) {
167+ e.peeled, e.peeledKnown = id, true
168+ }
169+ p.refs = append(p.refs, e)
170+ }
171+ cmp := func(a, b packedRef) int { return strings.Compare(a.name, b.name) }
172+ if !slices.IsSortedFunc(p.refs, cmp) {
173+ slices.SortStableFunc(p.refs, cmp)
174+ }
175+ return p, nil
176+}
177+
178+// validRefName rejects names that could leave the repository directory or
179+// that Git itself refuses. It is a subset of git check-ref-format.
180+func validRefName(name string) bool {
181+ if !strings.HasPrefix(name, "refs/") {
182+ return false
183+ }
184+ if name == "" || len(name) > 1024 || name[0] == '/' || name[len(name)-1] == '/' || strings.HasSuffix(name, ".lock") {
185+ return false
186+ }
187+ if strings.Contains(name, "..") || strings.Contains(name, "//") || strings.Contains(name, "@{") {
188+ return false
189+ }
190+ for _, c := range []byte(name) {
191+ if c < 0x20 || c == 0x7f || c == '\\' || c == ' ' || c == '~' || c == '^' || c == ':' || c == '?' || c == '*' || c == '[' {
192+ return false
193+ }
194+ }
195+ for comp := range strings.SplitSeq(name, "/") {
196+ if comp == "" || comp[0] == '.' || strings.HasSuffix(comp, ".lock") {
197+ return false
198+ }
199+ }
200+ return true
201+}
202+
203+// looseRef is the content of a loose ref file.
204+type looseRef struct {
205+ id ObjectID
206+ symref string
207+}
208+
209+// readLooseRef reads a loose ref. found is false when there is no such file
210+// (or it is a directory).
211+func (r *Repo) readLooseRef(name string) (lr looseRef, found bool, err error) {
212+ f, err := os.Open(r.dir + "/" + name)
213+ if err != nil {
214+ if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) {
215+ return lr, false, nil
216+ }
217+ return lr, false, err
218+ }
219+ defer func() { _ = f.Close() }()
220+ pb := getBuf()
221+ defer putBuf(pb)
222+ pb.b = grow(pb.b, maxRefFile+1)
223+ buf := pb.b
224+ n, err := io.ReadFull(f, buf)
225+ if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) {
226+ err = nil
227+ }
228+ if err != nil {
229+ var pe *fs.PathError
230+ if errors.As(err, &pe) && errors.Is(pe.Err, syscall.EISDIR) {
231+ return lr, false, nil
232+ }
233+ return lr, false, err
234+ }
235+ if n > maxRefFile {
236+ return lr, true, corrupt("ref %s is larger than %d bytes", name, maxRefFile)
237+ }
238+ lr, err = parseRefContent(buf[:n])
239+ if err != nil {
240+ return lr, true, fmt.Errorf("ref %s: %w", name, err)
241+ }
242+ return lr, true, nil
243+}
244+
245+func parseRefContent(b []byte) (looseRef, error) {
246+ s := strings.TrimRight(string(b), " \t\r\n")
247+ if target, ok := strings.CutPrefix(s, "ref:"); ok {
248+ target = strings.TrimSpace(target)
249+ if !validRef(target) {
250+ return looseRef{}, corrupt("bad symbolic ref target %q", truncate(target))
251+ }
252+ return looseRef{symref: target}, nil
253+ }
254+ if len(s) >= 40 {
255+ if id, ok := parseIDBytes([]byte(s[:40])); ok && (len(s) == 40 || s[40] == ' ' || s[40] == '\t') {
256+ return looseRef{id: id}, nil
257+ }
258+ }
259+ return looseRef{}, corrupt("bad ref content %q", truncate(s))
260+}
261+
262+// validRootRef reports whether name is a pseudo-ref of the Git directory such
263+// as HEAD or ORIG_HEAD: upper case letters and underscores only.
264+func validRootRef(name string) bool {
265+ if name == "" || len(name) > 64 {
266+ return false
267+ }
268+ for _, c := range []byte(name) {
269+ if (c < 'A' || c > 'Z') && c != '_' {
270+ return false
271+ }
272+ }
273+ return true
274+}
275+
276+// validRef accepts the names lookupRef may read.
277+func validRef(name string) bool { return validRefName(name) || validRootRef(name) }
278+
279+// lookupRef resolves name through symbolic refs. loose refs win over packed
280+// ones. The loose file is read before packed-refs, so a concurrent pack-refs
281+// (which writes packed-refs first and deletes the loose file after) is never
282+// missed.
283+func (r *Repo) lookupRef(name string) (Ref, bool, error) {
284+ if !validRef(name) {
285+ return Ref{}, false, nil
286+ }
287+ ref := Ref{Name: name}
288+ cur := name
289+ for depth := 0; ; depth++ {
290+ if depth > maxSymrefDepth {
291+ return Ref{}, false, corrupt("ref %s: symbolic ref loop", name)
292+ }
293+ lr, found, err := r.readLooseRef(cur)
294+ if err != nil {
295+ return Ref{}, false, err
296+ }
297+ if found && lr.symref != "" {
298+ if depth == 0 {
299+ ref.Symref = lr.symref
300+ }
301+ if !validRef(lr.symref) {
302+ return Ref{}, false, nil
303+ }
304+ cur = lr.symref
305+ continue
306+ }
307+ if found {
308+ ref.ID = lr.id
309+ return ref, true, nil
310+ }
311+ p, err := r.loadPacked(false)
312+ if err != nil {
313+ return Ref{}, false, err
314+ }
315+ e, ok := p.find(cur)
316+ if !ok {
317+ return Ref{}, false, nil
318+ }
319+ ref.ID = e.id
320+ if depth == 0 {
321+ ref.Peeled, ref.PeeledKnown = e.peeled, e.peeledKnown
322+ }
323+ return ref, true, nil
324+ }
325+}
326+
327+// ResolveRef returns the object the ref with this full name (such as
328+// "refs/heads/main", "refs/tags/v1" or "HEAD") designates, following
329+// symbolic refs. The result may be an annotated tag; see PeelToCommit. It
330+// returns ErrNotFound if the ref does not exist.
331+func (r *Repo) ResolveRef(name string) (ObjectID, error) {
332+ ref, ok, err := r.lookupRef(name)
333+ if err != nil {
334+ return ObjectID{}, err
335+ }
336+ if !ok {
337+ return ObjectID{}, notFound("ref %s", name)
338+ }
339+ return ref.ID, nil
340+}
341+
342+// LookupRef is like ResolveRef but returns the whole Ref.
343+func (r *Repo) LookupRef(name string) (Ref, error) {
344+ ref, ok, err := r.lookupRef(name)
345+ if err != nil {
346+ return Ref{}, err
347+ }
348+ if !ok {
349+ return Ref{}, notFound("ref %s", name)
350+ }
351+ return ref, nil
352+}
353+
354+// Head returns the full name of the ref HEAD points to ("refs/heads/main")
355+// and the object it designates. An unborn branch (the target does not exist
356+// yet) is not an error: id is then the zero ObjectID. For a detached HEAD
357+// target is empty and id is the commit.
358+func (r *Repo) Head() (target string, id ObjectID, err error) {
359+ lr, found, err := r.readLooseRef("HEAD")
360+ if err != nil {
361+ return "", ObjectID{}, err
362+ }
363+ if !found {
364+ return "", ObjectID{}, notFound("HEAD")
365+ }
366+ if lr.symref == "" {
367+ return "", lr.id, nil
368+ }
369+ ref, ok, err := r.lookupRef(lr.symref)
370+ if err != nil {
371+ return "", ObjectID{}, err
372+ }
373+ if !ok {
374+ return lr.symref, ObjectID{}, nil
375+ }
376+ return lr.symref, ref.ID, nil
377+}
378+
379+// Refs iterates over the refs whose full name starts with prefix, such as
380+// "refs/heads/" or "refs/tags/" ("" for all of them), sorted by full name.
381+// Loose refs override packed ones; the peeled lines of packed-refs are used.
382+// Symbolic refs are resolved; a ref that cannot be read is skipped. HEAD is
383+// not part of it, see Head. An error ends the iteration.
384+func (r *Repo) Refs(prefix string) iter.Seq2[Ref, error] {
385+ return func(yield func(Ref, error) bool) {
386+ refs, err := r.collectRefs(prefix)
387+ if err != nil {
388+ yield(Ref{}, err)
389+ return
390+ }
391+ for _, ref := range refs {
392+ if !yield(ref, nil) {
393+ return
394+ }
395+ }
396+ }
397+}
398+
399+func (r *Repo) collectRefs(prefix string) ([]Ref, error) {
400+ // Loose refs first, packed-refs after: see lookupRef.
401+ var dir string
402+ switch {
403+ case strings.HasPrefix(prefix, "refs/"):
404+ dir = prefix[:strings.LastIndexByte(prefix, '/')]
405+ case strings.HasPrefix("refs/", prefix):
406+ dir = "refs"
407+ default:
408+ return nil, nil
409+ }
410+ var loose []string
411+ if err := r.walkLoose(dir, prefix, &loose, 0); err != nil {
412+ return nil, err
413+ }
414+ p, err := r.loadPacked(false)
415+ if err != nil {
416+ return nil, err
417+ }
418+ names := loose
419+ start, _ := slices.BinarySearchFunc(p.refs, prefix, func(e packedRef, pre string) int {
420+ if strings.HasPrefix(e.name, pre) {
421+ return 0
422+ }
423+ return strings.Compare(e.name, pre)
424+ })
425+ // BinarySearchFunc with a prefix comparator may land inside the range:
426+ // walk back to its first element.
427+ for start > 0 && strings.HasPrefix(p.refs[start-1].name, prefix) {
428+ start--
429+ }
430+ for i := start; i < len(p.refs) && strings.HasPrefix(p.refs[i].name, prefix); i++ {
431+ names = append(names, p.refs[i].name)
432+ }
433+ slices.Sort(names)
434+ names = slices.Compact(names)
435+
436+ out := make([]Ref, 0, len(names))
437+ for _, n := range names {
438+ ref, ok, err := r.lookupRef(n)
439+ if err != nil {
440+ if errors.Is(err, ErrCorrupt) {
441+ continue // Git ignores broken refs when listing
442+ }
443+ return nil, err
444+ }
445+ if ok {
446+ ref.Name = n
447+ out = append(out, ref)
448+ }
449+ }
450+ return out, nil
451+}
452+
453+// maxRefDirDepth bounds the recursion into refs directories.
454+const maxRefDirDepth = 64
455+
456+func (r *Repo) walkLoose(dir, prefix string, out *[]string, depth int) error {
457+ if depth > maxRefDirDepth {
458+ return nil
459+ }
460+ entries, err := os.ReadDir(r.dir + "/" + dir)
461+ if err != nil {
462+ if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) {
463+ return nil
464+ }
465+ return err
466+ }
467+ for _, e := range entries {
468+ name := dir + "/" + e.Name()
469+ if e.IsDir() {
470+ // Descend only where the prefix can match.
471+ if strings.HasPrefix(name+"/", prefix) || strings.HasPrefix(prefix, name+"/") {
472+ if err := r.walkLoose(name, prefix, out, depth+1); err != nil {
473+ return err
474+ }
475+ }
476+ continue
477+ }
478+ if strings.HasPrefix(name, prefix) && validRefName(name) {
479+ *out = append(*out, name)
480+ }
481+ }
482+ return nil
483+}
484+
485+// PeelRef returns the commit a ref designates, following annotated tags. It
486+// uses the peeled value of packed-refs when the ref carries one. It returns
487+// ErrWrongType if the ref designates something else than a commit.
488+func (r *Repo) PeelRef(ref Ref) (ObjectID, error) {
489+ if ref.PeeledKnown {
490+ typ, _, err := r.ObjectHeader(ref.Peeled)
491+ if err != nil {
492+ return ObjectID{}, err
493+ }
494+ if typ != TypeCommit {
495+ return ObjectID{}, wrongType(ref.Peeled, typ, TypeCommit)
496+ }
497+ return ref.Peeled, nil
498+ }
499+ return r.PeelToCommit(ref.ID)
500+}
501+
502+// Resolve turns a revision name into an object id the way "git rev-parse"
503+// does for plain names: a full 40 digit id, "HEAD", a full ref name, a short
504+// ref name tried as "<n>", "refs/<n>", "refs/tags/<n>", "refs/heads/<n>",
505+// "refs/remotes/<n>" and "refs/remotes/<n>/HEAD" in that order, and finally
506+// an abbreviated id of 4 to 39 digits. Revision operators (~, ^, @{}) are not
507+// supported. The result is not peeled; use PeelToCommit.
508+func (r *Repo) Resolve(rev string) (ObjectID, error) {
509+ if rev == "" {
510+ return ObjectID{}, notFound("empty revision")
511+ }
512+ if len(rev) == 40 {
513+ if id, err := ParseID(rev); err == nil {
514+ if !r.HasObject(id) {
515+ return ObjectID{}, notFound("object %s", id)
516+ }
517+ return id, nil
518+ }
519+ }
520+ for _, pat := range [...]string{"%s", "refs/%s", "refs/tags/%s", "refs/heads/%s", "refs/remotes/%s", "refs/remotes/%s/HEAD"} {
521+ ref, ok, err := r.lookupRef(fmt.Sprintf(pat, rev))
522+ if err != nil {
523+ return ObjectID{}, err
524+ }
525+ if ok {
526+ return ref.ID, nil
527+ }
528+ }
529+ if len(rev) >= minPrefix && len(rev) < 40 {
530+ id, err := r.ResolvePrefix(rev)
531+ if err == nil || !errors.Is(err, ErrInvalidID) {
532+ return id, err
533+ }
534+ }
535+ return ObjectID{}, notFound("revision %q", truncate(rev))
536+}
@@ -0,0 +1,536 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "io"
9+ "io/fs"
10+ "iter"
11+ "os"
12+ "slices"
13+ "strings"
14+ "syscall"
15+)
16+
17+const (
18+ // maxRefFile bounds a loose ref file or HEAD.
19+ maxRefFile = 4096
20+ // maxPackedRefs bounds the packed-refs file that is loaded.
21+ maxPackedRefs = 1 << 30
22+ // maxSymrefDepth is the number of symbolic refs followed, as in Git.
23+ maxSymrefDepth = 5
24+)
25+
26+// Ref is a reference.
27+type Ref struct {
28+ // Name is the full name, such as "refs/heads/main".
29+ Name string
30+ // ID is the object the ref designates, after following symbolic refs.
31+ // It may be an annotated tag; see Repo.PeelRef.
32+ ID ObjectID
33+ // Symref is the name of the ref this one points to when it is a symbolic
34+ // ref, empty otherwise.
35+ Symref string
36+ // Peeled is the object that ID designates once annotated tags are
37+ // followed. It is only set when PeeledKnown, which is the case for refs
38+ // read from packed-refs that carry peeled values.
39+ Peeled ObjectID
40+ PeeledKnown bool
41+}
42+
43+// packedRef is an entry of packed-refs.
44+type packedRef struct {
45+ name string
46+ id ObjectID
47+ peeled ObjectID
48+ peeledKnown bool
49+}
50+
51+// packedRefs is an immutable parsed packed-refs file, sorted by name.
52+type packedRefs struct {
53+ sig fileSig
54+ refs []packedRef
55+}
56+
57+func (p *packedRefs) find(name string) (packedRef, bool) {
58+ i, ok := slices.BinarySearchFunc(p.refs, name, func(e packedRef, n string) int {
59+ return strings.Compare(e.name, n)
60+ })
61+ if !ok {
62+ return packedRef{}, false
63+ }
64+ return p.refs[i], true
65+}
66+
67+// loadPacked returns the current content of packed-refs, reparsing the file
68+// when its stat data changed (or always with force).
69+func (r *Repo) loadPacked(force bool) (*packedRefs, error) {
70+ path := r.dir + "/packed-refs"
71+ sig, err := statSig(path)
72+ if err != nil {
73+ return nil, err
74+ }
75+ if cur := r.packed.Load(); !force && cur != nil && cur.sig.same(sig) {
76+ return cur, nil
77+ }
78+ r.pmu.Lock()
79+ defer r.pmu.Unlock()
80+ f, err := os.Open(path)
81+ if errors.Is(err, fs.ErrNotExist) {
82+ p := &packedRefs{}
83+ r.packed.Store(p)
84+ return p, nil
85+ }
86+ if err != nil {
87+ return nil, err
88+ }
89+ defer func() { _ = f.Close() }()
90+ st, err := f.Stat()
91+ if err != nil {
92+ return nil, err
93+ }
94+ if st.Size() > maxPackedRefs {
95+ return nil, unsupported("packed-refs larger than %d bytes", maxPackedRefs)
96+ }
97+ data := make([]byte, st.Size())
98+ if _, err := io.ReadFull(f, data); err != nil {
99+ return nil, err
100+ }
101+ p, err := parsePackedRefs(string(data))
102+ if err != nil {
103+ return nil, err
104+ }
105+ if i, _ := slices.BinarySearchFunc(p.refs, "refs/replace/", func(e packedRef, n string) int {
106+ return strings.Compare(e.name, n)
107+ }); i < len(p.refs) && strings.HasPrefix(p.refs[i].name, "refs/replace/") {
108+ return nil, unsupported("replace refs (refs/replace in packed-refs)")
109+ }
110+ p.sig = sigOf(st)
111+ r.packed.Store(p)
112+ return p, nil
113+}
114+
115+// parsePackedRefs parses a packed-refs file. The ref names are substrings of
116+// data.
117+func parsePackedRefs(data string) (*packedRefs, error) {
118+ p := &packedRefs{}
119+ fullyPeeled, peeled := false, false
120+ first := true
121+ for len(data) > 0 {
122+ var line string
123+ line, data, _ = strings.Cut(data, "\n")
124+ line = strings.TrimSuffix(line, "\r")
125+ if line == "" {
126+ continue
127+ }
128+ switch line[0] {
129+ case '#':
130+ if first && strings.HasPrefix(line, "# pack-refs with:") {
131+ for _, tr := range strings.Fields(line[len("# pack-refs with:"):]) {
132+ switch tr {
133+ case "fully-peeled":
134+ fullyPeeled = true
135+ case "peeled":
136+ peeled = true
137+ }
138+ }
139+ }
140+ first = false
141+ continue
142+ case '^':
143+ if len(p.refs) == 0 {
144+ return nil, corrupt("packed-refs: peeled line without a ref")
145+ }
146+ id, ok := parseIDBytes([]byte(line[1:]))
147+ if !ok {
148+ return nil, corrupt("packed-refs: bad peeled line")
149+ }
150+ last := &p.refs[len(p.refs)-1]
151+ last.peeled, last.peeledKnown = id, true
152+ continue
153+ }
154+ first = false
155+ hex, name, ok := strings.Cut(line, " ")
156+ if !ok || len(hex) != 40 || name == "" {
157+ return nil, corrupt("packed-refs: bad line %q", truncate(line))
158+ }
159+ id, ok := parseIDBytes([]byte(hex))
160+ if !ok {
161+ return nil, corrupt("packed-refs: bad id in line %q", truncate(line))
162+ }
163+ e := packedRef{name: name, id: id}
164+ // Without a ^ line, a ref is known not to be a tag when the file
165+ // is fully peeled (or, for the old "peeled" trait, outside tags).
166+ if fullyPeeled || (peeled && strings.HasPrefix(name, "refs/tags/")) {
167+ e.peeled, e.peeledKnown = id, true
168+ }
169+ p.refs = append(p.refs, e)
170+ }
171+ cmp := func(a, b packedRef) int { return strings.Compare(a.name, b.name) }
172+ if !slices.IsSortedFunc(p.refs, cmp) {
173+ slices.SortStableFunc(p.refs, cmp)
174+ }
175+ return p, nil
176+}
177+
178+// validRefName rejects names that could leave the repository directory or
179+// that Git itself refuses. It is a subset of git check-ref-format.
180+func validRefName(name string) bool {
181+ if !strings.HasPrefix(name, "refs/") {
182+ return false
183+ }
184+ if name == "" || len(name) > 1024 || name[0] == '/' || name[len(name)-1] == '/' || strings.HasSuffix(name, ".lock") {
185+ return false
186+ }
187+ if strings.Contains(name, "..") || strings.Contains(name, "//") || strings.Contains(name, "@{") {
188+ return false
189+ }
190+ for _, c := range []byte(name) {
191+ if c < 0x20 || c == 0x7f || c == '\\' || c == ' ' || c == '~' || c == '^' || c == ':' || c == '?' || c == '*' || c == '[' {
192+ return false
193+ }
194+ }
195+ for comp := range strings.SplitSeq(name, "/") {
196+ if comp == "" || comp[0] == '.' || strings.HasSuffix(comp, ".lock") {
197+ return false
198+ }
199+ }
200+ return true
201+}
202+
203+// looseRef is the content of a loose ref file.
204+type looseRef struct {
205+ id ObjectID
206+ symref string
207+}
208+
209+// readLooseRef reads a loose ref. found is false when there is no such file
210+// (or it is a directory).
211+func (r *Repo) readLooseRef(name string) (lr looseRef, found bool, err error) {
212+ f, err := os.Open(r.dir + "/" + name)
213+ if err != nil {
214+ if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) {
215+ return lr, false, nil
216+ }
217+ return lr, false, err
218+ }
219+ defer func() { _ = f.Close() }()
220+ pb := getBuf()
221+ defer putBuf(pb)
222+ pb.b = grow(pb.b, maxRefFile+1)
223+ buf := pb.b
224+ n, err := io.ReadFull(f, buf)
225+ if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) {
226+ err = nil
227+ }
228+ if err != nil {
229+ var pe *fs.PathError
230+ if errors.As(err, &pe) && errors.Is(pe.Err, syscall.EISDIR) {
231+ return lr, false, nil
232+ }
233+ return lr, false, err
234+ }
235+ if n > maxRefFile {
236+ return lr, true, corrupt("ref %s is larger than %d bytes", name, maxRefFile)
237+ }
238+ lr, err = parseRefContent(buf[:n])
239+ if err != nil {
240+ return lr, true, fmt.Errorf("ref %s: %w", name, err)
241+ }
242+ return lr, true, nil
243+}
244+
245+func parseRefContent(b []byte) (looseRef, error) {
246+ s := strings.TrimRight(string(b), " \t\r\n")
247+ if target, ok := strings.CutPrefix(s, "ref:"); ok {
248+ target = strings.TrimSpace(target)
249+ if !validRef(target) {
250+ return looseRef{}, corrupt("bad symbolic ref target %q", truncate(target))
251+ }
252+ return looseRef{symref: target}, nil
253+ }
254+ if len(s) >= 40 {
255+ if id, ok := parseIDBytes([]byte(s[:40])); ok && (len(s) == 40 || s[40] == ' ' || s[40] == '\t') {
256+ return looseRef{id: id}, nil
257+ }
258+ }
259+ return looseRef{}, corrupt("bad ref content %q", truncate(s))
260+}
261+
262+// validRootRef reports whether name is a pseudo-ref of the Git directory such
263+// as HEAD or ORIG_HEAD: upper case letters and underscores only.
264+func validRootRef(name string) bool {
265+ if name == "" || len(name) > 64 {
266+ return false
267+ }
268+ for _, c := range []byte(name) {
269+ if (c < 'A' || c > 'Z') && c != '_' {
270+ return false
271+ }
272+ }
273+ return true
274+}
275+
276+// validRef accepts the names lookupRef may read.
277+func validRef(name string) bool { return validRefName(name) || validRootRef(name) }
278+
279+// lookupRef resolves name through symbolic refs. loose refs win over packed
280+// ones. The loose file is read before packed-refs, so a concurrent pack-refs
281+// (which writes packed-refs first and deletes the loose file after) is never
282+// missed.
283+func (r *Repo) lookupRef(name string) (Ref, bool, error) {
284+ if !validRef(name) {
285+ return Ref{}, false, nil
286+ }
287+ ref := Ref{Name: name}
288+ cur := name
289+ for depth := 0; ; depth++ {
290+ if depth > maxSymrefDepth {
291+ return Ref{}, false, corrupt("ref %s: symbolic ref loop", name)
292+ }
293+ lr, found, err := r.readLooseRef(cur)
294+ if err != nil {
295+ return Ref{}, false, err
296+ }
297+ if found && lr.symref != "" {
298+ if depth == 0 {
299+ ref.Symref = lr.symref
300+ }
301+ if !validRef(lr.symref) {
302+ return Ref{}, false, nil
303+ }
304+ cur = lr.symref
305+ continue
306+ }
307+ if found {
308+ ref.ID = lr.id
309+ return ref, true, nil
310+ }
311+ p, err := r.loadPacked(false)
312+ if err != nil {
313+ return Ref{}, false, err
314+ }
315+ e, ok := p.find(cur)
316+ if !ok {
317+ return Ref{}, false, nil
318+ }
319+ ref.ID = e.id
320+ if depth == 0 {
321+ ref.Peeled, ref.PeeledKnown = e.peeled, e.peeledKnown
322+ }
323+ return ref, true, nil
324+ }
325+}
326+
327+// ResolveRef returns the object the ref with this full name (such as
328+// "refs/heads/main", "refs/tags/v1" or "HEAD") designates, following
329+// symbolic refs. The result may be an annotated tag; see PeelToCommit. It
330+// returns ErrNotFound if the ref does not exist.
331+func (r *Repo) ResolveRef(name string) (ObjectID, error) {
332+ ref, ok, err := r.lookupRef(name)
333+ if err != nil {
334+ return ObjectID{}, err
335+ }
336+ if !ok {
337+ return ObjectID{}, notFound("ref %s", name)
338+ }
339+ return ref.ID, nil
340+}
341+
342+// LookupRef is like ResolveRef but returns the whole Ref.
343+func (r *Repo) LookupRef(name string) (Ref, error) {
344+ ref, ok, err := r.lookupRef(name)
345+ if err != nil {
346+ return Ref{}, err
347+ }
348+ if !ok {
349+ return Ref{}, notFound("ref %s", name)
350+ }
351+ return ref, nil
352+}
353+
354+// Head returns the full name of the ref HEAD points to ("refs/heads/main")
355+// and the object it designates. An unborn branch (the target does not exist
356+// yet) is not an error: id is then the zero ObjectID. For a detached HEAD
357+// target is empty and id is the commit.
358+func (r *Repo) Head() (target string, id ObjectID, err error) {
359+ lr, found, err := r.readLooseRef("HEAD")
360+ if err != nil {
361+ return "", ObjectID{}, err
362+ }
363+ if !found {
364+ return "", ObjectID{}, notFound("HEAD")
365+ }
366+ if lr.symref == "" {
367+ return "", lr.id, nil
368+ }
369+ ref, ok, err := r.lookupRef(lr.symref)
370+ if err != nil {
371+ return "", ObjectID{}, err
372+ }
373+ if !ok {
374+ return lr.symref, ObjectID{}, nil
375+ }
376+ return lr.symref, ref.ID, nil
377+}
378+
379+// Refs iterates over the refs whose full name starts with prefix, such as
380+// "refs/heads/" or "refs/tags/" ("" for all of them), sorted by full name.
381+// Loose refs override packed ones; the peeled lines of packed-refs are used.
382+// Symbolic refs are resolved; a ref that cannot be read is skipped. HEAD is
383+// not part of it, see Head. An error ends the iteration.
384+func (r *Repo) Refs(prefix string) iter.Seq2[Ref, error] {
385+ return func(yield func(Ref, error) bool) {
386+ refs, err := r.collectRefs(prefix)
387+ if err != nil {
388+ yield(Ref{}, err)
389+ return
390+ }
391+ for _, ref := range refs {
392+ if !yield(ref, nil) {
393+ return
394+ }
395+ }
396+ }
397+}
398+
399+func (r *Repo) collectRefs(prefix string) ([]Ref, error) {
400+ // Loose refs first, packed-refs after: see lookupRef.
401+ var dir string
402+ switch {
403+ case strings.HasPrefix(prefix, "refs/"):
404+ dir = prefix[:strings.LastIndexByte(prefix, '/')]
405+ case strings.HasPrefix("refs/", prefix):
406+ dir = "refs"
407+ default:
408+ return nil, nil
409+ }
410+ var loose []string
411+ if err := r.walkLoose(dir, prefix, &loose, 0); err != nil {
412+ return nil, err
413+ }
414+ p, err := r.loadPacked(false)
415+ if err != nil {
416+ return nil, err
417+ }
418+ names := loose
419+ start, _ := slices.BinarySearchFunc(p.refs, prefix, func(e packedRef, pre string) int {
420+ if strings.HasPrefix(e.name, pre) {
421+ return 0
422+ }
423+ return strings.Compare(e.name, pre)
424+ })
425+ // BinarySearchFunc with a prefix comparator may land inside the range:
426+ // walk back to its first element.
427+ for start > 0 && strings.HasPrefix(p.refs[start-1].name, prefix) {
428+ start--
429+ }
430+ for i := start; i < len(p.refs) && strings.HasPrefix(p.refs[i].name, prefix); i++ {
431+ names = append(names, p.refs[i].name)
432+ }
433+ slices.Sort(names)
434+ names = slices.Compact(names)
435+
436+ out := make([]Ref, 0, len(names))
437+ for _, n := range names {
438+ ref, ok, err := r.lookupRef(n)
439+ if err != nil {
440+ if errors.Is(err, ErrCorrupt) {
441+ continue // Git ignores broken refs when listing
442+ }
443+ return nil, err
444+ }
445+ if ok {
446+ ref.Name = n
447+ out = append(out, ref)
448+ }
449+ }
450+ return out, nil
451+}
452+
453+// maxRefDirDepth bounds the recursion into refs directories.
454+const maxRefDirDepth = 64
455+
456+func (r *Repo) walkLoose(dir, prefix string, out *[]string, depth int) error {
457+ if depth > maxRefDirDepth {
458+ return nil
459+ }
460+ entries, err := os.ReadDir(r.dir + "/" + dir)
461+ if err != nil {
462+ if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) {
463+ return nil
464+ }
465+ return err
466+ }
467+ for _, e := range entries {
468+ name := dir + "/" + e.Name()
469+ if e.IsDir() {
470+ // Descend only where the prefix can match.
471+ if strings.HasPrefix(name+"/", prefix) || strings.HasPrefix(prefix, name+"/") {
472+ if err := r.walkLoose(name, prefix, out, depth+1); err != nil {
473+ return err
474+ }
475+ }
476+ continue
477+ }
478+ if strings.HasPrefix(name, prefix) && validRefName(name) {
479+ *out = append(*out, name)
480+ }
481+ }
482+ return nil
483+}
484+
485+// PeelRef returns the commit a ref designates, following annotated tags. It
486+// uses the peeled value of packed-refs when the ref carries one. It returns
487+// ErrWrongType if the ref designates something else than a commit.
488+func (r *Repo) PeelRef(ref Ref) (ObjectID, error) {
489+ if ref.PeeledKnown {
490+ typ, _, err := r.ObjectHeader(ref.Peeled)
491+ if err != nil {
492+ return ObjectID{}, err
493+ }
494+ if typ != TypeCommit {
495+ return ObjectID{}, wrongType(ref.Peeled, typ, TypeCommit)
496+ }
497+ return ref.Peeled, nil
498+ }
499+ return r.PeelToCommit(ref.ID)
500+}
501+
502+// Resolve turns a revision name into an object id the way "git rev-parse"
503+// does for plain names: a full 40 digit id, "HEAD", a full ref name, a short
504+// ref name tried as "<n>", "refs/<n>", "refs/tags/<n>", "refs/heads/<n>",
505+// "refs/remotes/<n>" and "refs/remotes/<n>/HEAD" in that order, and finally
506+// an abbreviated id of 4 to 39 digits. Revision operators (~, ^, @{}) are not
507+// supported. The result is not peeled; use PeelToCommit.
508+func (r *Repo) Resolve(rev string) (ObjectID, error) {
509+ if rev == "" {
510+ return ObjectID{}, notFound("empty revision")
511+ }
512+ if len(rev) == 40 {
513+ if id, err := ParseID(rev); err == nil {
514+ if !r.HasObject(id) {
515+ return ObjectID{}, notFound("object %s", id)
516+ }
517+ return id, nil
518+ }
519+ }
520+ for _, pat := range [...]string{"%s", "refs/%s", "refs/tags/%s", "refs/heads/%s", "refs/remotes/%s", "refs/remotes/%s/HEAD"} {
521+ ref, ok, err := r.lookupRef(fmt.Sprintf(pat, rev))
522+ if err != nil {
523+ return ObjectID{}, err
524+ }
525+ if ok {
526+ return ref.ID, nil
527+ }
528+ }
529+ if len(rev) >= minPrefix && len(rev) < 40 {
530+ id, err := r.ResolvePrefix(rev)
531+ if err == nil || !errors.Is(err, ErrInvalidID) {
532+ return id, err
533+ }
534+ }
535+ return ObjectID{}, notFound("revision %q", truncate(rev))
536+}
A repo.go +204 -0
@@ -0,0 +1,204 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "errors"
7+ "io/fs"
8+ "os"
9+ "path/filepath"
10+ "sync"
11+ "sync/atomic"
12+)
13+
14+// Default bounds. See WithMaxObjectSize and WithDeltaCacheSize.
15+const (
16+ defaultMaxObjectSize = 128 << 20
17+ defaultDeltaCacheSize = 32 << 20
18+ // maxDeltaDepth bounds the length of a delta chain. Git itself writes
19+ // chains of at most 4095 and 50 by default.
20+ maxDeltaDepth = 10000
21+)
22+
23+type options struct {
24+ maxObject int64
25+ cacheBytes int64
26+}
27+
28+// Option configures Open.
29+type Option func(*options)
30+
31+// WithMaxObjectSize bounds the size of an object that is held in memory:
32+// commits, trees, tags and blobs that are stored as deltas. Bigger ones fail
33+// with ErrTooLarge. Undeltified blobs stream and are not bounded. The default
34+// is 128 MiB.
35+func WithMaxObjectSize(n int64) Option {
36+ return func(o *options) {
37+ if n > 0 {
38+ o.maxObject = n
39+ }
40+ }
41+}
42+
43+// WithDeltaCacheSize sets the budget in bytes of the cache of delta bases
44+// (LRU). The default is 32 MiB. Zero or less disables the cache.
45+func WithDeltaCacheSize(n int64) Option {
46+ return func(o *options) { o.cacheBytes = n }
47+}
48+
49+// Repo is an open Git repository. It is read-only and safe for concurrent
50+// use by multiple goroutines.
51+//
52+// Loose refs and loose objects are always read live. The list of packs and
53+// the content of packed-refs are loaded snapshots: a missing object triggers
54+// one rescan of the pack directory, packed-refs is stat-checked on each ref
55+// read, and Refresh reloads everything explicitly.
56+type Repo struct {
57+ dir string
58+ opts options
59+
60+ snap atomic.Pointer[snapshot]
61+ mu sync.Mutex // serializes pack list reloads and Close
62+ closed bool
63+ nextPack atomic.Uint32
64+
65+ cache *deltaCache
66+
67+ packed atomic.Pointer[packedRefs]
68+ pmu sync.Mutex // serializes packed-refs reloads
69+}
70+
71+// Open opens the repository whose Git directory is dir: a bare repository
72+// or a ".git" directory. If dir contains a ".git" directory, that one is
73+// used.
74+//
75+// Open returns an error wrapping ErrNotFound when dir is not a repository
76+// and one wrapping ErrUnsupported (with the reason) when the repository uses
77+// a feature the package does not read: an object format other than sha1, a
78+// ref storage other than files, other extensions, a shallow file,
79+// info/grafts, replace refs, alternates, a linked worktree, a partial clone,
80+// or a multi-pack-index whose packs lack their .idx file. A multi-pack-index
81+// is otherwise ignored: the packs are read through their own .idx files.
82+func Open(dir string, opts ...Option) (*Repo, error) {
83+ gitDir, err := findGitDir(dir)
84+ if err != nil {
85+ return nil, err
86+ }
87+ o := options{maxObject: defaultMaxObjectSize, cacheBytes: defaultDeltaCacheSize}
88+ for _, fn := range opts {
89+ fn(&o)
90+ }
91+ if err := checkConfig(filepath.Join(gitDir, "config")); err != nil {
92+ return nil, err
93+ }
94+ if err := checkLayout(gitDir); err != nil {
95+ return nil, err
96+ }
97+ r := &Repo{dir: gitDir, opts: o, cache: newDeltaCache(o.cacheBytes)}
98+ r.snap.Store(newSnapshot(nil))
99+ if _, err := r.reloadPacks(true); err != nil {
100+ _ = r.Close()
101+ return nil, err
102+ }
103+ if _, err := r.loadPacked(true); err != nil {
104+ _ = r.Close()
105+ return nil, err
106+ }
107+ return r, nil
108+}
109+
110+// Dir returns the Git directory that was opened.
111+func (r *Repo) Dir() string { return r.dir }
112+
113+// Close releases the repository. Readers still streaming a blob keep their
114+// packs mapped until they are closed. Close is idempotent.
115+func (r *Repo) Close() error {
116+ r.mu.Lock()
117+ defer r.mu.Unlock()
118+ if r.closed {
119+ return nil
120+ }
121+ r.closed = true
122+ if s := r.snap.Swap(nil); s != nil {
123+ s.release()
124+ }
125+ return nil
126+}
127+
128+// Refresh reloads the list of packs and packed-refs when they changed, and
129+// checks again that the repository is still supported (it returns an error
130+// wrapping ErrUnsupported when, for example, a replace ref was pushed). Packs
131+// that disappeared stay mapped until the readers using them are done, so a
132+// concurrent "git repack -ad" never breaks a read in flight.
133+func (r *Repo) Refresh() error {
134+ if err := checkLayout(r.dir); err != nil {
135+ return err
136+ }
137+ if _, err := r.reloadPacks(false); err != nil {
138+ return err
139+ }
140+ _, err := r.loadPacked(false)
141+ return err
142+}
143+
144+func findGitDir(dir string) (string, error) {
145+ for range 2 {
146+ if isGitDir(dir) {
147+ return dir, nil
148+ }
149+ sub := filepath.Join(dir, ".git")
150+ fi, err := os.Stat(sub)
151+ switch {
152+ case err != nil:
153+ return "", notFound("%s is not a git repository", dir)
154+ case fi.IsDir():
155+ dir = sub
156+ default:
157+ return "", unsupported("%s is a gitfile (linked worktree or submodule)", sub)
158+ }
159+ }
160+ return "", notFound("%s is not a git repository", dir)
161+}
162+
163+func isGitDir(dir string) bool {
164+ for _, n := range []string{"HEAD", "objects", "refs"} {
165+ if _, err := os.Stat(filepath.Join(dir, n)); err != nil {
166+ return false
167+ }
168+ }
169+ return true
170+}
171+
172+func exists(path string) bool {
173+ _, err := os.Lstat(path)
174+ return err == nil
175+}
176+
177+// checkLayout rejects the repository layouts that change what objects or
178+// refs mean and that the package does not implement.
179+func checkLayout(dir string) error {
180+ for _, c := range []struct{ path, why string }{
181+ {"shallow", "shallow repository"},
182+ {"info/grafts", "grafts file"},
183+ {"commondir", "linked worktree"},
184+ {"objects/info/alternates", "alternate object stores"},
185+ } {
186+ if exists(filepath.Join(dir, c.path)) {
187+ return unsupported("%s (%s)", c.why, c.path)
188+ }
189+ }
190+ f, err := os.Open(filepath.Join(dir, "refs", "replace"))
191+ if err == nil {
192+ names, rerr := f.Readdirnames(1)
193+ _ = f.Close()
194+ if rerr == nil && len(names) > 0 {
195+ return unsupported("replace refs (refs/replace)")
196+ }
197+ } else if !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, fs.ErrInvalid) {
198+ var pe *fs.PathError
199+ if !errors.As(err, &pe) {
200+ return err
201+ }
202+ }
203+ return nil
204+}
@@ -0,0 +1,204 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "errors"
7+ "io/fs"
8+ "os"
9+ "path/filepath"
10+ "sync"
11+ "sync/atomic"
12+)
13+
14+// Default bounds. See WithMaxObjectSize and WithDeltaCacheSize.
15+const (
16+ defaultMaxObjectSize = 128 << 20
17+ defaultDeltaCacheSize = 32 << 20
18+ // maxDeltaDepth bounds the length of a delta chain. Git itself writes
19+ // chains of at most 4095 and 50 by default.
20+ maxDeltaDepth = 10000
21+)
22+
23+type options struct {
24+ maxObject int64
25+ cacheBytes int64
26+}
27+
28+// Option configures Open.
29+type Option func(*options)
30+
31+// WithMaxObjectSize bounds the size of an object that is held in memory:
32+// commits, trees, tags and blobs that are stored as deltas. Bigger ones fail
33+// with ErrTooLarge. Undeltified blobs stream and are not bounded. The default
34+// is 128 MiB.
35+func WithMaxObjectSize(n int64) Option {
36+ return func(o *options) {
37+ if n > 0 {
38+ o.maxObject = n
39+ }
40+ }
41+}
42+
43+// WithDeltaCacheSize sets the budget in bytes of the cache of delta bases
44+// (LRU). The default is 32 MiB. Zero or less disables the cache.
45+func WithDeltaCacheSize(n int64) Option {
46+ return func(o *options) { o.cacheBytes = n }
47+}
48+
49+// Repo is an open Git repository. It is read-only and safe for concurrent
50+// use by multiple goroutines.
51+//
52+// Loose refs and loose objects are always read live. The list of packs and
53+// the content of packed-refs are loaded snapshots: a missing object triggers
54+// one rescan of the pack directory, packed-refs is stat-checked on each ref
55+// read, and Refresh reloads everything explicitly.
56+type Repo struct {
57+ dir string
58+ opts options
59+
60+ snap atomic.Pointer[snapshot]
61+ mu sync.Mutex // serializes pack list reloads and Close
62+ closed bool
63+ nextPack atomic.Uint32
64+
65+ cache *deltaCache
66+
67+ packed atomic.Pointer[packedRefs]
68+ pmu sync.Mutex // serializes packed-refs reloads
69+}
70+
71+// Open opens the repository whose Git directory is dir: a bare repository
72+// or a ".git" directory. If dir contains a ".git" directory, that one is
73+// used.
74+//
75+// Open returns an error wrapping ErrNotFound when dir is not a repository
76+// and one wrapping ErrUnsupported (with the reason) when the repository uses
77+// a feature the package does not read: an object format other than sha1, a
78+// ref storage other than files, other extensions, a shallow file,
79+// info/grafts, replace refs, alternates, a linked worktree, a partial clone,
80+// or a multi-pack-index whose packs lack their .idx file. A multi-pack-index
81+// is otherwise ignored: the packs are read through their own .idx files.
82+func Open(dir string, opts ...Option) (*Repo, error) {
83+ gitDir, err := findGitDir(dir)
84+ if err != nil {
85+ return nil, err
86+ }
87+ o := options{maxObject: defaultMaxObjectSize, cacheBytes: defaultDeltaCacheSize}
88+ for _, fn := range opts {
89+ fn(&o)
90+ }
91+ if err := checkConfig(filepath.Join(gitDir, "config")); err != nil {
92+ return nil, err
93+ }
94+ if err := checkLayout(gitDir); err != nil {
95+ return nil, err
96+ }
97+ r := &Repo{dir: gitDir, opts: o, cache: newDeltaCache(o.cacheBytes)}
98+ r.snap.Store(newSnapshot(nil))
99+ if _, err := r.reloadPacks(true); err != nil {
100+ _ = r.Close()
101+ return nil, err
102+ }
103+ if _, err := r.loadPacked(true); err != nil {
104+ _ = r.Close()
105+ return nil, err
106+ }
107+ return r, nil
108+}
109+
110+// Dir returns the Git directory that was opened.
111+func (r *Repo) Dir() string { return r.dir }
112+
113+// Close releases the repository. Readers still streaming a blob keep their
114+// packs mapped until they are closed. Close is idempotent.
115+func (r *Repo) Close() error {
116+ r.mu.Lock()
117+ defer r.mu.Unlock()
118+ if r.closed {
119+ return nil
120+ }
121+ r.closed = true
122+ if s := r.snap.Swap(nil); s != nil {
123+ s.release()
124+ }
125+ return nil
126+}
127+
128+// Refresh reloads the list of packs and packed-refs when they changed, and
129+// checks again that the repository is still supported (it returns an error
130+// wrapping ErrUnsupported when, for example, a replace ref was pushed). Packs
131+// that disappeared stay mapped until the readers using them are done, so a
132+// concurrent "git repack -ad" never breaks a read in flight.
133+func (r *Repo) Refresh() error {
134+ if err := checkLayout(r.dir); err != nil {
135+ return err
136+ }
137+ if _, err := r.reloadPacks(false); err != nil {
138+ return err
139+ }
140+ _, err := r.loadPacked(false)
141+ return err
142+}
143+
144+func findGitDir(dir string) (string, error) {
145+ for range 2 {
146+ if isGitDir(dir) {
147+ return dir, nil
148+ }
149+ sub := filepath.Join(dir, ".git")
150+ fi, err := os.Stat(sub)
151+ switch {
152+ case err != nil:
153+ return "", notFound("%s is not a git repository", dir)
154+ case fi.IsDir():
155+ dir = sub
156+ default:
157+ return "", unsupported("%s is a gitfile (linked worktree or submodule)", sub)
158+ }
159+ }
160+ return "", notFound("%s is not a git repository", dir)
161+}
162+
163+func isGitDir(dir string) bool {
164+ for _, n := range []string{"HEAD", "objects", "refs"} {
165+ if _, err := os.Stat(filepath.Join(dir, n)); err != nil {
166+ return false
167+ }
168+ }
169+ return true
170+}
171+
172+func exists(path string) bool {
173+ _, err := os.Lstat(path)
174+ return err == nil
175+}
176+
177+// checkLayout rejects the repository layouts that change what objects or
178+// refs mean and that the package does not implement.
179+func checkLayout(dir string) error {
180+ for _, c := range []struct{ path, why string }{
181+ {"shallow", "shallow repository"},
182+ {"info/grafts", "grafts file"},
183+ {"commondir", "linked worktree"},
184+ {"objects/info/alternates", "alternate object stores"},
185+ } {
186+ if exists(filepath.Join(dir, c.path)) {
187+ return unsupported("%s (%s)", c.why, c.path)
188+ }
189+ }
190+ f, err := os.Open(filepath.Join(dir, "refs", "replace"))
191+ if err == nil {
192+ names, rerr := f.Readdirnames(1)
193+ _ = f.Close()
194+ if rerr == nil && len(names) > 0 {
195+ return unsupported("replace refs (refs/replace)")
196+ }
197+ } else if !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, fs.ErrInvalid) {
198+ var pe *fs.PathError
199+ if !errors.As(err, &pe) {
200+ return err
201+ }
202+ }
203+ return nil
204+}
A snapshot.go +287 -0
@@ -0,0 +1,287 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "encoding/binary"
7+ "errors"
8+ "io/fs"
9+ "os"
10+ "slices"
11+ "strings"
12+ "sync/atomic"
13+ "time"
14+)
15+
16+// fileSig identifies a version of a file or directory by stat data.
17+type fileSig struct {
18+ exists bool
19+ size int64
20+ mtime int64 // unix nanoseconds
21+ ino uint64
22+}
23+
24+func sigOf(fi os.FileInfo) fileSig {
25+ return fileSig{exists: true, size: fi.Size(), mtime: fi.ModTime().UnixNano(), ino: fileIno(fi)}
26+}
27+
28+func statSig(path string) (fileSig, error) {
29+ fi, err := os.Stat(path)
30+ if errors.Is(err, fs.ErrNotExist) {
31+ return fileSig{}, nil
32+ }
33+ if err != nil {
34+ return fileSig{}, err
35+ }
36+ return sigOf(fi), nil
37+}
38+
39+// racyWindow is how long after its modification time a file may still change
40+// without its stat data changing, because of coarse file system clocks.
41+const racyWindow = 2 * time.Second
42+
43+// same reports whether b is the same version as a and can be trusted to be
44+// unchanged.
45+func (a fileSig) same(b fileSig) bool {
46+ if a != b {
47+ return false
48+ }
49+ return !a.exists || time.Since(time.Unix(0, a.mtime)) > racyWindow
50+}
51+
52+func ignoreMissing(err error) error {
53+ if errors.Is(err, fs.ErrNotExist) {
54+ return nil
55+ }
56+ return err
57+}
58+
59+// snapshot is an immutable list of packs, shared by readers. It is released
60+// by each reader and by the Repo; the last release drops its packs.
61+type snapshot struct {
62+ refs atomic.Int32
63+ packs []*pack // newest first
64+ dirSig fileSig // of objects/pack when the list was built
65+}
66+
67+func newSnapshot(packs []*pack) *snapshot {
68+ s := &snapshot{packs: packs}
69+ s.refs.Store(1)
70+ return s
71+}
72+
73+func (s *snapshot) release() {
74+ if s.refs.Add(-1) == 0 {
75+ for _, p := range s.packs {
76+ p.release()
77+ }
78+ }
79+}
80+
81+// find looks id up in the packs.
82+func (s *snapshot) find(id *ObjectID) (*pack, uint64, error) {
83+ for _, p := range s.packs {
84+ if pos, ok := p.idx.find(id); ok {
85+ off, err := p.idx.offset(pos)
86+ if err != nil {
87+ return nil, 0, wrapPack(p.name, err)
88+ }
89+ return p, off, nil
90+ }
91+ }
92+ return nil, 0, nil
93+}
94+
95+// acquire returns the current snapshot, which the caller must release.
96+func (r *Repo) acquire() (*snapshot, error) {
97+ for {
98+ s := r.snap.Load()
99+ if s == nil {
100+ return nil, ErrClosed
101+ }
102+ n := s.refs.Load()
103+ if n > 0 && s.refs.CompareAndSwap(n, n+1) {
104+ return s, nil
105+ }
106+ }
107+}
108+
109+func (r *Repo) packDir() string { return r.dir + "/objects/pack" }
110+
111+// reloadPacks rescans objects/pack and installs a new snapshot when the
112+// directory changed, or always with force. It reports whether the list of
113+// packs changed.
114+func (r *Repo) reloadPacks(force bool) (bool, error) {
115+ r.mu.Lock()
116+ defer r.mu.Unlock()
117+ if r.closed {
118+ return false, ErrClosed
119+ }
120+ dir := r.packDir()
121+ cur := r.snap.Load()
122+ dsig, err := statSig(dir)
123+ if err != nil {
124+ return false, err
125+ }
126+ if !force && cur.dirSig.same(dsig) {
127+ return false, nil
128+ }
129+ entries, err := os.ReadDir(dir)
130+ if err != nil && !errors.Is(err, fs.ErrNotExist) {
131+ return false, err
132+ }
133+ var idxNames []string
134+ hasMidx := false
135+ for _, e := range entries {
136+ n := e.Name()
137+ switch {
138+ case n == "multi-pack-index":
139+ hasMidx = true
140+ case n == "multi-pack-index.d":
141+ return false, unsupported("incremental multi-pack-index (objects/pack/multi-pack-index.d)")
142+ case strings.HasPrefix(n, "pack-") && strings.HasSuffix(n, ".promisor"):
143+ return false, unsupported("partial clone (%s)", n)
144+ case strings.HasPrefix(n, "pack-") && strings.HasSuffix(n, ".idx"):
145+ idxNames = append(idxNames, strings.TrimSuffix(n, ".idx"))
146+ }
147+ }
148+ if hasMidx {
149+ if err := checkMidx(dir+"/multi-pack-index", idxNames); err != nil {
150+ return false, err
151+ }
152+ }
153+
154+ old := make(map[string]*pack, len(cur.packs))
155+ for _, p := range cur.packs {
156+ old[p.name] = p
157+ }
158+ var packs []*pack
159+ changed := len(idxNames) != len(cur.packs)
160+ for _, name := range idxNames {
161+ isig, err := statSig(dir + "/" + name + ".idx")
162+ if err != nil {
163+ return false, err
164+ }
165+ if p := old[name]; p != nil && p.sig == isig {
166+ p.retain()
167+ packs = append(packs, p)
168+ continue
169+ }
170+ p, err := openPack(dir, name, r.nextPack.Add(1), isig)
171+ if err != nil {
172+ for _, q := range packs {
173+ q.release()
174+ }
175+ return false, err
176+ }
177+ if p == nil { // replaced while we looked
178+ changed = true
179+ continue
180+ }
181+ changed = true
182+ packs = append(packs, p)
183+ }
184+ if len(packs) != len(cur.packs) {
185+ changed = true
186+ }
187+ slices.SortStableFunc(packs, func(a, b *pack) int {
188+ switch {
189+ case a.sig.mtime > b.sig.mtime:
190+ return -1
191+ case a.sig.mtime < b.sig.mtime:
192+ return 1
193+ }
194+ return strings.Compare(a.name, b.name)
195+ })
196+ if !changed {
197+ // Same packs: keep the current snapshot (and its readers) unless the
198+ // directory stat data moved on.
199+ for _, p := range packs {
200+ p.release()
201+ }
202+ if cur.dirSig == dsig {
203+ return false, nil
204+ }
205+ packs = cur.packs
206+ for _, p := range packs {
207+ p.retain()
208+ }
209+ }
210+ ns := newSnapshot(packs)
211+ ns.dirSig = dsig
212+ r.snap.Store(ns)
213+ cur.release()
214+ return changed, nil
215+}
216+
217+const (
218+ midxHeaderLen = 12
219+ midxChunkPNAM = 0x504e414d
220+ maxMidxNames = 4 << 20
221+)
222+
223+// checkMidx accepts a multi-pack-index only when every pack it lists has
224+// its own .idx file: the midx is then redundant and ignored.
225+func checkMidx(path string, idxNames []string) error {
226+ f, err := os.Open(path)
227+ if err != nil {
228+ return ignoreMissing(err)
229+ }
230+ defer func() { _ = f.Close() }()
231+ var hdr [midxHeaderLen]byte
232+ if _, err := f.ReadAt(hdr[:], 0); err != nil || string(hdr[:4]) != "MIDX" {
233+ return unsupported("unreadable multi-pack-index")
234+ }
235+ if hdr[4] != 1 || hdr[5] != 1 {
236+ return unsupported("multi-pack-index version %d, object format %d", hdr[4], hdr[5])
237+ }
238+ chunks := int(hdr[6])
239+ if hdr[7] != 0 {
240+ return unsupported("incremental multi-pack-index")
241+ }
242+ nPacks := binary.BigEndian.Uint32(hdr[8:])
243+ table := make([]byte, (chunks+1)*12)
244+ if _, err := f.ReadAt(table, midxHeaderLen); err != nil {
245+ return unsupported("multi-pack-index chunk table: %v", err)
246+ }
247+ var start, end uint64
248+ for i := 0; i < chunks; i++ {
249+ if binary.BigEndian.Uint32(table[i*12:]) == midxChunkPNAM {
250+ start = binary.BigEndian.Uint64(table[i*12+4:])
251+ end = binary.BigEndian.Uint64(table[(i+1)*12+4:])
252+ break
253+ }
254+ }
255+ if end <= start || end-start > maxMidxNames {
256+ return unsupported("multi-pack-index without a usable pack name chunk")
257+ }
258+ names := make([]byte, end-start)
259+ if _, err := f.ReadAt(names, int64(start)); err != nil {
260+ return unsupported("multi-pack-index pack names: %v", err)
261+ }
262+ have := make(map[string]bool, len(idxNames))
263+ for _, n := range idxNames {
264+ have[n] = true
265+ }
266+ listed := uint32(0)
267+ for len(names) > 0 && listed < nPacks {
268+ i := 0
269+ for i < len(names) && names[i] != 0 {
270+ i++
271+ }
272+ if i == len(names) {
273+ break
274+ }
275+ name := string(names[:i])
276+ names = names[i+1:]
277+ listed++
278+ if name == "" { // padding
279+ continue
280+ }
281+ name = strings.TrimSuffix(strings.TrimSuffix(name, ".idx"), ".pack")
282+ if !have[name] {
283+ return unsupported("multi-pack-index lists %s, which has no .idx file", truncate(name))
284+ }
285+ }
286+ return nil
287+}
@@ -0,0 +1,287 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "encoding/binary"
7+ "errors"
8+ "io/fs"
9+ "os"
10+ "slices"
11+ "strings"
12+ "sync/atomic"
13+ "time"
14+)
15+
16+// fileSig identifies a version of a file or directory by stat data.
17+type fileSig struct {
18+ exists bool
19+ size int64
20+ mtime int64 // unix nanoseconds
21+ ino uint64
22+}
23+
24+func sigOf(fi os.FileInfo) fileSig {
25+ return fileSig{exists: true, size: fi.Size(), mtime: fi.ModTime().UnixNano(), ino: fileIno(fi)}
26+}
27+
28+func statSig(path string) (fileSig, error) {
29+ fi, err := os.Stat(path)
30+ if errors.Is(err, fs.ErrNotExist) {
31+ return fileSig{}, nil
32+ }
33+ if err != nil {
34+ return fileSig{}, err
35+ }
36+ return sigOf(fi), nil
37+}
38+
39+// racyWindow is how long after its modification time a file may still change
40+// without its stat data changing, because of coarse file system clocks.
41+const racyWindow = 2 * time.Second
42+
43+// same reports whether b is the same version as a and can be trusted to be
44+// unchanged.
45+func (a fileSig) same(b fileSig) bool {
46+ if a != b {
47+ return false
48+ }
49+ return !a.exists || time.Since(time.Unix(0, a.mtime)) > racyWindow
50+}
51+
52+func ignoreMissing(err error) error {
53+ if errors.Is(err, fs.ErrNotExist) {
54+ return nil
55+ }
56+ return err
57+}
58+
59+// snapshot is an immutable list of packs, shared by readers. It is released
60+// by each reader and by the Repo; the last release drops its packs.
61+type snapshot struct {
62+ refs atomic.Int32
63+ packs []*pack // newest first
64+ dirSig fileSig // of objects/pack when the list was built
65+}
66+
67+func newSnapshot(packs []*pack) *snapshot {
68+ s := &snapshot{packs: packs}
69+ s.refs.Store(1)
70+ return s
71+}
72+
73+func (s *snapshot) release() {
74+ if s.refs.Add(-1) == 0 {
75+ for _, p := range s.packs {
76+ p.release()
77+ }
78+ }
79+}
80+
81+// find looks id up in the packs.
82+func (s *snapshot) find(id *ObjectID) (*pack, uint64, error) {
83+ for _, p := range s.packs {
84+ if pos, ok := p.idx.find(id); ok {
85+ off, err := p.idx.offset(pos)
86+ if err != nil {
87+ return nil, 0, wrapPack(p.name, err)
88+ }
89+ return p, off, nil
90+ }
91+ }
92+ return nil, 0, nil
93+}
94+
95+// acquire returns the current snapshot, which the caller must release.
96+func (r *Repo) acquire() (*snapshot, error) {
97+ for {
98+ s := r.snap.Load()
99+ if s == nil {
100+ return nil, ErrClosed
101+ }
102+ n := s.refs.Load()
103+ if n > 0 && s.refs.CompareAndSwap(n, n+1) {
104+ return s, nil
105+ }
106+ }
107+}
108+
109+func (r *Repo) packDir() string { return r.dir + "/objects/pack" }
110+
111+// reloadPacks rescans objects/pack and installs a new snapshot when the
112+// directory changed, or always with force. It reports whether the list of
113+// packs changed.
114+func (r *Repo) reloadPacks(force bool) (bool, error) {
115+ r.mu.Lock()
116+ defer r.mu.Unlock()
117+ if r.closed {
118+ return false, ErrClosed
119+ }
120+ dir := r.packDir()
121+ cur := r.snap.Load()
122+ dsig, err := statSig(dir)
123+ if err != nil {
124+ return false, err
125+ }
126+ if !force && cur.dirSig.same(dsig) {
127+ return false, nil
128+ }
129+ entries, err := os.ReadDir(dir)
130+ if err != nil && !errors.Is(err, fs.ErrNotExist) {
131+ return false, err
132+ }
133+ var idxNames []string
134+ hasMidx := false
135+ for _, e := range entries {
136+ n := e.Name()
137+ switch {
138+ case n == "multi-pack-index":
139+ hasMidx = true
140+ case n == "multi-pack-index.d":
141+ return false, unsupported("incremental multi-pack-index (objects/pack/multi-pack-index.d)")
142+ case strings.HasPrefix(n, "pack-") && strings.HasSuffix(n, ".promisor"):
143+ return false, unsupported("partial clone (%s)", n)
144+ case strings.HasPrefix(n, "pack-") && strings.HasSuffix(n, ".idx"):
145+ idxNames = append(idxNames, strings.TrimSuffix(n, ".idx"))
146+ }
147+ }
148+ if hasMidx {
149+ if err := checkMidx(dir+"/multi-pack-index", idxNames); err != nil {
150+ return false, err
151+ }
152+ }
153+
154+ old := make(map[string]*pack, len(cur.packs))
155+ for _, p := range cur.packs {
156+ old[p.name] = p
157+ }
158+ var packs []*pack
159+ changed := len(idxNames) != len(cur.packs)
160+ for _, name := range idxNames {
161+ isig, err := statSig(dir + "/" + name + ".idx")
162+ if err != nil {
163+ return false, err
164+ }
165+ if p := old[name]; p != nil && p.sig == isig {
166+ p.retain()
167+ packs = append(packs, p)
168+ continue
169+ }
170+ p, err := openPack(dir, name, r.nextPack.Add(1), isig)
171+ if err != nil {
172+ for _, q := range packs {
173+ q.release()
174+ }
175+ return false, err
176+ }
177+ if p == nil { // replaced while we looked
178+ changed = true
179+ continue
180+ }
181+ changed = true
182+ packs = append(packs, p)
183+ }
184+ if len(packs) != len(cur.packs) {
185+ changed = true
186+ }
187+ slices.SortStableFunc(packs, func(a, b *pack) int {
188+ switch {
189+ case a.sig.mtime > b.sig.mtime:
190+ return -1
191+ case a.sig.mtime < b.sig.mtime:
192+ return 1
193+ }
194+ return strings.Compare(a.name, b.name)
195+ })
196+ if !changed {
197+ // Same packs: keep the current snapshot (and its readers) unless the
198+ // directory stat data moved on.
199+ for _, p := range packs {
200+ p.release()
201+ }
202+ if cur.dirSig == dsig {
203+ return false, nil
204+ }
205+ packs = cur.packs
206+ for _, p := range packs {
207+ p.retain()
208+ }
209+ }
210+ ns := newSnapshot(packs)
211+ ns.dirSig = dsig
212+ r.snap.Store(ns)
213+ cur.release()
214+ return changed, nil
215+}
216+
217+const (
218+ midxHeaderLen = 12
219+ midxChunkPNAM = 0x504e414d
220+ maxMidxNames = 4 << 20
221+)
222+
223+// checkMidx accepts a multi-pack-index only when every pack it lists has
224+// its own .idx file: the midx is then redundant and ignored.
225+func checkMidx(path string, idxNames []string) error {
226+ f, err := os.Open(path)
227+ if err != nil {
228+ return ignoreMissing(err)
229+ }
230+ defer func() { _ = f.Close() }()
231+ var hdr [midxHeaderLen]byte
232+ if _, err := f.ReadAt(hdr[:], 0); err != nil || string(hdr[:4]) != "MIDX" {
233+ return unsupported("unreadable multi-pack-index")
234+ }
235+ if hdr[4] != 1 || hdr[5] != 1 {
236+ return unsupported("multi-pack-index version %d, object format %d", hdr[4], hdr[5])
237+ }
238+ chunks := int(hdr[6])
239+ if hdr[7] != 0 {
240+ return unsupported("incremental multi-pack-index")
241+ }
242+ nPacks := binary.BigEndian.Uint32(hdr[8:])
243+ table := make([]byte, (chunks+1)*12)
244+ if _, err := f.ReadAt(table, midxHeaderLen); err != nil {
245+ return unsupported("multi-pack-index chunk table: %v", err)
246+ }
247+ var start, end uint64
248+ for i := 0; i < chunks; i++ {
249+ if binary.BigEndian.Uint32(table[i*12:]) == midxChunkPNAM {
250+ start = binary.BigEndian.Uint64(table[i*12+4:])
251+ end = binary.BigEndian.Uint64(table[(i+1)*12+4:])
252+ break
253+ }
254+ }
255+ if end <= start || end-start > maxMidxNames {
256+ return unsupported("multi-pack-index without a usable pack name chunk")
257+ }
258+ names := make([]byte, end-start)
259+ if _, err := f.ReadAt(names, int64(start)); err != nil {
260+ return unsupported("multi-pack-index pack names: %v", err)
261+ }
262+ have := make(map[string]bool, len(idxNames))
263+ for _, n := range idxNames {
264+ have[n] = true
265+ }
266+ listed := uint32(0)
267+ for len(names) > 0 && listed < nPacks {
268+ i := 0
269+ for i < len(names) && names[i] != 0 {
270+ i++
271+ }
272+ if i == len(names) {
273+ break
274+ }
275+ name := string(names[:i])
276+ names = names[i+1:]
277+ listed++
278+ if name == "" { // padding
279+ continue
280+ }
281+ name = strings.TrimSuffix(strings.TrimSuffix(name, ".idx"), ".pack")
282+ if !have[name] {
283+ return unsupported("multi-pack-index lists %s, which has no .idx file", truncate(name))
284+ }
285+ }
286+ return nil
287+}
A tree.go +118 -0
@@ -0,0 +1,118 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bytes"
7+ "fmt"
8+)
9+
10+// Entry is an entry of a tree.
11+type Entry struct {
12+ // Name is the file name. It points into the tree buffer and is only
13+ // valid during the callback that receives the entry; copy it to keep it.
14+ Name []byte
15+ Mode Mode
16+ ID ObjectID
17+}
18+
19+// ParseTree calls fn for each entry of a tree object in order, until fn
20+// returns false. The entries are not copied, so iterating allocates nothing.
21+// It returns ErrCorrupt on a malformed tree.
22+func ParseTree(data []byte, fn func(Entry) bool) error {
23+ var e Entry
24+ for len(data) > 0 {
25+ sp := bytes.IndexByte(data, ' ')
26+ if sp <= 0 || sp > 7 {
27+ return corrupt("tree: bad mode")
28+ }
29+ var mode uint32
30+ for _, c := range data[:sp] {
31+ if c < '0' || c > '7' {
32+ return corrupt("tree: bad mode")
33+ }
34+ mode = mode<<3 | uint32(c-'0')
35+ }
36+ data = data[sp+1:]
37+ nul := bytes.IndexByte(data, 0)
38+ if nul <= 0 || len(data)-nul-1 < 20 {
39+ return corrupt("tree: truncated entry")
40+ }
41+ e.Name = data[:nul]
42+ e.Mode = Mode(mode)
43+ copy(e.ID[:], data[nul+1:nul+21])
44+ data = data[nul+21:]
45+ if !fn(e) {
46+ return nil
47+ }
48+ }
49+ return nil
50+}
51+
52+// ReadTree reads the tree id and calls fn for each entry, in tree order,
53+// until fn returns false. Entry.Name is only valid during the call. The tree
54+// is read into a pooled buffer, so listing allocates nothing per entry.
55+func (r *Repo) ReadTree(id ObjectID, fn func(Entry) bool) error {
56+ b, err := r.readTyped(id, TypeTree)
57+ if err != nil {
58+ return err
59+ }
60+ defer putBuf(b)
61+ if err := ParseTree(b.b, fn); err != nil {
62+ return wrapObj(id, err)
63+ }
64+ return nil
65+}
66+
67+// LookupPath finds the entry at path (slash separated, no leading slash)
68+// below the tree root. It returns ErrNotFound when a component does not
69+// exist or is not a directory. An empty path designates root itself, as a
70+// directory.
71+func (r *Repo) LookupPath(root ObjectID, path string) (Entry, error) {
72+ cur := Entry{Mode: ModeTree, ID: root}
73+ for path != "" {
74+ var name string
75+ name, path, _ = cutByte(path, '/')
76+ if name == "" {
77+ return Entry{}, notFound("empty path component")
78+ }
79+ if !cur.Mode.IsTree() {
80+ return Entry{}, notFound("%q is not a directory", name)
81+ }
82+ found := false
83+ var next Entry
84+ err := r.ReadTree(cur.ID, func(e Entry) bool {
85+ if string(e.Name) == name {
86+ next = Entry{Mode: e.Mode, ID: e.ID}
87+ found = true
88+ return false
89+ }
90+ return true
91+ })
92+ if err != nil {
93+ return Entry{}, err
94+ }
95+ if !found {
96+ return Entry{}, notFound("path component %q", name)
97+ }
98+ cur = next
99+ }
100+ return cur, nil
101+}
102+
103+func cutByte(s string, c byte) (before, after string, found bool) {
104+ for i := 0; i < len(s); i++ {
105+ if s[i] == c {
106+ return s[:i], s[i+1:], true
107+ }
108+ }
109+ return s, "", false
110+}
111+
112+func wrongType(id ObjectID, got, want Type) error {
113+ return fmt.Errorf("%w: %s is a %s, not a %s", ErrWrongType, id.String(), got, want)
114+}
115+
116+func wrapObj(id ObjectID, err error) error {
117+ return fmt.Errorf("object %s: %w", id.String(), err)
118+}
@@ -0,0 +1,118 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import (
6+ "bytes"
7+ "fmt"
8+)
9+
10+// Entry is an entry of a tree.
11+type Entry struct {
12+ // Name is the file name. It points into the tree buffer and is only
13+ // valid during the callback that receives the entry; copy it to keep it.
14+ Name []byte
15+ Mode Mode
16+ ID ObjectID
17+}
18+
19+// ParseTree calls fn for each entry of a tree object in order, until fn
20+// returns false. The entries are not copied, so iterating allocates nothing.
21+// It returns ErrCorrupt on a malformed tree.
22+func ParseTree(data []byte, fn func(Entry) bool) error {
23+ var e Entry
24+ for len(data) > 0 {
25+ sp := bytes.IndexByte(data, ' ')
26+ if sp <= 0 || sp > 7 {
27+ return corrupt("tree: bad mode")
28+ }
29+ var mode uint32
30+ for _, c := range data[:sp] {
31+ if c < '0' || c > '7' {
32+ return corrupt("tree: bad mode")
33+ }
34+ mode = mode<<3 | uint32(c-'0')
35+ }
36+ data = data[sp+1:]
37+ nul := bytes.IndexByte(data, 0)
38+ if nul <= 0 || len(data)-nul-1 < 20 {
39+ return corrupt("tree: truncated entry")
40+ }
41+ e.Name = data[:nul]
42+ e.Mode = Mode(mode)
43+ copy(e.ID[:], data[nul+1:nul+21])
44+ data = data[nul+21:]
45+ if !fn(e) {
46+ return nil
47+ }
48+ }
49+ return nil
50+}
51+
52+// ReadTree reads the tree id and calls fn for each entry, in tree order,
53+// until fn returns false. Entry.Name is only valid during the call. The tree
54+// is read into a pooled buffer, so listing allocates nothing per entry.
55+func (r *Repo) ReadTree(id ObjectID, fn func(Entry) bool) error {
56+ b, err := r.readTyped(id, TypeTree)
57+ if err != nil {
58+ return err
59+ }
60+ defer putBuf(b)
61+ if err := ParseTree(b.b, fn); err != nil {
62+ return wrapObj(id, err)
63+ }
64+ return nil
65+}
66+
67+// LookupPath finds the entry at path (slash separated, no leading slash)
68+// below the tree root. It returns ErrNotFound when a component does not
69+// exist or is not a directory. An empty path designates root itself, as a
70+// directory.
71+func (r *Repo) LookupPath(root ObjectID, path string) (Entry, error) {
72+ cur := Entry{Mode: ModeTree, ID: root}
73+ for path != "" {
74+ var name string
75+ name, path, _ = cutByte(path, '/')
76+ if name == "" {
77+ return Entry{}, notFound("empty path component")
78+ }
79+ if !cur.Mode.IsTree() {
80+ return Entry{}, notFound("%q is not a directory", name)
81+ }
82+ found := false
83+ var next Entry
84+ err := r.ReadTree(cur.ID, func(e Entry) bool {
85+ if string(e.Name) == name {
86+ next = Entry{Mode: e.Mode, ID: e.ID}
87+ found = true
88+ return false
89+ }
90+ return true
91+ })
92+ if err != nil {
93+ return Entry{}, err
94+ }
95+ if !found {
96+ return Entry{}, notFound("path component %q", name)
97+ }
98+ cur = next
99+ }
100+ return cur, nil
101+}
102+
103+func cutByte(s string, c byte) (before, after string, found bool) {
104+ for i := 0; i < len(s); i++ {
105+ if s[i] == c {
106+ return s[:i], s[i+1:], true
107+ }
108+ }
109+ return s, "", false
110+}
111+
112+func wrongType(id ObjectID, got, want Type) error {
113+ return fmt.Errorf("%w: %s is a %s, not a %s", ErrWrongType, id.String(), got, want)
114+}
115+
116+func wrapObj(id ObjectID, err error) error {
117+ return fmt.Errorf("object %s: %w", id.String(), err)
118+}
A types.go +94 -0
@@ -0,0 +1,94 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import "strconv"
6+
7+// Type is the type of a Git object. The values are those of pack entries.
8+type Type uint8
9+
10+// Object types.
11+const (
12+ TypeCommit Type = 1
13+ TypeTree Type = 2
14+ TypeBlob Type = 3
15+ TypeTag Type = 4
16+)
17+
18+// String returns the name Git uses for the type ("commit", "tree", "blob",
19+// "tag").
20+func (t Type) String() string {
21+ switch t {
22+ case TypeCommit:
23+ return "commit"
24+ case TypeTree:
25+ return "tree"
26+ case TypeBlob:
27+ return "blob"
28+ case TypeTag:
29+ return "tag"
30+ }
31+ return "type(" + strconv.Itoa(int(t)) + ")"
32+}
33+
34+func parseTypeName(b []byte) (Type, bool) {
35+ switch string(b) {
36+ case "commit":
37+ return TypeCommit, true
38+ case "tree":
39+ return TypeTree, true
40+ case "blob":
41+ return TypeBlob, true
42+ case "tag":
43+ return TypeTag, true
44+ }
45+ return 0, false
46+}
47+
48+// Mode is the file mode of a tree entry.
49+type Mode uint32
50+
51+// Tree entry modes.
52+const (
53+ ModeTree Mode = 0o040000
54+ ModeFile Mode = 0o100644
55+ ModeExec Mode = 0o100755
56+ ModeSymlink Mode = 0o120000
57+ ModeGitlink Mode = 0o160000 // a submodule
58+)
59+
60+const modeTypeMask Mode = 0o170000
61+
62+// IsTree reports whether the entry is a directory.
63+func (m Mode) IsTree() bool { return m&modeTypeMask == ModeTree }
64+
65+// IsGitlink reports whether the entry is a submodule commit.
66+func (m Mode) IsGitlink() bool { return m&modeTypeMask == ModeGitlink }
67+
68+// IsSymlink reports whether the entry is a symbolic link.
69+func (m Mode) IsSymlink() bool { return m&modeTypeMask == ModeSymlink }
70+
71+// IsRegular reports whether the entry is a regular file, executable or not.
72+func (m Mode) IsRegular() bool { return m&modeTypeMask == 0o100000 }
73+
74+// Type returns the type of the object the entry designates: a tree for a
75+// directory, a commit for a submodule, a blob otherwise.
76+func (m Mode) Type() Type {
77+ switch m & modeTypeMask {
78+ case ModeTree:
79+ return TypeTree
80+ case ModeGitlink:
81+ return TypeCommit
82+ }
83+ return TypeBlob
84+}
85+
86+// String returns the mode as Git prints it: six octal digits (five for a
87+// directory is printed as "040000").
88+func (m Mode) String() string {
89+ s := strconv.FormatUint(uint64(m), 8)
90+ for len(s) < 6 {
91+ s = "0" + s
92+ }
93+ return s
94+}
@@ -0,0 +1,94 @@
1+// SPDX-License-Identifier: MIT
2+
3+package git
4+
5+import "strconv"
6+
7+// Type is the type of a Git object. The values are those of pack entries.
8+type Type uint8
9+
10+// Object types.
11+const (
12+ TypeCommit Type = 1
13+ TypeTree Type = 2
14+ TypeBlob Type = 3
15+ TypeTag Type = 4
16+)
17+
18+// String returns the name Git uses for the type ("commit", "tree", "blob",
19+// "tag").
20+func (t Type) String() string {
21+ switch t {
22+ case TypeCommit:
23+ return "commit"
24+ case TypeTree:
25+ return "tree"
26+ case TypeBlob:
27+ return "blob"
28+ case TypeTag:
29+ return "tag"
30+ }
31+ return "type(" + strconv.Itoa(int(t)) + ")"
32+}
33+
34+func parseTypeName(b []byte) (Type, bool) {
35+ switch string(b) {
36+ case "commit":
37+ return TypeCommit, true
38+ case "tree":
39+ return TypeTree, true
40+ case "blob":
41+ return TypeBlob, true
42+ case "tag":
43+ return TypeTag, true
44+ }
45+ return 0, false
46+}
47+
48+// Mode is the file mode of a tree entry.
49+type Mode uint32
50+
51+// Tree entry modes.
52+const (
53+ ModeTree Mode = 0o040000
54+ ModeFile Mode = 0o100644
55+ ModeExec Mode = 0o100755
56+ ModeSymlink Mode = 0o120000
57+ ModeGitlink Mode = 0o160000 // a submodule
58+)
59+
60+const modeTypeMask Mode = 0o170000
61+
62+// IsTree reports whether the entry is a directory.
63+func (m Mode) IsTree() bool { return m&modeTypeMask == ModeTree }
64+
65+// IsGitlink reports whether the entry is a submodule commit.
66+func (m Mode) IsGitlink() bool { return m&modeTypeMask == ModeGitlink }
67+
68+// IsSymlink reports whether the entry is a symbolic link.
69+func (m Mode) IsSymlink() bool { return m&modeTypeMask == ModeSymlink }
70+
71+// IsRegular reports whether the entry is a regular file, executable or not.
72+func (m Mode) IsRegular() bool { return m&modeTypeMask == 0o100000 }
73+
74+// Type returns the type of the object the entry designates: a tree for a
75+// directory, a commit for a submodule, a blob otherwise.
76+func (m Mode) Type() Type {
77+ switch m & modeTypeMask {
78+ case ModeTree:
79+ return TypeTree
80+ case ModeGitlink:
81+ return TypeCommit
82+ }
83+ return TypeBlob
84+}
85+
86+// String returns the mode as Git prints it: six octal digits (five for a
87+// directory is printed as "040000").
88+func (m Mode) String() string {
89+ s := strconv.FormatUint(uint64(m), 8)
90+ for len(s) < 6 {
91+ s = "0" + s
92+ }
93+ return s
94+}

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