1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
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.
14type finder struct {
15 r *Repo
16 s *snapshot
17 retried bool
18}
19
20func (r *Repo) finder() (finder, error) {
21 s, err := r.acquire()
22 return finder{r: r, s: s}, err
23}
24
25func (f *finder) close() { f.s.release() }
26
27func (f *finder) pack(id *ObjectID) (*pack, uint64, error) { return f.s.find(id) }
28
29// reloadPacksHook, when set by a test, replaces the reload done by again.
30var reloadPacksHook func(r *Repo) (bool, error)
31
32// again reloads the pack list if it changed and reports whether the lookup
33// is worth repeating, that is whether the current snapshot differs from the
34// one this finder used. Another goroutine may already have installed the new
35// list, so the answer of reloadPacks alone is not enough. It returns true at
36// most once. An error from the reload is returned: the object may well exist
37// in a pack that could not be listed, so "not found" would be a wrong answer.
38func (f *finder) again() (bool, error) {
39 if f.retried {
40 return false, nil
41 }
42 f.retried = true
43 reload := f.r.reloadPacks
44 if hook := reloadPacksHook; hook != nil {
45 reload = func(bool) (bool, error) { return hook(f.r) }
46 }
47 if _, err := reload(false); err != nil {
48 return false, err
49 }
50 s, err := f.r.acquire()
51 if err != nil {
52 return false, err
53 }
54 if s == f.s {
55 s.release()
56 return false, nil
57 }
58 f.s.release()
59 f.s = s
60 return true, nil
61}
62
63// HasObject reports whether the object exists. The error is not nil when the
64// answer is unknown: the repository is closed, or the pack list could not be
65// reloaded while the object was missing from the current one (during a gc,
66// for example).
67func (r *Repo) HasObject(id ObjectID) (bool, error) {
68 f, err := r.finder()
69 if err != nil {
70 return false, err
71 }
72 defer f.close()
73 for {
74 if p, _, err := f.pack(&id); err != nil || p != nil {
75 return err == nil, err
76 }
77 if exists(r.loosePath(&id)) {
78 return true, nil
79 }
80 again, err := f.again()
81 if err != nil {
82 return false, err
83 }
84 if !again {
85 return false, nil
86 }
87 }
88}
89
90// ObjectHeader returns the type and the size of an object without reading its
91// content. For a deltified object the size is the result size announced by
92// the delta header. It returns ErrNotFound if the object does not exist.
93func (r *Repo) ObjectHeader(id ObjectID) (Type, int64, error) {
94 f, err := r.finder()
95 if err != nil {
96 return 0, 0, err
97 }
98 defer f.close()
99 for {
100 p, off, err := f.pack(&id)
101 if err != nil {
102 return 0, 0, err
103 }
104 if p != nil {
105 typ, size, err := p.header(off)
106 if err != nil {
107 return 0, 0, wrapPack(p.name, err)
108 }
109 return typ, size, nil
110 }
111 typ, size, err := r.looseHeader(&id)
112 if err == nil {
113 return typ, size, nil
114 }
115 if !errors.Is(err, ErrNotFound) {
116 return 0, 0, err
117 }
118 if again, err := f.again(); err != nil {
119 return 0, 0, err
120 } else if !again {
121 return 0, 0, notFound("object %s", id.String())
122 }
123 }
124}
125
126// ReadObject reads the whole content of an object. The content is appended
127// to dst[:0], which is grown if needed; pass the previous result to reuse its
128// storage. Objects over the WithMaxObjectSize bound fail with ErrTooLarge.
129func (r *Repo) ReadObject(id ObjectID, dst []byte) (Type, []byte, error) {
130 f, err := r.finder()
131 if err != nil {
132 return 0, nil, err
133 }
134 defer f.close()
135 for {
136 p, off, err := f.pack(&id)
137 if err != nil {
138 return 0, nil, err
139 }
140 if p != nil {
141 typ, data, err := r.readPacked(p, off, dst)
142 if err != nil {
143 return 0, nil, wrapPack(p.name, err)
144 }
145 return typ, data, nil
146 }
147 typ, data, err := r.readLoose(&id, dst)
148 if err == nil {
149 return typ, data, nil
150 }
151 if !errors.Is(err, ErrNotFound) {
152 return 0, nil, err
153 }
154 if again, err := f.again(); err != nil {
155 return 0, nil, err
156 } else if !again {
157 return 0, nil, notFound("object %s", id.String())
158 }
159 }
160}
161
162// readTyped reads an object that must have type want into a pooled buffer.
163// The caller returns the buffer with putBuf.
164func (r *Repo) readTyped(id ObjectID, want Type) (*buffer, error) {
165 b := getBuf()
166 typ, data, err := r.ReadObject(id, b.b)
167 if err != nil {
168 putBuf(b)
169 return nil, err
170 }
171 b.b = data
172 if typ != want {
173 putBuf(b)
174 return nil, wrongType(id, typ, want)
175 }
176 return b, nil
177}
178
179// Peel follows annotated tags from id until it reaches an object that is not
180// a tag, and returns that object and its type. Chains of tags longer than 64
181// are corrupt.
182func (r *Repo) Peel(id ObjectID) (ObjectID, Type, error) {
183 for range maxPeel {
184 typ, _, err := r.ObjectHeader(id)
185 if err != nil {
186 return ObjectID{}, 0, err
187 }
188 if typ != TypeTag {
189 return id, typ, nil
190 }
191 b, err := r.readTyped(id, TypeTag)
192 if err != nil {
193 return ObjectID{}, 0, err
194 }
195 var t Tag
196 err = ParseTag(b.b, &t)
197 next := t.Object
198 putBuf(b)
199 if err != nil {
200 return ObjectID{}, 0, fmt.Errorf("tag %s: %w", id, err)
201 }
202 id = next
203 }
204 return ObjectID{}, 0, corrupt("more than %d nested tags", maxPeel)
205}
206
207const maxPeel = 64
208
209// PeelToCommit follows annotated tags from id and returns the commit they
210// designate. It returns ErrWrongType if the result is not a commit (a tag of
211// a tree, for example).
212func (r *Repo) PeelToCommit(id ObjectID) (ObjectID, error) {
213 got, typ, err := r.Peel(id)
214 if err != nil {
215 return ObjectID{}, err
216 }
217 if typ != TypeCommit {
218 return ObjectID{}, fmt.Errorf("%w: %s is a %s, not a commit", ErrWrongType, got, typ)
219 }
220 return got, nil
221}
222
223// minPrefix is the shortest abbreviation accepted, as in Git.
224const minPrefix = 4
225
226// ResolvePrefix returns the object whose id starts with the hexadecimal
227// digits in hex (4 to 40, either case). It returns ErrNotFound when none
228// matches and ErrAmbiguous when several do, whatever their type; see
229// ResolveCommitPrefix. A full id must exist.
230func (r *Repo) ResolvePrefix(hex string) (ObjectID, error) {
231 return r.resolvePrefix(hex, nil)
232}
233
234// ResolveCommitPrefix is like ResolvePrefix but only considers the objects
235// that designate a commit: commits and tags that peel to a commit. It is what
236// "git rev-parse <hex>^{commit}" does.
237func (r *Repo) ResolveCommitPrefix(hex string) (ObjectID, error) {
238 return r.resolvePrefix(hex, func(id ObjectID) (bool, error) {
239 _, err := r.PeelToCommit(id)
240 if errors.Is(err, ErrWrongType) {
241 return false, nil
242 }
243 return err == nil, err
244 })
245}
246
247func (r *Repo) resolvePrefix(hex string, accept func(ObjectID) (bool, error)) (ObjectID, error) {
248 n := len(hex)
249 if n < minPrefix || n > 40 {
250 return ObjectID{}, fmt.Errorf("%w: %q must have %d to 40 hexadecimal digits", ErrInvalidID, truncate(hex), minPrefix)
251 }
252 var key [20]byte
253 for i := 0; i < n; i++ {
254 v := oidHexVal[hex[i]]
255 if v > 0x0f {
256 return ObjectID{}, fmt.Errorf("%w: %q is not hexadecimal", ErrInvalidID, truncate(hex))
257 }
258 if i%2 == 0 {
259 key[i/2] = v << 4
260 } else {
261 key[i/2] |= v
262 }
263 }
264 hex = strings.ToLower(hex)
265
266 f, err := r.finder()
267 if err != nil {
268 return ObjectID{}, err
269 }
270 defer f.close()
271 for {
272 var found [2]ObjectID
273 count := 0
274 var acceptErr error
275 add := func(id ObjectID) bool {
276 if count > 0 && found[0] == id {
277 return true
278 }
279 if count > 1 && found[1] == id {
280 return true
281 }
282 if accept != nil {
283 ok, err := accept(id)
284 if err != nil {
285 acceptErr = err
286 return false
287 }
288 if !ok {
289 return true
290 }
291 }
292 found[count] = id
293 count++
294 return count < 2
295 }
296 for _, p := range f.s.packs {
297 p.idx.prefixMatches(key[:], n, add)
298 if count >= 2 {
299 break
300 }
301 }
302 if count < 2 && acceptErr == nil {
303 if err := r.looseWithPrefix(hex, add); err != nil {
304 return ObjectID{}, err
305 }
306 }
307 if acceptErr != nil {
308 return ObjectID{}, acceptErr
309 }
310 switch {
311 case count >= 2:
312 return ObjectID{}, fmt.Errorf("%w: %s matches several objects", ErrAmbiguous, hex)
313 case count == 1:
314 return found[0], nil
315 }
316 if again, err := f.again(); err != nil {
317 return ObjectID{}, err
318 } else if !again {
319 return ObjectID{}, notFound("no object starts with %s", hex)
320 }
321 }
322}