v0.1.1 de06cbd

loose.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
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.
15const maxLooseHeader = 32
16
17func (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.
30type 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.
42func (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.
79const statThreshold = 1 << 16
80
81func 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
88func (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
99func (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>".
129func 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.
159func (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.
183const headerPrefix = 1024
184
185// looseHeader reads the type and size of a loose object, inflating as little
186// as possible.
187func (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.
219func 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.
244func (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}

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