v0.3.0 99dc266

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

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