v0.1.0 5228465

parse_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6	"bytes"
7	"compress/zlib"
8	"crypto/sha1"
9	"encoding/binary"
10	"errors"
11	"fmt"
12	"strings"
13	"testing"
14)
15
16func TestObjectID(t *testing.T) {
17	const hex = "0123456789abcdef0123456789abcdef01234567"
18	id, err := ParseID(hex)
19	if err != nil || id.String() != hex || id.IsZero() {
20		t.Fatalf("ParseID = %s %v", id, err)
21	}
22	if up, err := ParseID(strings.ToUpper(hex)); err != nil || up != id {
23		t.Fatalf("upper case: %v", err)
24	}
25	for _, bad := range []string{"", "abc", hex[:39], hex + "0", strings.Repeat("g", 40), hex[:39] + "\x00"} {
26		if _, err := ParseID(bad); !errors.Is(err, ErrInvalidID) {
27			t.Errorf("ParseID(%q) = %v", bad, err)
28		}
29	}
30	if !(ObjectID{}).IsZero() {
31		t.Error("zero id not zero")
32	}
33	buf := make([]byte, 0, 64)
34	out := id.AppendHex(append(buf, "x:"...))
35	if string(out) != "x:"+hex {
36		t.Errorf("AppendHex = %s", out)
37	}
38	if n := testing.AllocsPerRun(100, func() { _ = id.AppendHex(buf[:0]) }); n != 0 {
39		t.Errorf("AppendHex allocates %v", n)
40	}
41}
42
43// varint encodes a delta header size.
44func varint(v uint64) []byte {
45	var b []byte
46	for {
47		c := byte(v & 0x7f)
48		v >>= 7
49		if v != 0 {
50			b = append(b, c|0x80)
51		} else {
52			return append(b, c)
53		}
54	}
55}
56
57func copyOp(off, size uint32) []byte {
58	op := byte(0x80)
59	var args []byte
60	for i := 0; i < 4; i++ {
61		if b := byte(off >> (8 * i)); b != 0 {
62			op |= 1 << i
63			args = append(args, b)
64		}
65	}
66	for i := 0; i < 3; i++ {
67		if b := byte(size >> (8 * i)); b != 0 {
68			op |= 0x10 << i
69			args = append(args, b)
70		}
71	}
72	return append([]byte{op}, args...)
73}
74
75func TestApplyDelta(t *testing.T) {
76	base := []byte("hello, delta world")
77	mk := func(baseSize, resSize uint64, ops ...[]byte) []byte {
78		d := append(varint(baseSize), varint(resSize)...)
79		for _, o := range ops {
80			d = append(d, o...)
81		}
82		return d
83	}
84	ins := func(s string) []byte { return append([]byte{byte(len(s))}, s...) }
85	good := mk(uint64(len(base)), 12, copyOp(0, 5), ins(" Go"), copyOp(11, 4))
86	got, err := patch(base, good, 1<<20)
87	if err != nil || string(got) != "hello Go"+string(base[11:15]) {
88		t.Fatalf("patch = %q, %v", got, err)
89	}
90	// copy size 0 means 0x10000
91	bigBase := bytes.Repeat([]byte{'x'}, 0x10000)
92	got, err = patch(bigBase, mk(0x10000, 0x10000, []byte{0x80}), 1<<20)
93	if err != nil || !bytes.Equal(got, bigBase) {
94		t.Fatalf("copy 0x10000: %v", err)
95	}
96	bad := map[string][]byte{
97		"empty":            nil,
98		"truncated header": {0x80},
99		"base size":        mk(3, 1, ins("a")),
100		"copy outside":     mk(uint64(len(base)), 4, copyOp(15, 4)),
101		"copy huge":        mk(uint64(len(base)), 4, copyOp(0xffffffff, 0xffffff)),
102		"insert past end":  mk(uint64(len(base)), 9, []byte{9, 'a'}),
103		"result too long":  mk(uint64(len(base)), 1, ins("ab")),
104		"result too short": mk(uint64(len(base)), 5, ins("ab")),
105		"opcode zero":      mk(uint64(len(base)), 1, []byte{0}),
106		"truncated copy":   mk(uint64(len(base)), 1, []byte{0x81}),
107		"huge varint":      append(bytes.Repeat([]byte{0xff}, 12), 0x01),
108	}
109	for name, d := range bad {
110		if _, err := patch(base, d, 1<<20); !errors.Is(err, ErrCorrupt) {
111			t.Errorf("%s: %v, want ErrCorrupt", name, err)
112		}
113	}
114	if _, err := patch(base, mk(uint64(len(base)), 1<<40, ins("a")), 1<<20); !errors.Is(err, ErrTooLarge) {
115		t.Errorf("huge result: %v, want ErrTooLarge", err)
116	}
117}
118
119func TestParseEntryHeader(t *testing.T) {
120	mk := func(body ...byte) []byte {
121		d := append([]byte("PACK\x00\x00\x00\x02\x00\x00\x00\x01"), body...)
122		return append(d, make([]byte, 20)...)
123	}
124	// blob, size 5
125	h, err := parseEntryHeader(mk(0x35, 0x78, 0x9c), 12)
126	if err != nil || h.typ != 3 || h.size != 5 || h.data != 13 {
127		t.Fatalf("blob header = %+v %v", h, err)
128	}
129	// size 0x1234 : 4 low bits 4, then 7 bits ...
130	h, err = parseEntryHeader(mk(0xb4, 0x23, 0x78), 12) // type 3 continued
131	if err != nil || h.size != 4|0x23<<4 {
132		t.Fatalf("multi byte size = %+v %v", h, err)
133	}
134	for name, d := range map[string][]byte{
135		"unterminated":  mk(bytes.Repeat([]byte{0xbf}, 20)...),
136		"bad type 0":    mk(0x05, 0x78),
137		"bad type 5":    mk(0x55, 0x78),
138		"ofs no data":   mk(0x65),
139		"ofs outside":   mk(0x65, 0x7f, 0x78),
140		"ref truncated": mk(0x75, 1, 2, 3),
141	} {
142		off := uint64(12)
143		if _, err := parseEntryHeader(d, off); !errors.Is(err, ErrCorrupt) {
144			t.Errorf("%s: %v", name, err)
145		}
146	}
147	if _, err := parseEntryHeader(mk(0x35, 0x78), 5); !errors.Is(err, ErrCorrupt) {
148		t.Error("offset inside the pack header")
149	}
150	if _, err := parseEntryHeader(mk(0x35, 0x78), 1<<40); !errors.Is(err, ErrCorrupt) {
151		t.Error("offset far outside")
152	}
153	if _, err := parseEntryHeader(nil, 12); !errors.Is(err, ErrCorrupt) {
154		t.Error("nil pack")
155	}
156}
157
158func TestParseTreeErrors(t *testing.T) {
159	good := append([]byte("100644 a\x00"), bytes.Repeat([]byte{1}, 20)...)
160	good = append(good, append([]byte("40000 d\x00"), bytes.Repeat([]byte{2}, 20)...)...)
161	n := 0
162	if err := ParseTree(good, func(e Entry) bool { n++; return true }); err != nil || n != 2 {
163		t.Fatalf("good tree: %d %v", n, err)
164	}
165	n = 0
166	if err := ParseTree(good, func(e Entry) bool { n++; return false }); err != nil || n != 1 {
167		t.Fatalf("early stop: %d %v", n, err)
168	}
169	for name, d := range map[string][]byte{
170		"no space":     []byte("100644"),
171		"mode letters": []byte("10x644 a\x00" + strings.Repeat("a", 20)),
172		"empty mode":   []byte(" a\x00" + strings.Repeat("a", 20)),
173		"long mode":    []byte("10000000 a\x00" + strings.Repeat("a", 20)),
174		"no nul":       []byte("100644 abc"),
175		"short id":     []byte("100644 a\x00" + strings.Repeat("a", 19)),
176		"empty name":   []byte("100644 \x00" + strings.Repeat("a", 20)),
177	} {
178		if err := ParseTree(d, func(Entry) bool { return true }); !errors.Is(err, ErrCorrupt) {
179			t.Errorf("%s: %v", name, err)
180		}
181	}
182	if err := ParseTree(nil, func(Entry) bool { t.Fatal("called"); return true }); err != nil {
183		t.Errorf("empty tree: %v", err)
184	}
185}
186
187func TestParseCommitEdgeCases(t *testing.T) {
188	tree := strings.Repeat("a", 40)
189	par := strings.Repeat("b", 40)
190	var c Commit
191	// No message at all, no trailing newline.
192	if err := ParseCommit([]byte("tree "+tree+"\nauthor A <a@x> 1 +0000\ncommitter B <b@x> 2 -0130"), &c); err != nil {
193		t.Fatal(err)
194	}
195	if c.Message != nil || c.Committer.Offset != -(3600+30*60) || len(c.Parents) != 0 {
196		t.Errorf("commit = %+v", c)
197	}
198	// Two parents then reuse with zero parents keeps no stale state.
199	if err := ParseCommit([]byte("tree "+tree+"\nparent "+par+"\nparent "+tree+"\nauthor A <a@x> 1 +0000\n\nmsg\n"), &c); err != nil || len(c.Parents) != 2 || string(c.Message) != "msg\n" {
200		t.Fatalf("two parents: %+v %v", c, err)
201	}
202	if err := ParseCommit([]byte("tree "+tree+"\n\n"), &c); err != nil || len(c.Parents) != 0 || c.Author.Name != nil || len(c.Message) != 0 {
203		t.Fatalf("reuse: %+v %v", c, err)
204	}
205	// Multi-line header continuation containing something that looks like a header.
206	raw := "tree " + tree + "\nmergetag object x\n tag y\n parent " + par + "\n\n  \nauthor A <a@x> 1 +0000\ncommitter C <c@x> 5 +0100\n\nbody"
207	if err := ParseCommit([]byte(raw), &c); err != nil || len(c.Parents) != 0 {
208		t.Fatalf("continuation: %+v %v", c, err)
209	}
210	// Odd signatures.
211	for in, want := range map[string]Signature{
212		"Name <e> 12 +0200":                {Name: []byte("Name"), Email: []byte("e"), Time: 12, Offset: 7200},
213		"<> 0 +0000":                       {Name: []byte{}, Email: []byte{}},
214		"No Email 1 +0000":                 {Name: []byte("No Email 1 +0000")},
215		"A <b> notatime +0100":             {Name: []byte("A"), Email: []byte("b")},
216		"A <b> 5 +01":                      {Name: []byte("A"), Email: []byte("b"), Time: 5},
217		"A <b <c> 7 -0000":                 {Name: []byte("A"), Email: []byte("b <c"), Time: 7},
218		"A <b> 99999999999999999999 +0000": {Name: []byte("A"), Email: []byte("b")},
219	} {
220		got := parseSignature([]byte(in))
221		if !bytes.Equal(got.Name, want.Name) || !bytes.Equal(got.Email, want.Email) || got.Time != want.Time || got.Offset != want.Offset {
222			t.Errorf("parseSignature(%q) = %+v %q %q, want %+v", in, got, got.Name, got.Email, want)
223		}
224	}
225	for name, d := range map[string]string{
226		"empty":            "",
227		"no tree":          "author A <a@x> 1 +0000\n\nmsg",
228		"bad tree":         "tree xyz\n\n",
229		"short tree":       "tree " + tree[:39] + "\n\n",
230		"bad parent":       "tree " + tree + "\nparent zz\n\n",
231		"tree second":      "author A <a@x> 1 +0000\ntree " + tree + "\n\n",
232		"parent first":     "parent " + tree + "\ntree " + tree + "\n\n",
233		"blank first":      "\ntree " + tree + "\n",
234		"bad parent after": "tree " + tree + "\nparent " + tree + "\nparent x\n\n",
235	} {
236		if err := ParseCommit([]byte(d), &c); !errors.Is(err, ErrCorrupt) {
237			t.Errorf("%s: %v", name, err)
238		}
239	}
240}
241
242func TestParseTagErrors(t *testing.T) {
243	obj := strings.Repeat("c", 40)
244	var tg Tag
245	if err := ParseTag([]byte("object "+obj+"\ntype commit\ntag v1\ntagger T <t@x> 3 +0000\n\nmsg"), &tg); err != nil || tg.Type != TypeCommit || string(tg.Name) != "v1" || string(tg.Message) != "msg" {
246		t.Fatalf("tag = %+v %v", tg, err)
247	}
248	for name, d := range map[string]string{
249		"empty":    "",
250		"no type":  "object " + obj + "\n",
251		"no obj":   "type commit\n",
252		"bad type": "object " + obj + "\ntype thing\n",
253		"bad obj":  "object zz\ntype commit\n",
254	} {
255		if err := ParseTag([]byte(d), &tg); !errors.Is(err, ErrCorrupt) {
256			t.Errorf("%s: %v", name, err)
257		}
258	}
259}
260
261func TestParsePackedRefsFormats(t *testing.T) {
262	a, b, c := strings.Repeat("a", 40), strings.Repeat("b", 40), strings.Repeat("c", 40)
263	p, err := parsePackedRefs("# pack-refs with: peeled fully-peeled sorted \n" + a + " refs/heads/main\n" + b + " refs/tags/v1\n^" + c + "\n" + c + " refs/tags/v2\n")
264	if err != nil {
265		t.Fatal(err)
266	}
267	if e, _ := p.find("refs/tags/v1"); !e.peeledKnown || e.peeled.String() != c {
268		t.Errorf("v1 = %+v", e)
269	}
270	if e, _ := p.find("refs/heads/main"); !e.peeledKnown || e.peeled.String() != a {
271		t.Errorf("fully peeled: main = %+v", e)
272	}
273	// Without the header nothing is known.
274	p, err = parsePackedRefs(a + " refs/tags/v1\n")
275	if err != nil {
276		t.Fatal(err)
277	}
278	if e, _ := p.find("refs/tags/v1"); e.peeledKnown {
279		t.Errorf("unknown peel claimed known")
280	}
281	// Old "peeled" trait: tags known, branches not.
282	p, _ = parsePackedRefs("# pack-refs with: peeled\n" + a + " refs/heads/x\n" + a + " refs/tags/t\n")
283	if e, _ := p.find("refs/heads/x"); e.peeledKnown {
284		t.Error("branch peel known with the peeled trait")
285	}
286	if e, _ := p.find("refs/tags/t"); !e.peeledKnown {
287		t.Error("tag peel unknown with the peeled trait")
288	}
289	for _, bad := range []string{"^" + a + "\n", a + "\n", "zz refs/x\n", a + " \n", a + " refs/x\n^zz\n"} {
290		if _, err := parsePackedRefs(bad); !errors.Is(err, ErrCorrupt) {
291			t.Errorf("parsePackedRefs(%q) = %v", bad, err)
292		}
293	}
294}
295
296func TestValidRefName(t *testing.T) {
297	for _, ok := range []string{"refs/heads/main", "refs/heads/a/b", "refs/tags/v1.0", "refs/heads/é"} {
298		if !validRefName(ok) {
299			t.Errorf("%q rejected", ok)
300		}
301	}
302	for _, bad := range []string{"", "HEAD", "refs", "refs/", "/refs/heads/x", "refs/heads/x/", "refs/heads/../x", "refs/heads/a..b", "refs/heads/.x", "refs/heads/x.lock",
303		"refs/heads//x", "refs/heads/a b", "refs/heads/a~1", "refs/heads/a^", "refs/heads/a:b", "refs/heads/a?", "refs/heads/a*", "refs/heads/a[", "refs/heads/a\\b", "refs/heads/a@{1}", "refs/heads/\x01"} {
304		if validRefName(bad) {
305			t.Errorf("%q accepted", bad)
306		}
307	}
308}
309
310// zlibBytes compresses b.
311func zlibBytes(b []byte) []byte {
312	var out bytes.Buffer
313	w := zlib.NewWriter(&out)
314	_, _ = w.Write(b)
315	_ = w.Close()
316	return out.Bytes()
317}
318
319// entryBytes encodes a pack entry header followed by data.
320func entryBytes(typ int, size int, extra []byte, data []byte) []byte {
321	c := byte(typ<<4) | byte(size&0x0f)
322	size >>= 4
323	var out []byte
324	for size > 0 {
325		out = append(out, c|0x80)
326		c = byte(size & 0x7f)
327		size >>= 7
328	}
329	out = append(out, c)
330	out = append(out, extra...)
331	return append(out, zlibBytes(data)...)
332}
333
334// buildPack assembles a pack file and its v2 index from entries (whose ids
335// are given) and returns both.
336func buildPack(entries [][]byte, ids []ObjectID, offs *[]uint64) (pack, idx []byte) {
337	pack = append([]byte("PACK"), 0, 0, 0, 2, 0, 0, 0, byte(len(entries)))
338	for _, e := range entries {
339		*offs = append(*offs, uint64(len(pack)))
340		pack = append(pack, e...)
341	}
342	sum := sha1.Sum(pack)
343	pack = append(pack, sum[:]...)
344
345	order := make([]int, len(ids))
346	for i := range order {
347		order[i] = i
348	}
349	for i := range order { // insertion sort by id
350		for j := i; j > 0 && bytes.Compare(ids[order[j]][:], ids[order[j-1]][:]) < 0; j-- {
351			order[j], order[j-1] = order[j-1], order[j]
352		}
353	}
354	idx = append([]byte{0xff, 't', 'O', 'c', 0, 0, 0, 2}, make([]byte, 1024)...)
355	for i, k := range order {
356		for b := int(ids[k][0]); b < 256; b++ {
357			cur := binary.BigEndian.Uint32(idx[8+4*b:])
358			if cur < uint32(i+1) {
359				binary.BigEndian.PutUint32(idx[8+4*b:], uint32(i+1))
360			}
361		}
362	}
363	for _, k := range order {
364		idx = append(idx, ids[k][:]...)
365	}
366	idx = append(idx, make([]byte, 4*len(order))...) // crc
367	for _, k := range order {
368		idx = binary.BigEndian.AppendUint32(idx, uint32((*offs)[k]))
369	}
370	idx = append(idx, pack[len(pack)-20:]...)
371	isum := sha1.Sum(idx)
372	return pack, append(idx, isum[:]...)
373}
374
375func objectIDOf(typ string, data []byte) ObjectID {
376	h := sha1.New()
377	_, _ = fmt.Fprintf(h, "%s %d\x00", typ, len(data))
378	_, _ = h.Write(data)
379	var id ObjectID
380	copy(id[:], h.Sum(nil))
381	return id
382}
383
384func TestParseIdxErrors(t *testing.T) {
385	var offs []uint64
386	blob := []byte("hello")
387	_, idx := buildPack([][]byte{entryBytes(3, len(blob), nil, blob)}, []ObjectID{objectIDOf("blob", blob)}, &offs)
388	x, err := parseIdx(idx)
389	if err != nil || x.n != 1 {
390		t.Fatalf("good idx: %v", err)
391	}
392	if _, ok := x.find(&ObjectID{}); ok {
393		t.Error("found a missing id")
394	}
395	id := objectIDOf("blob", blob)
396	if pos, ok := x.find(&id); !ok || pos != 0 {
397		t.Error("id not found")
398	}
399	trunc := func(n int) []byte { return idx[:n] }
400	mut := func(f func(b []byte)) []byte { b := bytes.Clone(idx); f(b); return b }
401	for name, d := range map[string][]byte{
402		"empty":          nil,
403		"short":          trunc(100),
404		"no trailer":     trunc(len(idx) - 41),
405		"magic":          mut(func(b []byte) { b[0] = 0 }),
406		"version 3":      mut(func(b []byte) { b[7] = 3 }),
407		"fanout down":    mut(func(b []byte) { binary.BigEndian.PutUint32(b[8+4*10:], 5) }),
408		"n too big":      mut(func(b []byte) { binary.BigEndian.PutUint32(b[8+4*255:], 1<<31) }),
409		"odd large part": append(bytes.Clone(idx), 1, 2, 3),
410	} {
411		if _, err := parseIdx(d); err == nil {
412			t.Errorf("%s: accepted", name)
413		}
414	}
415	// A large offset entry pointing outside of the table.
416	d := mut(func(b []byte) {
417		off := len(b) - 40 - 4
418		binary.BigEndian.PutUint32(b[off:], 0x80000005)
419	})
420	x, err = parseIdx(d)
421	if err != nil {
422		t.Fatal(err)
423	}
424	if _, err := x.offset(0); !errors.Is(err, ErrCorrupt) {
425		t.Errorf("large offset outside table: %v", err)
426	}
427}
428
429// TestCorruptPacks mutates a real pack on disk and checks that every read
430// either succeeds with the right content or fails with ErrCorrupt, never
431// panics.
432func TestCorruptPacks(t *testing.T) {
433	// A small hand built pack: a blob, a delta against it, a ref-delta.
434	base := bytes.Repeat([]byte("0123456789abcdef"), 50)
435	res := append(bytes.Clone(base[:400]), []byte("tail")...)
436	delta := append(varint(uint64(len(base))), varint(uint64(len(res)))...)
437	delta = append(delta, copyOp(0, 400)...)
438	delta = append(delta, 4, 't', 'a', 'i', 'l')
439	baseID, resID := objectIDOf("blob", base), objectIDOf("blob", res)
440
441	var offs []uint64
442	e0 := entryBytes(3, len(base), nil, base)
443	// ofs-delta distance to entry 0 is len(e0)
444	ofs := []byte{byte(len(e0))}
445	if len(e0) >= 128 {
446		t.Fatal("fixture: e0 too long for a one byte offset")
447	}
448	e1 := entryBytes(objOfsDelta, len(delta), ofs, delta)
449	res2 := append(bytes.Clone(base[:400]), []byte("TAIL")...)
450	delta2 := append(varint(uint64(len(base))), varint(uint64(len(res2)))...)
451	delta2 = append(delta2, copyOp(0, 400)...)
452	delta2 = append(delta2, 4, 'T', 'A', 'I', 'L')
453	e2 := entryBytes(objRefDelta, len(delta2), baseID[:], delta2)
454	res2ID := objectIDOf("blob", res2)
455	pk, idx := buildPack([][]byte{e0, e1, e2}, []ObjectID{baseID, resID, res2ID}, &offs)
456
457	dir := t.TempDir()
458	write := func(pack, idxd []byte) *Repo {
459		t.Helper()
460		g := dir + "/g" + fmt.Sprint(len(pack)) + fmt.Sprint(len(idxd)) + fmt.Sprint(bytes.Count(pack, []byte{0}))
461		writeFile(t, g+"/HEAD", "ref: refs/heads/main\n")
462		writeFile(t, g+"/refs/.keep", "")
463		name := fmt.Sprintf("pack-%x", pk[len(pk)-20:])
464		writeFile(t, g+"/objects/pack/"+name+".pack", string(pack))
465		writeFile(t, g+"/objects/pack/"+name+".idx", string(idxd))
466		r, err := Open(g)
467		if err != nil {
468			return nil
469		}
470		t.Cleanup(func() { _ = r.Close() })
471		return r
472	}
473	// Intact.
474	r := write(pk, idx)
475	if r == nil {
476		t.Fatal("intact pack rejected")
477	}
478	for id, want := range map[ObjectID][]byte{baseID: base, resID: res, res2ID: res2} {
479		_, got, err := r.ReadObject(id, nil)
480		if err != nil || !bytes.Equal(got, want) {
481			t.Fatalf("intact %s: %v", id, err)
482		}
483		if _, n, err := r.ObjectHeader(id); err != nil || n != int64(len(want)) {
484			t.Fatalf("intact header %s: %d %v", id, n, err)
485		}
486	}
487	// Every single byte flipped in the pack body and truncations.
488	cases := 0
489	check := func(pack []byte) {
490		cases++
491		r := write(pack, idx)
492		if r == nil {
493			return // rejected at Open: fine
494		}
495		for _, id := range []ObjectID{baseID, resID, res2ID} {
496			_, _, err := r.ReadObject(id, nil)
497			if err != nil && !errors.Is(err, ErrCorrupt) && !errors.Is(err, ErrTooLarge) {
498				t.Fatalf("ReadObject: %v is not ErrCorrupt", err)
499			}
500			_, _, err = r.ObjectHeader(id)
501			if err != nil && !errors.Is(err, ErrCorrupt) {
502				t.Fatalf("ObjectHeader: %v is not ErrCorrupt", err)
503			}
504			if _, rc, err := r.OpenBlob(id); err == nil {
505				_, rerr := readAll(rc)
506				_ = rc.Close()
507				if rerr != nil && !errors.Is(rerr, ErrCorrupt) {
508					t.Fatalf("blob read: %v is not ErrCorrupt", rerr)
509				}
510			} else if !errors.Is(err, ErrCorrupt) && !errors.Is(err, ErrTooLarge) && !errors.Is(err, ErrWrongType) {
511				t.Fatalf("OpenBlob: %v", err)
512			}
513		}
514	}
515	for i := 12; i < len(pk)-20; i++ {
516		for _, x := range []byte{0x01, 0x80, 0xff} {
517			m := bytes.Clone(pk)
518			m[i] ^= x
519			check(m)
520		}
521	}
522	for n := 12; n < len(pk); n += 7 {
523		check(pk[:n])
524	}
525	t.Logf("%d corrupted packs", cases)
526}
527
528func readAll(rc interface{ Read([]byte) (int, error) }) ([]byte, error) {
529	var out []byte
530	buf := make([]byte, 4096)
531	for {
532		n, err := rc.Read(buf)
533		out = append(out, buf[:n]...)
534		if err != nil {
535			if err.Error() == "EOF" {
536				return out, nil
537			}
538			return out, err
539		}
540	}
541}

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