v0.1.0 7deb448

record.go RawHistoryFolder

1package persist
2
3import (
4	"encoding/binary"
5	"errors"
6	"fmt"
7	"math"
8
9	"github.com/bnema/vev/internal/domain"
10)
11
12const catalogueRecordVersion uint16 = 3
13
14var errMalformedRecord = errors.New("persist: malformed catalogue record")
15
16var catalogueMagic = [4]byte{'V', 'E', 'V', 'C'}
17
18// Record is the durable catalogue record used by daemon code.
19type Record = domain.CatalogueRecord
20
21func encodeRecordValue(record domain.CatalogueRecord) ([]byte, error) {
22	if err := record.Validate(); err != nil {
23		return nil, fmt.Errorf("persist: invalid catalogue record: %w", err)
24	}
25	buf := make([]byte, 0, 128)
26	buf = append(buf, catalogueMagic[:]...)
27	buf = binary.BigEndian.AppendUint16(buf, catalogueRecordVersion)
28	buf = append(buf, record.IncarnationID[:]...)
29	var err error
30	buf, err = appendCheckedString(buf, record.Cwd)
31	if err != nil {
32		return nil, err
33	}
34	buf = binary.BigEndian.AppendUint64(buf, uint64(record.CreatedAt))
35	buf = binary.BigEndian.AppendUint64(buf, uint64(record.UpdatedAt))
36	buf = binary.BigEndian.AppendUint64(buf, record.LastUsedSeq)
37	if uint64(len(record.TabNames)) > math.MaxUint32 {
38		return nil, errors.New("persist: too many tab names")
39	}
40	buf = binary.BigEndian.AppendUint32(buf, uint32(len(record.TabNames)))
41	for _, name := range record.TabNames {
42		buf, err = appendCheckedString(buf, name)
43		if err != nil {
44			return nil, err
45		}
46	}
47	buf = appendRef(buf, record.Committed)
48	buf, err = appendCheckedString(buf, record.DegradedReason)
49	return buf, err
50}
51
52func decodeRecordValue(name string, value []byte) (domain.CatalogueRecord, error) {
53	if err := domain.ValidateSessionName(name); err != nil {
54		return domain.CatalogueRecord{}, err
55	}
56	reader := valueReader{data: value}
57	magic, ok := reader.take(4)
58	if !ok || string(magic) != string(catalogueMagic[:]) {
59		return domain.CatalogueRecord{}, errMalformedRecord
60	}
61	version, ok := reader.u16()
62	if !ok || version != catalogueRecordVersion {
63		return domain.CatalogueRecord{}, errMalformedRecord
64	}
65	id, ok := reader.take(16)
66	if !ok {
67		return domain.CatalogueRecord{}, errMalformedRecord
68	}
69	record := domain.CatalogueRecord{Name: name}
70	copy(record.IncarnationID[:], id)
71	var valid bool
72	if record.Cwd, valid = reader.str(); !valid {
73		return domain.CatalogueRecord{}, errMalformedRecord
74	}
75	created, ok := reader.u64()
76	if !ok {
77		return domain.CatalogueRecord{}, errMalformedRecord
78	}
79	record.CreatedAt = int64(created)
80	updated, ok := reader.u64()
81	if !ok {
82		return domain.CatalogueRecord{}, errMalformedRecord
83	}
84	record.UpdatedAt = int64(updated)
85	if record.LastUsedSeq, ok = reader.u64(); !ok {
86		return domain.CatalogueRecord{}, errMalformedRecord
87	}
88	count, ok := reader.u32()
89	if !ok || uint64(count) > uint64(reader.remaining())/4 {
90		return domain.CatalogueRecord{}, errMalformedRecord
91	}
92	if count > 0 {
93		record.TabNames = make([]string, 0, int(count))
94	}
95	for range count {
96		tab, ok := reader.str()
97		if !ok {
98			return domain.CatalogueRecord{}, errMalformedRecord
99		}
100		record.TabNames = append(record.TabNames, tab)
101	}
102	if record.Committed, ok = reader.ref(); !ok {
103		return domain.CatalogueRecord{}, errMalformedRecord
104	}
105	if record.DegradedReason, ok = reader.str(); !ok || reader.remaining() != 0 {
106		return domain.CatalogueRecord{}, errMalformedRecord
107	}
108	if err := record.Validate(); err != nil {
109		return domain.CatalogueRecord{}, errors.Join(errMalformedRecord, fmt.Errorf("persist: invalid catalogue record: %w", err))
110	}
111	return record, nil
112}
113
114func appendCheckedString(buf []byte, value string) ([]byte, error) {
115	if uint64(len(value)) > math.MaxUint32 {
116		return nil, errors.New("persist: string too large")
117	}
118	buf = binary.BigEndian.AppendUint32(buf, uint32(len(value)))
119	return append(buf, value...), nil
120}
121func appendRef(buf []byte, ref *domain.CheckpointRef) []byte {
122	if ref == nil {
123		return append(buf, 0)
124	}
125	buf = append(buf, 1)
126	buf = binary.BigEndian.AppendUint64(buf, ref.Generation)
127	return append(buf, ref.ManifestDigest[:]...)
128}
129
130type valueReader struct {
131	data []byte
132	off  int
133}
134
135func (r *valueReader) remaining() int { return len(r.data) - r.off }
136func (r *valueReader) take(n int) ([]byte, bool) {
137	if n < 0 || n > r.remaining() {
138		return nil, false
139	}
140	value := r.data[r.off : r.off+n]
141	r.off += n
142	return value, true
143}
144func (r *valueReader) byte() (byte, bool) {
145	value, ok := r.take(1)
146	if !ok {
147		return 0, false
148	}
149	return value[0], true
150}
151func (r *valueReader) u16() (uint16, bool) {
152	value, ok := r.take(2)
153	if !ok {
154		return 0, false
155	}
156	return binary.BigEndian.Uint16(value), true
157}
158func (r *valueReader) u32() (uint32, bool) {
159	value, ok := r.take(4)
160	if !ok {
161		return 0, false
162	}
163	return binary.BigEndian.Uint32(value), true
164}
165func (r *valueReader) u64() (uint64, bool) {
166	value, ok := r.take(8)
167	if !ok {
168		return 0, false
169	}
170	return binary.BigEndian.Uint64(value), true
171}
172func (r *valueReader) str() (string, bool) {
173	size, ok := r.u32()
174	if !ok || uint64(size) > uint64(r.remaining()) {
175		return "", false
176	}
177	value, _ := r.take(int(size))
178	return string(value), true
179}
180func (r *valueReader) ref() (*domain.CheckpointRef, bool) {
181	present, ok := r.byte()
182	if !ok {
183		return nil, false
184	}
185	if present == 0 {
186		return nil, true
187	}
188	if present != 1 {
189		return nil, false
190	}
191	generation, ok := r.u64()
192	if !ok {
193		return nil, false
194	}
195	digest, ok := r.take(32)
196	if !ok {
197		return nil, false
198	}
199	ref := &domain.CheckpointRef{Generation: generation}
200	copy(ref.ManifestDigest[:], digest)
201	return ref, true
202}

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