detached d2b05f9

fix(vt): align history codec limits

commit
d2b05f96e2430c598579a7a071b333ddc7d1fcfc
author
brice · · 2026-07-12 15:43 UTC
parent
a23e755
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +139 -16
M pkg/vt/history_codec.go +42 -14
@@ -16,13 +16,16 @@ const (
1616 historyCellBytes = 41
1717 maxHistoryChunkRows = 256
1818
19- // Preserve ten-thousand-row histories while bounding untrusted snapshot
20- // payloads. The cell limit permits an average width of 100 cells per row.
21- maxHistoryDecodeChunks = 10_000
22- maxHistoryDecodeRows = 10_000
23- maxHistoryDecodeCells = 1_000_000
24- maxHistoryDecodeStyles = maxHistoryDecodeCells
25- maxHistoryDecodedBytes = 64 << 20
19+ // The daemon retains 10,000 history rows. Support a 20% row margin and
20+ // 160-cell rows, which leaves headroom above the representative 10k×120
21+ // terminal while bounding snapshot allocation.
22+ maxHistoryChunks = 12_000
23+ maxHistoryRows = 12_000
24+ maxHistoryRowCells = 160
25+ maxHistoryCells = maxHistoryRows * maxHistoryRowCells
26+
27+ maxHistoryDecodeStyles = maxHistoryCells
28+ maxHistoryDecodedBytes = maxHistoryCells * historyCellBytes
2629 )
2730
2831 var errInvalidHistory = errors.New("invalid history payload")
@@ -30,9 +33,27 @@ var errInvalidHistory = errors.New("invalid history payload")
3033 // MarshalHistory encodes a HistoryView in a deterministic, self-contained
3134 // format. It preserves chunk boundaries as well as every Cell and Style field.
3235 func MarshalHistory(view HistoryView) ([]byte, error) {
33- if view.rows < 0 || view.rows != historyViewRowCount(view) || uint64(len(view.chunks)) > math.MaxUint32 {
36+ if view.rows < 0 || view.rows != historyViewRowCount(view) || uint64(len(view.chunks)) > math.MaxUint32 || uint64(len(view.chunks)) > maxHistoryChunks {
3437 return nil, fmt.Errorf("marshal history: %w", errInvalidHistory)
3538 }
39+ stats := historyDecodeStats{}
40+ for _, chunk := range view.chunks {
41+ if chunk == nil || len(chunk.rows) == 0 || len(chunk.rows) > maxHistoryChunkRows ||
42+ !addHistoryDecodeBudget(&stats.rows, uint64(len(chunk.rows)), maxHistoryRows) {
43+ return nil, fmt.Errorf("marshal history: %w", errInvalidHistory)
44+ }
45+ for _, row := range chunk.rows {
46+ cellCount := uint64(len(row))
47+ cellBytes, ok := historyCellByteCount(cellCount)
48+ if cellCount > maxHistoryRowCells || !ok ||
49+ !addHistoryDecodeBudget(&stats.cells, cellCount, maxHistoryCells) ||
50+ !addHistoryDecodeBudget(&stats.styles, cellCount, maxHistoryDecodeStyles) ||
51+ !addHistoryDecodeBudget(&stats.bytes, cellBytes, maxHistoryDecodedBytes) {
52+ return nil, fmt.Errorf("marshal history: %w", errInvalidHistory)
53+ }
54+ }
55+ }
56+
3657 out := make([]byte, 0, 9)
3758 out = append(out, historyMagic...)
3859 out = append(out, historyVersion)
@@ -141,7 +162,7 @@ func UnmarshalHistory(data []byte) (HistoryView, error) {
141162 rows := make([][]renderer.Cell, 0, rowCount)
142163 for range rowCount {
143164 cellCount, ok := p.uint32()
144- if !ok || uint64(cellCount) > uint64(len(p.data))/historyCellBytes || uint64(cellCount) > uint64(math.MaxInt) {
165+ if !ok || uint64(cellCount) > maxHistoryRowCells || uint64(cellCount) > uint64(len(p.data))/historyCellBytes || uint64(cellCount) > uint64(math.MaxInt) {
145166 return HistoryView{}, fmt.Errorf("unmarshal history: %w", errInvalidHistory)
146167 }
147168 row := make([]renderer.Cell, cellCount)
@@ -176,22 +197,22 @@ type historyDecodeStats struct {
176197 func preflightHistory(data []byte) (historyDecodeStats, bool) {
177198 p := historyParser{data: data}
178199 chunkCount, ok := p.uint32()
179- if !ok || uint64(chunkCount) > maxHistoryDecodeChunks || uint64(chunkCount) > uint64(len(p.data))/4 {
200+ if !ok || uint64(chunkCount) > maxHistoryChunks || uint64(chunkCount) > uint64(len(p.data))/4 {
180201 return historyDecodeStats{}, false
181202 }
182203 stats := historyDecodeStats{chunks: uint64(chunkCount)}
183204 for range chunkCount {
184205 rowCount, ok := p.uint32()
185- if !ok || rowCount == 0 || rowCount > maxHistoryChunkRows || uint64(rowCount) > uint64(len(p.data))/4 || !addHistoryDecodeBudget(&stats.rows, uint64(rowCount), maxHistoryDecodeRows) {
206+ if !ok || rowCount == 0 || rowCount > maxHistoryChunkRows || uint64(rowCount) > uint64(len(p.data))/4 || !addHistoryDecodeBudget(&stats.rows, uint64(rowCount), maxHistoryRows) {
186207 return historyDecodeStats{}, false
187208 }
188209 for range rowCount {
189210 cellCount, ok := p.uint32()
190- if !ok || uint64(cellCount) > uint64(len(p.data))/historyCellBytes {
211+ cellBytes, validCellBytes := historyCellByteCount(uint64(cellCount))
212+ if !ok || uint64(cellCount) > maxHistoryRowCells || !validCellBytes || uint64(cellCount) > uint64(len(p.data))/historyCellBytes {
191213 return historyDecodeStats{}, false
192214 }
193- cellBytes := uint64(cellCount) * historyCellBytes
194- if !addHistoryDecodeBudget(&stats.cells, uint64(cellCount), maxHistoryDecodeCells) ||
215+ if !addHistoryDecodeBudget(&stats.cells, uint64(cellCount), maxHistoryCells) ||
195216 !addHistoryDecodeBudget(&stats.styles, uint64(cellCount), maxHistoryDecodeStyles) ||
196217 !addHistoryDecodeBudget(&stats.bytes, cellBytes, maxHistoryDecodedBytes) {
197218 return historyDecodeStats{}, false
@@ -202,6 +223,13 @@ func preflightHistory(data []byte) (historyDecodeStats, bool) {
202223 return stats, len(p.data) == 0
203224 }
204225
226+func historyCellByteCount(cellCount uint64) (uint64, bool) {
227+ if cellCount > math.MaxUint64/historyCellBytes {
228+ return 0, false
229+ }
230+ return cellCount * historyCellBytes, true
231+}
232+
205233 func addHistoryDecodeBudget(total *uint64, amount, limit uint64) bool {
206234 if *total > limit || amount > limit-*total {
207235 return false
@@ -16,13 +16,16 @@ const (
16 historyCellBytes = 4116 historyCellBytes = 41
17 maxHistoryChunkRows = 25617 maxHistoryChunkRows = 256
18 18
19- // Preserve ten-thousand-row histories while bounding untrusted snapshot19+ // The daemon retains 10,000 history rows. Support a 20% row margin and
20- // payloads. The cell limit permits an average width of 100 cells per row.20+ // 160-cell rows, which leaves headroom above the representative 10k×120
21- maxHistoryDecodeChunks = 10_00021+ // terminal while bounding snapshot allocation.
22- maxHistoryDecodeRows = 10_00022+ maxHistoryChunks = 12_000
23- maxHistoryDecodeCells = 1_000_00023+ maxHistoryRows = 12_000
24- maxHistoryDecodeStyles = maxHistoryDecodeCells24+ maxHistoryRowCells = 160
25- maxHistoryDecodedBytes = 64 << 2025+ maxHistoryCells = maxHistoryRows * maxHistoryRowCells
26+
27+ maxHistoryDecodeStyles = maxHistoryCells
28+ maxHistoryDecodedBytes = maxHistoryCells * historyCellBytes
26 )29 )
27 30
28 var errInvalidHistory = errors.New("invalid history payload")31 var errInvalidHistory = errors.New("invalid history payload")
@@ -30,9 +33,27 @@ var errInvalidHistory = errors.New("invalid history payload")
30 // MarshalHistory encodes a HistoryView in a deterministic, self-contained33 // MarshalHistory encodes a HistoryView in a deterministic, self-contained
31 // format. It preserves chunk boundaries as well as every Cell and Style field.34 // format. It preserves chunk boundaries as well as every Cell and Style field.
32 func MarshalHistory(view HistoryView) ([]byte, error) {35 func MarshalHistory(view HistoryView) ([]byte, error) {
33- if view.rows < 0 || view.rows != historyViewRowCount(view) || uint64(len(view.chunks)) > math.MaxUint32 {36+ if view.rows < 0 || view.rows != historyViewRowCount(view) || uint64(len(view.chunks)) > math.MaxUint32 || uint64(len(view.chunks)) > maxHistoryChunks {
34 return nil, fmt.Errorf("marshal history: %w", errInvalidHistory)37 return nil, fmt.Errorf("marshal history: %w", errInvalidHistory)
35 }38 }
39+ stats := historyDecodeStats{}
40+ for _, chunk := range view.chunks {
41+ if chunk == nil || len(chunk.rows) == 0 || len(chunk.rows) > maxHistoryChunkRows ||
42+ !addHistoryDecodeBudget(&stats.rows, uint64(len(chunk.rows)), maxHistoryRows) {
43+ return nil, fmt.Errorf("marshal history: %w", errInvalidHistory)
44+ }
45+ for _, row := range chunk.rows {
46+ cellCount := uint64(len(row))
47+ cellBytes, ok := historyCellByteCount(cellCount)
48+ if cellCount > maxHistoryRowCells || !ok ||
49+ !addHistoryDecodeBudget(&stats.cells, cellCount, maxHistoryCells) ||
50+ !addHistoryDecodeBudget(&stats.styles, cellCount, maxHistoryDecodeStyles) ||
51+ !addHistoryDecodeBudget(&stats.bytes, cellBytes, maxHistoryDecodedBytes) {
52+ return nil, fmt.Errorf("marshal history: %w", errInvalidHistory)
53+ }
54+ }
55+ }
56+
36 out := make([]byte, 0, 9)57 out := make([]byte, 0, 9)
37 out = append(out, historyMagic...)58 out = append(out, historyMagic...)
38 out = append(out, historyVersion)59 out = append(out, historyVersion)
@@ -141,7 +162,7 @@ func UnmarshalHistory(data []byte) (HistoryView, error) {
141 rows := make([][]renderer.Cell, 0, rowCount)162 rows := make([][]renderer.Cell, 0, rowCount)
142 for range rowCount {163 for range rowCount {
143 cellCount, ok := p.uint32()164 cellCount, ok := p.uint32()
144- if !ok || uint64(cellCount) > uint64(len(p.data))/historyCellBytes || uint64(cellCount) > uint64(math.MaxInt) {165+ if !ok || uint64(cellCount) > maxHistoryRowCells || uint64(cellCount) > uint64(len(p.data))/historyCellBytes || uint64(cellCount) > uint64(math.MaxInt) {
145 return HistoryView{}, fmt.Errorf("unmarshal history: %w", errInvalidHistory)166 return HistoryView{}, fmt.Errorf("unmarshal history: %w", errInvalidHistory)
146 }167 }
147 row := make([]renderer.Cell, cellCount)168 row := make([]renderer.Cell, cellCount)
@@ -176,22 +197,22 @@ type historyDecodeStats struct {
176 func preflightHistory(data []byte) (historyDecodeStats, bool) {197 func preflightHistory(data []byte) (historyDecodeStats, bool) {
177 p := historyParser{data: data}198 p := historyParser{data: data}
178 chunkCount, ok := p.uint32()199 chunkCount, ok := p.uint32()
179- if !ok || uint64(chunkCount) > maxHistoryDecodeChunks || uint64(chunkCount) > uint64(len(p.data))/4 {200+ if !ok || uint64(chunkCount) > maxHistoryChunks || uint64(chunkCount) > uint64(len(p.data))/4 {
180 return historyDecodeStats{}, false201 return historyDecodeStats{}, false
181 }202 }
182 stats := historyDecodeStats{chunks: uint64(chunkCount)}203 stats := historyDecodeStats{chunks: uint64(chunkCount)}
183 for range chunkCount {204 for range chunkCount {
184 rowCount, ok := p.uint32()205 rowCount, ok := p.uint32()
185- if !ok || rowCount == 0 || rowCount > maxHistoryChunkRows || uint64(rowCount) > uint64(len(p.data))/4 || !addHistoryDecodeBudget(&stats.rows, uint64(rowCount), maxHistoryDecodeRows) {206+ if !ok || rowCount == 0 || rowCount > maxHistoryChunkRows || uint64(rowCount) > uint64(len(p.data))/4 || !addHistoryDecodeBudget(&stats.rows, uint64(rowCount), maxHistoryRows) {
186 return historyDecodeStats{}, false207 return historyDecodeStats{}, false
187 }208 }
188 for range rowCount {209 for range rowCount {
189 cellCount, ok := p.uint32()210 cellCount, ok := p.uint32()
190- if !ok || uint64(cellCount) > uint64(len(p.data))/historyCellBytes {211+ cellBytes, validCellBytes := historyCellByteCount(uint64(cellCount))
212+ if !ok || uint64(cellCount) > maxHistoryRowCells || !validCellBytes || uint64(cellCount) > uint64(len(p.data))/historyCellBytes {
191 return historyDecodeStats{}, false213 return historyDecodeStats{}, false
192 }214 }
193- cellBytes := uint64(cellCount) * historyCellBytes215+ if !addHistoryDecodeBudget(&stats.cells, uint64(cellCount), maxHistoryCells) ||
194- if !addHistoryDecodeBudget(&stats.cells, uint64(cellCount), maxHistoryDecodeCells) ||
195 !addHistoryDecodeBudget(&stats.styles, uint64(cellCount), maxHistoryDecodeStyles) ||216 !addHistoryDecodeBudget(&stats.styles, uint64(cellCount), maxHistoryDecodeStyles) ||
196 !addHistoryDecodeBudget(&stats.bytes, cellBytes, maxHistoryDecodedBytes) {217 !addHistoryDecodeBudget(&stats.bytes, cellBytes, maxHistoryDecodedBytes) {
197 return historyDecodeStats{}, false218 return historyDecodeStats{}, false
@@ -202,6 +223,13 @@ func preflightHistory(data []byte) (historyDecodeStats, bool) {
202 return stats, len(p.data) == 0223 return stats, len(p.data) == 0
203 }224 }
204 225
226+func historyCellByteCount(cellCount uint64) (uint64, bool) {
227+ if cellCount > math.MaxUint64/historyCellBytes {
228+ return 0, false
229+ }
230+ return cellCount * historyCellBytes, true
231+}
232+
205 func addHistoryDecodeBudget(total *uint64, amount, limit uint64) bool {233 func addHistoryDecodeBudget(total *uint64, amount, limit uint64) bool {
206 if *total > limit || amount > limit-*total {234 if *total > limit || amount > limit-*total {
207 return false235 return false
M pkg/vt/history_codec_test.go +97 -2
@@ -98,14 +98,72 @@ func TestChunkCodecRejectsTruncatedAndTrailingPayloads(t *testing.T) {
9898 }
9999 }
100100
101+func TestChunkCodecSupportsRepresentativeHistory(t *testing.T) {
102+ const (
103+ rows = 10_000
104+ width = 120
105+ )
106+
107+ encoded, err := MarshalHistory(historyViewWithDimensions(rows, width))
108+ if err != nil {
109+ t.Fatalf("marshal representative history: %v", err)
110+ }
111+ decoded, err := UnmarshalHistory(encoded)
112+ if err != nil {
113+ t.Fatalf("unmarshal representative history: %v", err)
114+ }
115+ if got := decoded.Len(); got != rows {
116+ t.Fatalf("decoded rows = %d, want %d", got, rows)
117+ }
118+ for _, rowIndex := range []int{0, rows - 1} {
119+ if got := len(decoded.Row(rowIndex)); got != width {
120+ t.Fatalf("decoded row %d width = %d, want %d", rowIndex, got, width)
121+ }
122+ }
123+}
124+
125+func TestChunkCodecRejectsDimensionsBeyondSupportedLimits(t *testing.T) {
126+ const (
127+ supportedRows = 12_000
128+ supportedWidth = 160
129+ )
130+ tests := []struct {
131+ name string
132+ view HistoryView
133+ data []byte
134+ }{
135+ {
136+ name: "too many rows",
137+ view: historyViewWithDimensions(supportedRows+1, 0),
138+ data: historyPayloadWithDimensions(supportedRows+1, 0),
139+ },
140+ {
141+ name: "too many cells in a row",
142+ view: historyViewWithDimensions(1, supportedWidth+1),
143+ data: historyPayloadWithDimensions(1, supportedWidth+1),
144+ },
145+ }
146+
147+ for _, tt := range tests {
148+ t.Run(tt.name, func(t *testing.T) {
149+ if _, err := MarshalHistory(tt.view); err == nil {
150+ t.Fatal("marshal accepted dimensions beyond the supported limits")
151+ }
152+ if _, err := UnmarshalHistory(tt.data); err == nil {
153+ t.Fatal("unmarshal accepted dimensions beyond the supported limits")
154+ }
155+ })
156+ }
157+}
158+
101159 func TestChunkCodecRejectsHostileChunkAndRowDeclarations(t *testing.T) {
102160 tests := []struct {
103161 name string
104162 chunkCount int
105163 rowCount int
106164 }{
107- {name: "more than ten thousand chunks", chunkCount: 10_001, rowCount: 1},
108- {name: "more than ten thousand rows", chunkCount: 40, rowCount: maxHistoryChunkRows},
165+ {name: "more than twelve thousand chunks", chunkCount: 12_001, rowCount: 1},
166+ {name: "more than twelve thousand rows", chunkCount: 47, rowCount: maxHistoryChunkRows},
109167 }
110168
111169 for _, tt := range tests {
@@ -117,6 +175,43 @@ func TestChunkCodecRejectsHostileChunkAndRowDeclarations(t *testing.T) {
117175 }
118176 }
119177
178+func historyViewWithDimensions(rowCount, width int) HistoryView {
179+ totalRows := rowCount
180+ row := make([]renderer.Cell, width)
181+ for i := range row {
182+ row[i].Rune = 'x'
183+ }
184+ chunks := make([]*HistoryChunk, 0, (rowCount+maxHistoryChunkRows-1)/maxHistoryChunkRows)
185+ for rowCount > 0 {
186+ chunkRows := min(rowCount, maxHistoryChunkRows)
187+ rows := make([][]renderer.Cell, chunkRows)
188+ for i := range rows {
189+ rows[i] = row
190+ }
191+ chunks = append(chunks, &HistoryChunk{rows: rows})
192+ rowCount -= chunkRows
193+ }
194+ return HistoryView{chunks: chunks, rows: totalRows}
195+}
196+
197+func historyPayloadWithDimensions(rowCount, width int) []byte {
198+ data := make([]byte, 9)
199+ copy(data, historyMagic)
200+ data[4] = historyVersion
201+ chunkCount := (rowCount + maxHistoryChunkRows - 1) / maxHistoryChunkRows
202+ binary.BigEndian.PutUint32(data[5:], uint32(chunkCount))
203+ for rowCount > 0 {
204+ chunkRows := min(rowCount, maxHistoryChunkRows)
205+ data = binary.BigEndian.AppendUint32(data, uint32(chunkRows))
206+ for range chunkRows {
207+ data = binary.BigEndian.AppendUint32(data, uint32(width))
208+ data = append(data, make([]byte, width*historyCellBytes)...)
209+ }
210+ rowCount -= chunkRows
211+ }
212+ return data
213+}
214+
120215 func hostileHistoryDeclarations(chunkCount, rowCount int) []byte {
121216 data := make([]byte, 9, 9+chunkCount*(4+rowCount*4))
122217 copy(data, historyMagic)
@@ -98,14 +98,72 @@ func TestChunkCodecRejectsTruncatedAndTrailingPayloads(t *testing.T) {
98 }98 }
99 }99 }
100 100
101+func TestChunkCodecSupportsRepresentativeHistory(t *testing.T) {
102+ const (
103+ rows = 10_000
104+ width = 120
105+ )
106+
107+ encoded, err := MarshalHistory(historyViewWithDimensions(rows, width))
108+ if err != nil {
109+ t.Fatalf("marshal representative history: %v", err)
110+ }
111+ decoded, err := UnmarshalHistory(encoded)
112+ if err != nil {
113+ t.Fatalf("unmarshal representative history: %v", err)
114+ }
115+ if got := decoded.Len(); got != rows {
116+ t.Fatalf("decoded rows = %d, want %d", got, rows)
117+ }
118+ for _, rowIndex := range []int{0, rows - 1} {
119+ if got := len(decoded.Row(rowIndex)); got != width {
120+ t.Fatalf("decoded row %d width = %d, want %d", rowIndex, got, width)
121+ }
122+ }
123+}
124+
125+func TestChunkCodecRejectsDimensionsBeyondSupportedLimits(t *testing.T) {
126+ const (
127+ supportedRows = 12_000
128+ supportedWidth = 160
129+ )
130+ tests := []struct {
131+ name string
132+ view HistoryView
133+ data []byte
134+ }{
135+ {
136+ name: "too many rows",
137+ view: historyViewWithDimensions(supportedRows+1, 0),
138+ data: historyPayloadWithDimensions(supportedRows+1, 0),
139+ },
140+ {
141+ name: "too many cells in a row",
142+ view: historyViewWithDimensions(1, supportedWidth+1),
143+ data: historyPayloadWithDimensions(1, supportedWidth+1),
144+ },
145+ }
146+
147+ for _, tt := range tests {
148+ t.Run(tt.name, func(t *testing.T) {
149+ if _, err := MarshalHistory(tt.view); err == nil {
150+ t.Fatal("marshal accepted dimensions beyond the supported limits")
151+ }
152+ if _, err := UnmarshalHistory(tt.data); err == nil {
153+ t.Fatal("unmarshal accepted dimensions beyond the supported limits")
154+ }
155+ })
156+ }
157+}
158+
101 func TestChunkCodecRejectsHostileChunkAndRowDeclarations(t *testing.T) {159 func TestChunkCodecRejectsHostileChunkAndRowDeclarations(t *testing.T) {
102 tests := []struct {160 tests := []struct {
103 name string161 name string
104 chunkCount int162 chunkCount int
105 rowCount int163 rowCount int
106 }{164 }{
107- {name: "more than ten thousand chunks", chunkCount: 10_001, rowCount: 1},165+ {name: "more than twelve thousand chunks", chunkCount: 12_001, rowCount: 1},
108- {name: "more than ten thousand rows", chunkCount: 40, rowCount: maxHistoryChunkRows},166+ {name: "more than twelve thousand rows", chunkCount: 47, rowCount: maxHistoryChunkRows},
109 }167 }
110 168
111 for _, tt := range tests {169 for _, tt := range tests {
@@ -117,6 +175,43 @@ func TestChunkCodecRejectsHostileChunkAndRowDeclarations(t *testing.T) {
117 }175 }
118 }176 }
119 177
178+func historyViewWithDimensions(rowCount, width int) HistoryView {
179+ totalRows := rowCount
180+ row := make([]renderer.Cell, width)
181+ for i := range row {
182+ row[i].Rune = 'x'
183+ }
184+ chunks := make([]*HistoryChunk, 0, (rowCount+maxHistoryChunkRows-1)/maxHistoryChunkRows)
185+ for rowCount > 0 {
186+ chunkRows := min(rowCount, maxHistoryChunkRows)
187+ rows := make([][]renderer.Cell, chunkRows)
188+ for i := range rows {
189+ rows[i] = row
190+ }
191+ chunks = append(chunks, &HistoryChunk{rows: rows})
192+ rowCount -= chunkRows
193+ }
194+ return HistoryView{chunks: chunks, rows: totalRows}
195+}
196+
197+func historyPayloadWithDimensions(rowCount, width int) []byte {
198+ data := make([]byte, 9)
199+ copy(data, historyMagic)
200+ data[4] = historyVersion
201+ chunkCount := (rowCount + maxHistoryChunkRows - 1) / maxHistoryChunkRows
202+ binary.BigEndian.PutUint32(data[5:], uint32(chunkCount))
203+ for rowCount > 0 {
204+ chunkRows := min(rowCount, maxHistoryChunkRows)
205+ data = binary.BigEndian.AppendUint32(data, uint32(chunkRows))
206+ for range chunkRows {
207+ data = binary.BigEndian.AppendUint32(data, uint32(width))
208+ data = append(data, make([]byte, width*historyCellBytes)...)
209+ }
210+ rowCount -= chunkRows
211+ }
212+ return data
213+}
214+
120 func hostileHistoryDeclarations(chunkCount, rowCount int) []byte {215 func hostileHistoryDeclarations(chunkCount, rowCount int) []byte {
121 data := make([]byte, 9, 9+chunkCount*(4+rowCount*4))216 data := make([]byte, 9, 9+chunkCount*(4+rowCount*4))
122 copy(data, historyMagic)217 copy(data, historyMagic)

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