v0.7.2 ab6686c

snapshot.go RawHistoryFolder

1package vt
2
3import (
4	"fmt"
5	"math"
6)
7
8// DecodeStats describes resources declared by one canonical VT blob.
9type DecodeStats struct {
10	Chunks uint64
11	Rows   uint64
12	Cells  uint64
13	Styles uint64
14	Bytes  uint64
15}
16
17// Add adds another preflight result with overflow checking.
18func (s *DecodeStats) Add(other DecodeStats) bool {
19	for _, totals := range [][2]*uint64{{&s.Chunks, &other.Chunks}, {&s.Rows, &other.Rows}, {&s.Cells, &other.Cells}, {&s.Styles, &other.Styles}, {&s.Bytes, &other.Bytes}} {
20		if math.MaxUint64-*totals[0] < *totals[1] {
21			return false
22		}
23		*totals[0] += *totals[1]
24	}
25	return true
26}
27
28// PreflightHistoryBlob validates one self-contained history blob without
29// allocating decoded rows.
30func PreflightHistoryBlob(data []byte) (DecodeStats, error) {
31	stats, ok := preflightHistory(data)
32	if !ok {
33		return DecodeStats{}, fmt.Errorf("preflight history: %w", errInvalidHistory)
34	}
35	return DecodeStats{Chunks: stats.chunks, Rows: stats.rows, Cells: stats.cells, Styles: stats.styles, Bytes: stats.bytes}, nil
36}
37
38// MarshalHistoryChunk encodes one immutable chunk as a self-contained blob.
39func MarshalHistoryChunk(chunk *HistoryChunk) ([]byte, error) {
40	if chunk == nil {
41		return nil, fmt.Errorf("marshal history chunk: %w", errInvalidHistory)
42	}
43	return MarshalHistory(HistoryView{chunks: []*HistoryChunk{chunk}, rows: chunk.len(), cells: chunk.cells()})
44}
45
46// MarshalEmptyHistoryTail returns the mandatory canonical empty tail blob.
47func MarshalEmptyHistoryTail() ([]byte, error) { return MarshalHistory(HistoryView{}) }
48
49// MarshalHistoryTail encodes the copied mutable tail of a snapshot view as one
50// canonical history blob. It does not seal or otherwise mutate live history.
51func MarshalHistoryTail(view HistorySnapshotView) ([]byte, error) {
52	return MarshalHistory(view.Tail())
53}
54
55// MarshalSealedHistory serializes a SealAndView result as oldest-first,
56// self-contained sealed blobs plus a mandatory empty tail blob. The empty tail
57// carries NextRowID and is not the canonical default empty encoding.
58func MarshalSealedHistory(view HistoryView) ([][]byte, []byte, error) {
59	if view.rows != historyViewRowCount(view) {
60		return nil, nil, fmt.Errorf("marshal sealed history: %w", errInvalidHistory)
61	}
62	sealed := make([][]byte, len(view.chunks))
63	for i, chunk := range view.chunks {
64		blob, err := MarshalHistoryChunk(chunk)
65		if err != nil {
66			return nil, nil, err
67		}
68		sealed[i] = blob
69	}
70	tail, err := MarshalHistory(HistoryView{nextRowID: view.NextRowID()})
71	if err != nil {
72		return nil, nil, err
73	}
74	return sealed, tail, nil
75}
76
77// HistoryFromBlobs restores history directly from sealed, oldest-first blobs
78// and the mandatory tail blob. It never feeds decoded rows through Append.
79func HistoryFromBlobs(config HistoryConfig, sealed [][]byte, tail []byte) (*History, error) {
80	if err := config.Validate(); err != nil {
81		return nil, err
82	}
83	sealedViews, tailView, err := decodeRestoredHistoryBlobs(sealed, tail)
84	if err != nil {
85		return nil, err
86	}
87	return restoreHistoryViews(config, sealedViews, tailView), nil
88}
89
90func decodeRestoredHistoryBlobs(sealed [][]byte, tail []byte, extra ...HistoryView) ([]HistoryView, HistoryView, error) {
91	var seen rowIDSet
92	sealedViews := make([]HistoryView, len(sealed))
93	for i, blob := range sealed {
94		view, err := UnmarshalHistory(blob)
95		if err != nil || len(view.chunks) != 1 || view.chunks[0].len() == 0 || !validateRestoredHistoryView(view, &seen) {
96			return nil, HistoryView{}, fmt.Errorf("restore sealed history: %w", errInvalidHistory)
97		}
98		sealedViews[i] = view
99	}
100
101	tailView, err := UnmarshalHistory(tail)
102	if err != nil || !validateRestoredHistoryView(tailView, &seen) {
103		return nil, HistoryView{}, fmt.Errorf("restore history tail: %w", errInvalidHistory)
104	}
105	for _, view := range extra {
106		if !validateRestoredHistoryView(view, &seen) {
107			return nil, HistoryView{}, fmt.Errorf("restore recovery transcript: %w", errInvalidHistory)
108		}
109	}
110	return sealedViews, tailView, nil
111}
112
113func restoreHistoryViews(config HistoryConfig, sealed []HistoryView, tail HistoryView) *History {
114	h := NewHistory(config)
115	for _, view := range sealed {
116		h.nextRowID = max(h.nextRowID, view.nextRowID)
117		chunk := h.boundedRestoredChunk(view.chunks[0])
118		if chunk == nil {
119			continue
120		}
121		chunkBytes := historyChunkLogicalBytes(chunk)
122		for h.rows > h.maxRows-chunk.len() || h.logicalBytes > h.maxBytes-chunkBytes {
123			h.evictOldest()
124		}
125		h.chunks = append(h.chunks, chunk)
126		h.rows += chunk.len()
127		h.cells += chunk.cells()
128		h.logicalBytes += chunkBytes
129	}
130	h.nextRowID = max(h.nextRowID, tail.nextRowID)
131	if h.maxRows == 0 {
132		return h
133	}
134	for _, chunk := range tail.chunks {
135		for row := range chunk.len() {
136			if h.prepareChunkRowStyles(chunk, row) > h.maxBytes {
137				continue
138			}
139			for {
140				delta := h.tailAppendDelta(chunk.width)
141				if h.rows < h.maxRows && h.logicalBytes <= h.maxBytes-delta {
142					h.appendTailChunkRow(chunk, row)
143					h.recordTailRowStyles(chunk.width)
144					h.tailBytes += delta
145					h.logicalBytes += delta
146					h.tailBounds = append(h.tailBounds, chunk.bounds[row])
147					h.tailIDs = append(h.tailIDs, chunk.rowIDs[row])
148					h.rows++
149					h.cells += chunk.width
150					if len(h.tail) == h.chunkRows {
151						h.sealTail()
152					}
153					break
154				}
155				h.evictOldest()
156				h.prepareChunkRowStyles(chunk, row)
157			}
158		}
159	}
160	return h
161}
162
163func (h *History) boundedRestoredChunk(chunk *HistoryChunk) *HistoryChunk {
164	if h.maxRows == 0 || chunk == nil {
165		return nil
166	}
167	bounded := chunk
168	for bounded.count > h.maxRows || historyChunkLogicalBytes(bounded) > h.maxBytes {
169		if bounded.count == 1 {
170			return nil
171		}
172		bounded = bounded.withoutFirstRow()
173	}
174	return bounded
175}
176
177func validateRestoredHistoryView(view HistoryView, seen *rowIDSet) bool {
178	if view.nextRowID == 0 || view.nextRowID == ^RowID(0) {
179		return false
180	}
181	var maxID RowID
182	seen.begin(0)
183	for _, chunk := range view.chunks {
184		if chunk == nil || chunk.len() != len(chunk.rowIDs) || chunk.len() != len(chunk.bounds) {
185			return false
186		}
187		seen.reserve(len(chunk.rowIDs))
188		for _, id := range chunk.rowIDs {
189			if id == 0 || id >= ^RowID(0)-1 {
190				return false
191			}
192			seen.collect(id)
193			maxID = max(maxID, id)
194		}
195	}
196	return view.nextRowID > maxID && seen.commit()
197}
198
199// NewScreenWithRecoveryTranscript constructs a fresh blank screen whose
200// history contains the restored bounded history followed by the recovery
201// transcript. The transcript is decoded in full before history is restored.
202func NewScreenWithRecoveryTranscript(width, height int, config HistoryConfig, sealed [][]byte, tail, transcript []byte) (*Screen, error) {
203	if err := config.Validate(); err != nil {
204		return nil, err
205	}
206	transcriptView, err := UnmarshalHistory(transcript)
207	if err != nil {
208		return nil, fmt.Errorf("restore recovery transcript: %w", err)
209	}
210
211	sealedViews, tailView, err := decodeRestoredHistoryBlobs(sealed, tail, transcriptView)
212	if err != nil {
213		return nil, err
214	}
215	history := restoreHistoryViews(config, sealedViews, tailView)
216	remainingRows := transcriptView.rows
217	for _, chunk := range transcriptView.chunks {
218		for i := range chunk.len() {
219			row := chunk.row(i)
220			remainingRows--
221			bound := chunk.bounds[i]
222			if remainingRows == 0 {
223				bound.Soft = false
224			}
225			if err := history.AppendWithID(row, bound, chunk.rowIDs[i]); err != nil {
226				return nil, fmt.Errorf("restore recovery transcript: %w", err)
227			}
228		}
229	}
230
231	screen := NewScreenWithHistory(width, height, config)
232	screen.history = history
233	counter := max(history.nextRowID, transcriptView.nextRowID, RowID(1))
234	if counter == 0 || counter >= ^RowID(0) || uint64(height) > uint64(^RowID(0)-1-counter) {
235		return nil, fmt.Errorf("restore recovery transcript: %w", errInvalidHistory)
236	}
237	screen.nextRowID = counter - 1
238	for i := range screen.buffer.rowIDs {
239		screen.buffer.rowIDs[i] = screen.nextRowIDValue()
240	}
241	screen.frame = screen.buffer.frame
242	return screen, nil
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 2

Tags 13

↑↓ moveenter openesc close