v0.9.1 ece7682

manifest.go RawHistoryFolder

1package snapshotcodec
2
3import (
4	"crypto/sha256"
5	"encoding/binary"
6	"fmt"
7	"hash/crc32"
8	"math"
9
10	"github.com/bnema/vev/internal/domain"
11	"github.com/bnema/vev/internal/domain/layout"
12	"github.com/bnema/vev/internal/ports"
13)
14
15const (
16	manifestMagic                = "VEVM"
17	headMagic                    = "VEVH"
18	objectMagic                  = "VEVO"
19	ManifestVersion              = uint16(5) // Objects are zlib-compressed; reject older checkpoints before loading them.
20	objectVersion                = uint16(2) // kind, raw payload length, zlib payload
21	manifestHeaderSize           = 16
22	objectEnvelopeBodyPrefixSize = 1 + 4 // object kind and payload length
23	minObjectEnvelopeSize        = manifestHeaderSize + objectEnvelopeBodyPrefixSize + 1
24	objectRefDigestSize          = sha256.Size
25	objectRefSizeFieldSize       = 4
26
27	manifestNodeLeaf  uint8 = 0
28	manifestNodeSplit uint8 = 1
29	manifestNodeStack uint8 = 2
30	manifestNodeNil   uint8 = 3
31
32	processAbsent  uint8 = 0
33	processPresent uint8 = 1
34
35	manifestWeightTag  = "WGT1"
36	manifestWeightSize = 8
37	// maxDecodedBodySize includes the object kind and payload-length prefix.
38	maxObjectEnvelopeSize = manifestHeaderSize + maxDecodedBodySize
39	maxObjectPayloadSize  = maxDecodedBodySize - objectEnvelopeBodyPrefixSize
40)
41
42// SnapshotDigest is the shared content-address type used by the repository.
43type SnapshotDigest = ports.SnapshotDigest
44
45func ManifestDigest(encoded []byte) SnapshotDigest { return sha256.Sum256(encoded) }
46
47type ObjectKind uint8
48
49const (
50	HistoryChunk ObjectKind = iota + 1
51	HistoryTail
52	RecoveryTranscript
53)
54
55type ObjectRef struct {
56	Kind   ObjectKind
57	Digest SnapshotDigest
58	Size   uint32
59}
60
61// Head is the repository-owned generation pointer. It is deliberately not a
62// VEVM payload; adapters produce and atomically manage HEAD later.
63type Head struct {
64	Generation     uint64
65	ManifestDigest SnapshotDigest
66}
67
68// MarshalHead encodes the deterministic, strict VEVH generation pointer.
69func MarshalHead(h Head) ([]byte, error) {
70	if h.Generation == 0 || isZeroDigestBytes(h.ManifestDigest[:]) {
71		return nil, fmt.Errorf("%w: head", ErrInvalidData)
72	}
73	var w payloadWriter
74	w.putUint64(h.Generation)
75	w.b = append(w.b, h.ManifestDigest[:]...)
76	return marshalManifestEnvelope(headMagic, w.b)
77}
78
79// UnmarshalHead decodes a complete VEVH generation pointer.
80func UnmarshalHead(encoded []byte) (Head, error) {
81	body, err := unmarshalManifestEnvelope(encoded, headMagic)
82	if err != nil {
83		return Head{}, err
84	}
85	if len(body) < 8+sha256.Size {
86		return Head{}, ErrShortPayload
87	}
88	if len(body) != 8+sha256.Size {
89		return Head{}, ErrTrailingBytes
90	}
91	head := Head{Generation: binary.BigEndian.Uint64(body[:8])}
92	copy(head.ManifestDigest[:], body[8:])
93	if head.Generation == 0 || isZeroDigestBytes(head.ManifestDigest[:]) {
94		return Head{}, fmt.Errorf("%w: head", ErrInvalidData)
95	}
96	return head, nil
97}
98
99// Manifest is the complete VEVM payload carried directly in a publication.
100type Manifest struct {
101	Generation       uint64
102	IncarnationID    domain.IncarnationID
103	ParentCheckpoint *domain.CheckpointRef
104	Name             string
105	CreatedAt        uint64
106	Active           uint16
107	Tabs             []ManifestTab
108}
109
110type ManifestTab struct {
111	StableID   string
112	Cols       uint16
113	Rows       uint16
114	NextPaneID uint64
115	Focus      layout.PaneID
116	Tree       *layout.Tree
117	Panes      []ManifestPane
118}
119
120type ManifestPane struct {
121	ID         layout.PaneID
122	StableID   string
123	Cwd        string
124	Sealed     []ObjectRef
125	Tail       ObjectRef
126	Transcript ObjectRef
127	Process    *Process
128}
129
130func MarshalManifest(m Manifest) ([]byte, error) {
131	if err := validateManifest(m); err != nil {
132		return nil, err
133	}
134	var w payloadWriter
135	w.putUint64(m.Generation)
136	w.b = append(w.b, m.IncarnationID[:]...)
137	if m.ParentCheckpoint == nil {
138		w.putUint8(0)
139	} else {
140		w.putUint8(1)
141		w.putUint64(m.ParentCheckpoint.Generation)
142		w.b = append(w.b, m.ParentCheckpoint.ManifestDigest[:]...)
143	}
144	if err := w.putString(m.Name); err != nil {
145		return nil, err
146	}
147	w.putUint64(m.CreatedAt)
148	w.putUint16(m.Active)
149	if len(m.Tabs) > math.MaxUint16 {
150		return nil, fmt.Errorf("%w: too many tabs", ErrInvalidData)
151	}
152	w.putUint16(uint16(len(m.Tabs)))
153	for _, tab := range m.Tabs {
154		if err := writeManifestTab(&w, tab); err != nil {
155			return nil, err
156		}
157	}
158	if err := writeManifestWeights(&w, manifestNodes(m)); err != nil {
159		return nil, err
160	}
161	return marshalManifestEnvelope(manifestMagic, w.b)
162}
163
164func UnmarshalManifest(encoded []byte) (Manifest, error) {
165	body, err := unmarshalManifestEnvelope(encoded, manifestMagic)
166	if err != nil {
167		return Manifest{}, err
168	}
169	if err := preflightManifest(body); err != nil {
170		return Manifest{}, err
171	}
172	return decodeManifest(body)
173}
174
175// decodeManifest decodes an already preflighted payload. Keeping this separate
176// allows the preflight grammar to be differentially tested against decoding.
177func decodeManifest(body []byte) (Manifest, error) {
178	r := payloadReader{b: body}
179	generation, err := r.getUint64()
180	if err != nil {
181		return Manifest{}, err
182	}
183	incarnationBytes, err := r.getBytes(len(domain.IncarnationID{}))
184	if err != nil {
185		return Manifest{}, err
186	}
187	var incarnationID domain.IncarnationID
188	copy(incarnationID[:], incarnationBytes)
189	parentPresent, err := r.getUint8()
190	if err != nil {
191		return Manifest{}, err
192	}
193	var parent *domain.CheckpointRef
194	switch parentPresent {
195	case 0:
196	case 1:
197		parent = &domain.CheckpointRef{}
198		parent.Generation, err = r.getUint64()
199		if err != nil {
200			return Manifest{}, err
201		}
202		digest, err := r.getBytes(len(parent.ManifestDigest))
203		if err != nil {
204			return Manifest{}, err
205		}
206		copy(parent.ManifestDigest[:], digest)
207	default:
208		return Manifest{}, fmt.Errorf("%w: parent checkpoint presence", ErrInvalidData)
209	}
210	name, err := r.getString()
211	if err != nil {
212		return Manifest{}, err
213	}
214	createdAt, err := r.getUint64()
215	if err != nil {
216		return Manifest{}, err
217	}
218	active, err := r.getUint16()
219	if err != nil {
220		return Manifest{}, err
221	}
222	n, err := r.getUint16()
223	if err != nil {
224		return Manifest{}, err
225	}
226	m := Manifest{Generation: generation, IncarnationID: incarnationID, ParentCheckpoint: parent, Name: name, CreatedAt: createdAt, Active: active, Tabs: make([]ManifestTab, n)}
227	for i := range m.Tabs {
228		if m.Tabs[i], err = readManifestTab(&r); err != nil {
229			return Manifest{}, err
230		}
231	}
232	if err := readManifestWeights(&r, manifestNodes(m)); err != nil {
233		return Manifest{}, err
234	}
235	if err := validateManifest(m); err != nil {
236		return Manifest{}, err
237	}
238	return m, nil
239}
240
241// MarshalObject frames payload as a zlib-compressed VEVO object and returns the
242// bytes together with the SHA-256 address of those complete bytes. Encoding is
243// deterministic, so equal payloads still share one content address.
244func MarshalObject(kind ObjectKind, payload []byte) (ports.SnapshotObject, error) {
245	if !validObjectKind(kind) || len(payload) == 0 || len(payload) > maxObjectPayloadSize {
246		return ports.SnapshotObject{}, fmt.Errorf("%w: object", ErrInvalidData)
247	}
248	var w payloadWriter
249	w.putUint8(uint8(kind))
250	w.putUint32(uint32(len(payload)))
251	body, err := compressObjectPayload(w.b, payload)
252	if err != nil {
253		return ports.SnapshotObject{}, err
254	}
255	data, err := marshalManifestEnvelope(objectMagic, body)
256	if err != nil {
257		return ports.SnapshotObject{}, err
258	}
259	return ports.SnapshotObject{Digest: sha256.Sum256(data), Data: data}, nil
260}
261
262// UnmarshalObject verifies VEVO framing and returns the inflated payload. It
263// requires exactly the declared payload length and a clean zlib end of stream.
264func UnmarshalObject(encoded []byte) (ObjectKind, []byte, error) {
265	kind, rawLen, compressed, err := preflightObject(encoded)
266	if err != nil {
267		return 0, nil, err
268	}
269	payload, err := inflateObjectPayload(compressed, rawLen)
270	if err != nil {
271		return 0, nil, err
272	}
273	return kind, payload, nil
274}
275
276// PreflightObject verifies VEVO framing (magic, version, length, CRC, kind, and
277// declared payload length) without inflating it. Repository adapters use it to
278// accept or reject stored objects cheaply; UnmarshalObject decodes the payload.
279func PreflightObject(encoded []byte) (ObjectKind, error) {
280	kind, _, _, err := preflightObject(encoded)
281	return kind, err
282}
283
284func preflightObject(encoded []byte) (ObjectKind, uint32, []byte, error) {
285	body, err := unmarshalManifestEnvelope(encoded, objectMagic)
286	if err != nil {
287		return 0, 0, nil, err
288	}
289	if len(body) < objectEnvelopeBodyPrefixSize+1 {
290		return 0, 0, nil, ErrShortPayload
291	}
292	kind := ObjectKind(body[0])
293	if !validObjectKind(kind) {
294		return 0, 0, nil, fmt.Errorf("%w: object kind", ErrInvalidData)
295	}
296	n := binary.BigEndian.Uint32(body[1:5])
297	if n == 0 || n > maxObjectPayloadSize {
298		return 0, 0, nil, fmt.Errorf("%w: object length", ErrInvalidData)
299	}
300	return kind, n, body[objectEnvelopeBodyPrefixSize:], nil
301}
302
303func marshalManifestEnvelope(magic string, body []byte) ([]byte, error) {
304	if len(body) > maxDecodedBodySize || len(body) > math.MaxUint32 {
305		return nil, fmt.Errorf("%w: body too large", ErrInvalidData)
306	}
307	out := make([]byte, manifestHeaderSize+len(body))
308	copy(out[:4], magic)
309	binary.BigEndian.PutUint16(out[4:6], envelopeVersion(magic))
310	binary.BigEndian.PutUint32(out[8:12], uint32(len(body)))
311	binary.BigEndian.PutUint32(out[12:16], crc32.ChecksumIEEE(body))
312	copy(out[16:], body)
313	return out, nil
314}
315
316func envelopeVersion(magic string) uint16 {
317	switch magic {
318	case manifestMagic:
319		return ManifestVersion
320	case objectMagic:
321		return objectVersion
322	}
323	return 1
324}
325
326func unmarshalManifestEnvelope(encoded []byte, expectedMagic string) ([]byte, error) {
327	if len(encoded) < manifestHeaderSize {
328		return nil, ErrShortPayload
329	}
330	if string(encoded[:4]) != expectedMagic {
331		return nil, ErrBadMagic
332	}
333	if binary.BigEndian.Uint16(encoded[4:6]) != envelopeVersion(expectedMagic) {
334		return nil, ErrBadVersion
335	}
336	if binary.BigEndian.Uint16(encoded[6:8]) != 0 {
337		return nil, fmt.Errorf("%w: flags", ErrInvalidData)
338	}
339	n := binary.BigEndian.Uint32(encoded[8:12])
340	if n > maxDecodedBodySize {
341		return nil, fmt.Errorf("%w: body too large", ErrInvalidData)
342	}
343	if uint64(len(encoded)) < uint64(manifestHeaderSize)+uint64(n) {
344		return nil, ErrShortPayload
345	}
346	if uint64(len(encoded)) != uint64(manifestHeaderSize)+uint64(n) {
347		return nil, ErrTrailingBytes
348	}
349	body := encoded[manifestHeaderSize:]
350	if crc32.ChecksumIEEE(body) != binary.BigEndian.Uint32(encoded[12:16]) {
351		return nil, ErrBadCRC
352	}
353	return body, nil
354}
355
356func writeManifestTab(w *payloadWriter, tab ManifestTab) error {
357	if err := w.putString(tab.StableID); err != nil {
358		return err
359	}
360	w.putUint16(tab.Cols)
361	w.putUint16(tab.Rows)
362	w.putUint64(tab.NextPaneID)
363	if err := w.putString(string(tab.Focus)); err != nil {
364		return err
365	}
366	var root *layout.Node
367	if tab.Tree != nil {
368		root = tab.Tree.Root
369	}
370	if err := writeNode(w, root); err != nil {
371		return err
372	}
373	if len(tab.Panes) > math.MaxUint16 {
374		return fmt.Errorf("%w: too many panes", ErrInvalidData)
375	}
376	w.putUint16(uint16(len(tab.Panes)))
377	for _, pane := range tab.Panes {
378		if err := writeManifestPane(w, pane); err != nil {
379			return err
380		}
381	}
382	return nil
383}
384func writeManifestWeights(w *payloadWriter, nodes []*layout.Node) error {
385	hasWeights := false
386	for _, node := range nodes {
387		if node.Weight != 0 {
388			hasWeights = true
389			break
390		}
391	}
392	if !hasWeights {
393		return nil
394	}
395	if len(nodes) > math.MaxUint32 {
396		return fmt.Errorf("%w: too many weighted nodes", ErrInvalidData)
397	}
398	w.b = append(w.b, manifestWeightTag...)
399	w.putUint32(uint32(len(nodes)))
400	for _, node := range nodes {
401		w.putUint64(math.Float64bits(node.Weight))
402	}
403	return nil
404}
405
406func readManifestWeights(r *payloadReader, nodes []*layout.Node) error {
407	return readManifestWeightExtension(r, uint64(len(nodes)), func(i int, weight float64) {
408		nodes[i].Weight = weight
409	})
410}
411
412// readManifestWeightExtension validates the optional weight extension and
413// calls assign for each decoded weight when assignment is required.
414func readManifestWeightExtension(r *payloadReader, wantNodes uint64, assign func(int, float64)) error {
415	if len(r.b) == 0 {
416		return nil
417	}
418	if len(r.b) < len(manifestWeightTag) {
419		return ErrShortPayload
420	}
421	if string(r.b[:len(manifestWeightTag)]) != manifestWeightTag {
422		return fmt.Errorf("%w: weight extension tag", ErrInvalidData)
423	}
424	r.b = r.b[len(manifestWeightTag):]
425	count, err := r.getUint32()
426	if err != nil {
427		return err
428	}
429	if uint64(count) != wantNodes {
430		return fmt.Errorf("%w: weight node count", ErrInvalidData)
431	}
432	if uint64(count) > uint64(len(r.b))/manifestWeightSize {
433		return ErrShortPayload
434	}
435	if uint64(len(r.b)) != uint64(count)*manifestWeightSize {
436		return ErrTrailingBytes
437	}
438	for i := range count {
439		bits, err := r.getUint64()
440		if err != nil {
441			return err
442		}
443		weight := math.Float64frombits(bits)
444		if !validManifestWeight(weight) {
445			return fmt.Errorf("%w: node weight", ErrInvalidData)
446		}
447		if assign != nil {
448			assign(int(i), weight)
449		}
450	}
451	return r.done()
452}
453
454func manifestNodes(m Manifest) []*layout.Node {
455	var nodes []*layout.Node
456	var visit func(*layout.Node)
457	visit = func(node *layout.Node) {
458		if node == nil {
459			return
460		}
461		nodes = append(nodes, node)
462		for _, child := range node.Children {
463			visit(child)
464		}
465	}
466	for _, tab := range m.Tabs {
467		if tab.Tree != nil {
468			visit(tab.Tree.Root)
469		}
470	}
471	return nodes
472}
473
474func validManifestWeight(weight float64) bool {
475	return weight >= 0 && !math.IsNaN(weight) && !math.IsInf(weight, 0)
476}
477
478func writeManifestPane(w *payloadWriter, pane ManifestPane) error {
479	if err := w.putString(string(pane.ID)); err != nil {
480		return err
481	}
482	if err := w.putString(pane.StableID); err != nil {
483		return err
484	}
485	if err := w.putString(pane.Cwd); err != nil {
486		return err
487	}
488	if len(pane.Sealed) > math.MaxUint16 {
489		return fmt.Errorf("%w: too many sealed references", ErrInvalidData)
490	}
491	w.putUint16(uint16(len(pane.Sealed)))
492	for _, ref := range pane.Sealed {
493		writeObjectRef(w, ref)
494	}
495	writeObjectRef(w, pane.Tail)
496	writeObjectRef(w, pane.Transcript)
497	return writeProcess(w, pane.Process)
498}
499func writeObjectRef(w *payloadWriter, ref ObjectRef) {
500	w.putUint8(uint8(ref.Kind))
501	w.b = append(w.b, ref.Digest[:]...)
502	w.putUint32(ref.Size)
503}
504
505func readManifestTab(r *payloadReader) (ManifestTab, error) {
506	stableID, err := r.getString()
507	if err != nil {
508		return ManifestTab{}, err
509	}
510	cols, err := r.getUint16()
511	if err != nil {
512		return ManifestTab{}, err
513	}
514	rows, err := r.getUint16()
515	if err != nil {
516		return ManifestTab{}, err
517	}
518	next, err := r.getUint64()
519	if err != nil {
520		return ManifestTab{}, err
521	}
522	focus, err := r.getString()
523	if err != nil {
524		return ManifestTab{}, err
525	}
526	root, err := readNode(r)
527	if err != nil {
528		return ManifestTab{}, err
529	}
530	n, err := r.getUint16()
531	if err != nil {
532		return ManifestTab{}, err
533	}
534	tab := ManifestTab{StableID: stableID, Cols: cols, Rows: rows, NextPaneID: next, Focus: layout.PaneID(focus), Tree: &layout.Tree{Root: root, Focus: layout.PaneID(focus)}, Panes: make([]ManifestPane, n)}
535	for i := range tab.Panes {
536		if tab.Panes[i], err = readManifestPane(r); err != nil {
537			return ManifestTab{}, err
538		}
539	}
540	return tab, nil
541}
542func readManifestPane(r *payloadReader) (ManifestPane, error) {
543	id, err := r.getString()
544	if err != nil {
545		return ManifestPane{}, err
546	}
547	stableID, err := r.getString()
548	if err != nil {
549		return ManifestPane{}, err
550	}
551	cwd, err := r.getString()
552	if err != nil {
553		return ManifestPane{}, err
554	}
555	n, err := r.getUint16()
556	if err != nil {
557		return ManifestPane{}, err
558	}
559	pane := ManifestPane{ID: layout.PaneID(id), StableID: stableID, Cwd: cwd, Sealed: make([]ObjectRef, n)}
560	for i := range pane.Sealed {
561		if pane.Sealed[i], err = readObjectRef(r); err != nil {
562			return ManifestPane{}, err
563		}
564	}
565	if pane.Tail, err = readObjectRef(r); err != nil {
566		return ManifestPane{}, err
567	}
568	if pane.Transcript, err = readObjectRef(r); err != nil {
569		return ManifestPane{}, err
570	}
571	pane.Process, err = readProcess(r)
572	return pane, err
573}
574func readObjectRef(r *payloadReader) (ObjectRef, error) {
575	kind, digestBytes, size, err := parseObjectRef(r)
576	if err != nil {
577		return ObjectRef{}, err
578	}
579	var digest SnapshotDigest
580	copy(digest[:], digestBytes)
581	return ObjectRef{Kind: kind, Digest: digest, Size: size}, nil
582}
583
584// parseObjectRef advances r over an encoded object reference without copying
585// its digest. Callers that retain the digest must copy it before reusing input.
586func parseObjectRef(r *payloadReader) (ObjectKind, []byte, uint32, error) {
587	kind, err := r.getUint8()
588	if err != nil {
589		return 0, nil, 0, err
590	}
591	if len(r.b) < objectRefDigestSize+objectRefSizeFieldSize {
592		return 0, nil, 0, ErrShortPayload
593	}
594	digest := r.b[:objectRefDigestSize]
595	r.b = r.b[objectRefDigestSize:]
596	size, err := r.getUint32()
597	if err != nil {
598		return 0, nil, 0, err
599	}
600	return ObjectKind(kind), digest, size, nil
601}
602
603func validateManifest(m Manifest) error {
604	if m.Generation == 0 {
605		return fmt.Errorf("%w: manifest generation", ErrInvalidData)
606	}
607	if m.IncarnationID == (domain.IncarnationID{}) {
608		return fmt.Errorf("%w: manifest incarnation", ErrInvalidData)
609	}
610	if m.ParentCheckpoint != nil {
611		if m.ParentCheckpoint.Generation == 0 || m.ParentCheckpoint.Generation >= m.Generation || isZeroDigestBytes(m.ParentCheckpoint.ManifestDigest[:]) {
612			return fmt.Errorf("%w: parent checkpoint", ErrInvalidData)
613		}
614	}
615	if m.Name == "" {
616		return fmt.Errorf("%w: manifest name", ErrInvalidData)
617	}
618	if err := validateActive(m.Active, len(m.Tabs)); err != nil {
619		return err
620	}
621	for _, node := range manifestNodes(m) {
622		if !validManifestWeight(node.Weight) {
623			return fmt.Errorf("%w: node weight", ErrInvalidData)
624		}
625	}
626	for _, tab := range m.Tabs {
627		if !safeDimensions(tab.Cols, tab.Rows) {
628			return fmt.Errorf("%w: dimensions", ErrInvalidData)
629		}
630		ids := make(map[layout.PaneID]struct{}, len(tab.Panes))
631		for _, pane := range tab.Panes {
632			if pane.ID == "" {
633				return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
634			}
635			if _, exists := ids[pane.ID]; exists {
636				return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
637			}
638			if err := validatePaneRefs(pane); err != nil {
639				return err
640			}
641			ids[pane.ID] = struct{}{}
642		}
643		if _, ok := ids[tab.Focus]; tab.Focus != "" && !ok {
644			return ErrUnknownPane
645		}
646		if err := validateTree(tab.Tree, ids); err != nil {
647			return err
648		}
649	}
650	return nil
651}
652func safeDimensions(cols, rows uint16) bool {
653	return cols != 0 && rows != 0 && uint64(cols)*uint64(rows) <= maxSnapshotCells
654}
655func validatePaneRefs(p ManifestPane) error {
656	for _, ref := range p.Sealed {
657		if err := validateObjectRef(ref, HistoryChunk); err != nil {
658			return err
659		}
660	}
661	if err := validateObjectRef(p.Tail, HistoryTail); err != nil {
662		return err
663	}
664	return validateObjectRef(p.Transcript, RecoveryTranscript)
665}
666func validateObjectRef(ref ObjectRef, want ObjectKind) error {
667	return validateObjectRefFields(ref.Kind, ref.Digest[:], ref.Size, want)
668}
669
670func validateObjectRefFields(kind ObjectKind, digest []byte, size uint32, want ObjectKind) error {
671	if kind != want || !validObjectEnvelopeSize(size) || isZeroDigestBytes(digest) {
672		return fmt.Errorf("%w: object reference", ErrInvalidData)
673	}
674	return nil
675}
676func validObjectEnvelopeSize(size uint32) bool {
677	return size >= minObjectEnvelopeSize && size <= maxObjectEnvelopeSize
678}
679
680func validObjectKind(kind ObjectKind) bool {
681	return kind == HistoryChunk || kind == HistoryTail || kind == RecoveryTranscript
682}
683func isZeroDigestBytes(d []byte) bool {
684	for _, b := range d {
685		if b != 0 {
686			return false
687		}
688	}
689	return true
690}

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 12

Tags 11

↑↓ moveenter openesc close