v0.1.1 de06cbd

object.go RawHistoryFolder

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}

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