detached 8e5d334

feat(vt): capture history without sealing tails

commit
8e5d334b29f1cfd4a6e215d3a6e88010e40c4603
author
brice · · 2026-07-21 18:46 UTC
parent
d75df92
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +70 -0
M pkg/vt/snapshot.go +8 -0
@@ -54,6 +54,12 @@ func MarshalHistoryChunk(chunk *HistoryChunk) ([]byte, error) {
5454 // MarshalEmptyHistoryTail returns the mandatory canonical empty tail blob.
5555 func MarshalEmptyHistoryTail() ([]byte, error) { return MarshalHistory(HistoryView{}) }
5656
57+// MarshalHistoryTail encodes the copied mutable tail of a snapshot view as one
58+// canonical history blob. It does not seal or otherwise mutate live history.
59+func MarshalHistoryTail(view HistorySnapshotView) ([]byte, error) {
60+ return MarshalHistory(view.Tail())
61+}
62+
5763 // MarshalSealedHistory serializes a SealAndView result as oldest-first,
5864 // self-contained sealed blobs plus a mandatory empty canonical tail blob.
5965 func MarshalSealedHistory(view HistoryView) ([][]byte, []byte, error) {
@@ -86,6 +92,7 @@ func HistoryFromBlobs(config HistoryConfig, sealed [][]byte, tail []byte) (*Hist
8692 }
8793 h.chunks = append(h.chunks, view.chunks[0])
8894 h.rows += len(view.chunks[0].rows)
95+ h.cells += view.Cells()
8996 }
9097 view, err := UnmarshalHistory(tail)
9198 if err != nil || len(view.chunks) > 1 {
@@ -94,6 +101,7 @@ func HistoryFromBlobs(config HistoryConfig, sealed [][]byte, tail []byte) (*Hist
94101 if len(view.chunks) == 1 {
95102 h.tail = view.chunks[0].rows
96103 h.rows += len(h.tail)
104+ h.cells += view.Cells()
97105 }
98106 h.evict()
99107 return h, nil
@@ -54,6 +54,12 @@ func MarshalHistoryChunk(chunk *HistoryChunk) ([]byte, error) {
54 // MarshalEmptyHistoryTail returns the mandatory canonical empty tail blob.54 // MarshalEmptyHistoryTail returns the mandatory canonical empty tail blob.
55 func MarshalEmptyHistoryTail() ([]byte, error) { return MarshalHistory(HistoryView{}) }55 func MarshalEmptyHistoryTail() ([]byte, error) { return MarshalHistory(HistoryView{}) }
56 56
57+// MarshalHistoryTail encodes the copied mutable tail of a snapshot view as one
58+// canonical history blob. It does not seal or otherwise mutate live history.
59+func MarshalHistoryTail(view HistorySnapshotView) ([]byte, error) {
60+ return MarshalHistory(view.Tail())
61+}
62+
57 // MarshalSealedHistory serializes a SealAndView result as oldest-first,63 // MarshalSealedHistory serializes a SealAndView result as oldest-first,
58 // self-contained sealed blobs plus a mandatory empty canonical tail blob.64 // self-contained sealed blobs plus a mandatory empty canonical tail blob.
59 func MarshalSealedHistory(view HistoryView) ([][]byte, []byte, error) {65 func MarshalSealedHistory(view HistoryView) ([][]byte, []byte, error) {
@@ -86,6 +92,7 @@ func HistoryFromBlobs(config HistoryConfig, sealed [][]byte, tail []byte) (*Hist
86 }92 }
87 h.chunks = append(h.chunks, view.chunks[0])93 h.chunks = append(h.chunks, view.chunks[0])
88 h.rows += len(view.chunks[0].rows)94 h.rows += len(view.chunks[0].rows)
95+ h.cells += view.Cells()
89 }96 }
90 view, err := UnmarshalHistory(tail)97 view, err := UnmarshalHistory(tail)
91 if err != nil || len(view.chunks) > 1 {98 if err != nil || len(view.chunks) > 1 {
@@ -94,6 +101,7 @@ func HistoryFromBlobs(config HistoryConfig, sealed [][]byte, tail []byte) (*Hist
94 if len(view.chunks) == 1 {101 if len(view.chunks) == 1 {
95 h.tail = view.chunks[0].rows102 h.tail = view.chunks[0].rows
96 h.rows += len(h.tail)103 h.rows += len(h.tail)
104+ h.cells += view.Cells()
97 }105 }
98 h.evict()106 h.evict()
99 return h, nil107 return h, nil
M pkg/vt/snapshot_test.go +62 -0
@@ -7,6 +7,68 @@ import (
77 "github.com/stretchr/testify/require"
88 )
99
10+func TestHistorySnapshotViewDoesNotSealTailAndCopiesIt(t *testing.T) {
11+ history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2})
12+ if err := history.Append(historyRow("AAAA")); err != nil {
13+ t.Fatal(err)
14+ }
15+ if err := history.Append(historyRow("BBBB")); err != nil {
16+ t.Fatal(err)
17+ }
18+ if err := history.Append(historyRow("CCCC")); err != nil {
19+ t.Fatal(err)
20+ }
21+ sealed := history.View().Chunk(0)
22+
23+ view := history.SnapshotView()
24+ if got, want := view.ChunkCount(), 1; got != want {
25+ t.Fatalf("sealed chunks = %d, want %d", got, want)
26+ }
27+ if view.Chunk(0) != sealed {
28+ t.Fatal("snapshot copied an immutable sealed chunk")
29+ }
30+ if got, want := len(history.tail), 1; got != want {
31+ t.Fatalf("live tail rows = %d, want %d; snapshot sealed it", got, want)
32+ }
33+ history.tail[0][0].Rune = 'X'
34+ if got, want := rowText(view.Tail().Row(0)), "CCCC"; got != want {
35+ t.Fatalf("snapshot tail = %q, want %q", got, want)
36+ }
37+ if got, want := view.Len(), 3; got != want {
38+ t.Fatalf("snapshot rows = %d, want %d", got, want)
39+ }
40+ if got, want := view.Cells(), 12; got != want {
41+ t.Fatalf("snapshot cells = %d, want %d", got, want)
42+ }
43+
44+ tail, err := MarshalHistoryTail(view)
45+ require.NoError(t, err)
46+ decoded, err := UnmarshalHistory(tail)
47+ require.NoError(t, err)
48+ require.Equal(t, []string{"CCCC"}, historyViewTexts(decoded))
49+}
50+
51+func TestHistoryFromBlobsRestoresCellAccountingAndEvictsToBothBudgets(t *testing.T) {
52+ history := NewHistory(HistoryConfig{MaxRows: 4, MaxCells: 10, ChunkRows: 2})
53+ for _, text := range []string{"a", "b", "cc"} {
54+ require.NoError(t, history.Append(historyRow(text)))
55+ }
56+ view := history.SnapshotView()
57+ sealed := make([][]byte, view.ChunkCount())
58+ for i := range sealed {
59+ var err error
60+ sealed[i], err = MarshalHistoryChunk(view.Chunk(i))
61+ require.NoError(t, err)
62+ }
63+ tail, err := MarshalHistoryTail(view)
64+ require.NoError(t, err)
65+
66+ restored, err := HistoryFromBlobs(HistoryConfig{MaxRows: 4, MaxCells: 2, ChunkRows: 2}, sealed, tail)
67+ require.NoError(t, err)
68+ require.Equal(t, []string{"cc"}, historyViewTexts(restored.View()))
69+ require.Equal(t, 2, restored.Cells())
70+}
71+
1072 func TestPrimaryVisibleSnapshotRetainsReflowBoundaries(t *testing.T) {
1173 s := NewScreen(5, 3)
1274 s.Write([]byte("abcdefgh"))
@@ -7,6 +7,68 @@ import (
7 "github.com/stretchr/testify/require"7 "github.com/stretchr/testify/require"
8 )8 )
9 9
10+func TestHistorySnapshotViewDoesNotSealTailAndCopiesIt(t *testing.T) {
11+ history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2})
12+ if err := history.Append(historyRow("AAAA")); err != nil {
13+ t.Fatal(err)
14+ }
15+ if err := history.Append(historyRow("BBBB")); err != nil {
16+ t.Fatal(err)
17+ }
18+ if err := history.Append(historyRow("CCCC")); err != nil {
19+ t.Fatal(err)
20+ }
21+ sealed := history.View().Chunk(0)
22+
23+ view := history.SnapshotView()
24+ if got, want := view.ChunkCount(), 1; got != want {
25+ t.Fatalf("sealed chunks = %d, want %d", got, want)
26+ }
27+ if view.Chunk(0) != sealed {
28+ t.Fatal("snapshot copied an immutable sealed chunk")
29+ }
30+ if got, want := len(history.tail), 1; got != want {
31+ t.Fatalf("live tail rows = %d, want %d; snapshot sealed it", got, want)
32+ }
33+ history.tail[0][0].Rune = 'X'
34+ if got, want := rowText(view.Tail().Row(0)), "CCCC"; got != want {
35+ t.Fatalf("snapshot tail = %q, want %q", got, want)
36+ }
37+ if got, want := view.Len(), 3; got != want {
38+ t.Fatalf("snapshot rows = %d, want %d", got, want)
39+ }
40+ if got, want := view.Cells(), 12; got != want {
41+ t.Fatalf("snapshot cells = %d, want %d", got, want)
42+ }
43+
44+ tail, err := MarshalHistoryTail(view)
45+ require.NoError(t, err)
46+ decoded, err := UnmarshalHistory(tail)
47+ require.NoError(t, err)
48+ require.Equal(t, []string{"CCCC"}, historyViewTexts(decoded))
49+}
50+
51+func TestHistoryFromBlobsRestoresCellAccountingAndEvictsToBothBudgets(t *testing.T) {
52+ history := NewHistory(HistoryConfig{MaxRows: 4, MaxCells: 10, ChunkRows: 2})
53+ for _, text := range []string{"a", "b", "cc"} {
54+ require.NoError(t, history.Append(historyRow(text)))
55+ }
56+ view := history.SnapshotView()
57+ sealed := make([][]byte, view.ChunkCount())
58+ for i := range sealed {
59+ var err error
60+ sealed[i], err = MarshalHistoryChunk(view.Chunk(i))
61+ require.NoError(t, err)
62+ }
63+ tail, err := MarshalHistoryTail(view)
64+ require.NoError(t, err)
65+
66+ restored, err := HistoryFromBlobs(HistoryConfig{MaxRows: 4, MaxCells: 2, ChunkRows: 2}, sealed, tail)
67+ require.NoError(t, err)
68+ require.Equal(t, []string{"cc"}, historyViewTexts(restored.View()))
69+ require.Equal(t, 2, restored.Cells())
70+}
71+
10 func TestPrimaryVisibleSnapshotRetainsReflowBoundaries(t *testing.T) {72 func TestPrimaryVisibleSnapshotRetainsReflowBoundaries(t *testing.T) {
11 s := NewScreen(5, 3)73 s := NewScreen(5, 3)
12 s.Write([]byte("abcdefgh"))74 s.Write([]byte("abcdefgh"))

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