v0.2.0 80c61e3

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

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