detached b1b81df

fix(diff): omit a file over the byte budget of earlier files as budget

A file that meets the bytes spent by the files read before it may fit on
its own: it is OmitBudget, loadable alone, and OmitUnavailable is kept
for a file over the bytes by itself. OneFileDiffLimits with a negative
index selects no file instead of the whole diff.
commit
b1b81df962f73a979745802dcf9186d557312d8c
author
brice · · 2026-10-20 07:37 UTC
committer
brice · · 2026-10-20 09:12 UTC
parent
0e23da4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +55 -8
M internal/adapters/gitcli/diff.go +1 -0
@@ -145,6 +145,7 @@ func readDiff(r *bufio.Reader, limits domain.DiffLimits) (domain.Diff, domain.Di
145145 e.file.Omitted = why
146146 default:
147147 before := pr.budget
148+ pr.budget.StartFile()
148149 hunks, _, why, err := pr.file(sections, pr.budget.FileLines())
149150 if err != nil {
150151 return domain.Diff{}, domain.DiffStats{}, err
@@ -145,6 +145,7 @@ func readDiff(r *bufio.Reader, limits domain.DiffLimits) (domain.Diff, domain.Di
145 e.file.Omitted = why145 e.file.Omitted = why
146 default:146 default:
147 before := pr.budget147 before := pr.budget
148+ pr.budget.StartFile()
148 hunks, _, why, err := pr.file(sections, pr.budget.FileLines())149 hunks, _, why, err := pr.file(sections, pr.budget.FileLines())
149 if err != nil {150 if err != nil {
150 return domain.Diff{}, domain.DiffStats{}, err151 return domain.Diff{}, domain.DiffStats{}, err
M internal/domain/budget.go +13 -3
@@ -53,10 +53,12 @@ func (b *CommitObjectBudget) Charge(id ObjectID, size int64) {
5353 type DiffBudget struct {
5454 limits DiffLimits
5555 linesLeft, bytesLeft int
56+ // fileBytes is bytesLeft when the current file started (StartFile).
57+ fileBytes int
5658 }
5759
5860 func NewDiffBudget(limits DiffLimits) DiffBudget {
59- return DiffBudget{limits: limits, linesLeft: limits.MaxTotalLines, bytesLeft: limits.MaxTotalBytes}
61+ return DiffBudget{limits: limits, linesLeft: limits.MaxTotalLines, bytesLeft: limits.MaxTotalBytes, fileBytes: limits.MaxTotalBytes}
6062 }
6163
6264 func (b DiffBudget) Window(files int) (kept, from, to int, truncated bool) {
@@ -103,10 +105,16 @@ func (b DiffBudget) FileLines() int {
103105 return n
104106 }
105107
108+// StartFile marks the start of a file's lines, for ChargeLine to tell a file
109+// over the byte limit on its own from one that met the bytes of earlier files.
110+func (b *DiffBudget) StartFile() { b.fileBytes = b.bytesLeft }
111+
106112 // ChargeLine accounts for decoded text bytes and, for additions or deletions,
107113 // one changed line. Context consumes bytes only. It returns OmitNone when the
108-// line fits, OmitBudget when the changed lines ran out and OmitUnavailable when
109-// the bytes ran out.
114+// line fits and OmitBudget when the changed lines ran out. When the bytes run
115+// out it returns OmitBudget if files read before this one (since StartFile)
116+// spent some of them, as this file may still fit on its own, and
117+// OmitUnavailable if this file alone is over MaxTotalBytes.
110118 func (b *DiffBudget) ChargeLine(bytes int, changed bool) FileOmission {
111119 if changed && b.limits.MaxTotalLines > 0 {
112120 b.linesLeft--
@@ -115,6 +123,8 @@ func (b *DiffBudget) ChargeLine(bytes int, changed bool) FileOmission {
115123 switch {
116124 case b.limits.MaxTotalLines > 0 && b.linesLeft < 0:
117125 return OmitBudget
126+ case b.bytesLeft < 0 && b.fileBytes < b.limits.MaxTotalBytes:
127+ return OmitBudget
118128 case b.bytesLeft < 0:
119129 return OmitUnavailable
120130 }
@@ -53,10 +53,12 @@ func (b *CommitObjectBudget) Charge(id ObjectID, size int64) {
53 type DiffBudget struct {53 type DiffBudget struct {
54 limits DiffLimits54 limits DiffLimits
55 linesLeft, bytesLeft int55 linesLeft, bytesLeft int
56+ // fileBytes is bytesLeft when the current file started (StartFile).
57+ fileBytes int
56 }58 }
57 59
58 func NewDiffBudget(limits DiffLimits) DiffBudget {60 func NewDiffBudget(limits DiffLimits) DiffBudget {
59- return DiffBudget{limits: limits, linesLeft: limits.MaxTotalLines, bytesLeft: limits.MaxTotalBytes}61+ return DiffBudget{limits: limits, linesLeft: limits.MaxTotalLines, bytesLeft: limits.MaxTotalBytes, fileBytes: limits.MaxTotalBytes}
60 }62 }
61 63
62 func (b DiffBudget) Window(files int) (kept, from, to int, truncated bool) {64 func (b DiffBudget) Window(files int) (kept, from, to int, truncated bool) {
@@ -103,10 +105,16 @@ func (b DiffBudget) FileLines() int {
103 return n105 return n
104 }106 }
105 107
108+// StartFile marks the start of a file's lines, for ChargeLine to tell a file
109+// over the byte limit on its own from one that met the bytes of earlier files.
110+func (b *DiffBudget) StartFile() { b.fileBytes = b.bytesLeft }
111+
106 // ChargeLine accounts for decoded text bytes and, for additions or deletions,112 // ChargeLine accounts for decoded text bytes and, for additions or deletions,
107 // one changed line. Context consumes bytes only. It returns OmitNone when the113 // one changed line. Context consumes bytes only. It returns OmitNone when the
108-// line fits, OmitBudget when the changed lines ran out and OmitUnavailable when114+// line fits and OmitBudget when the changed lines ran out. When the bytes run
109-// the bytes ran out.115+// out it returns OmitBudget if files read before this one (since StartFile)
116+// spent some of them, as this file may still fit on its own, and
117+// OmitUnavailable if this file alone is over MaxTotalBytes.
110 func (b *DiffBudget) ChargeLine(bytes int, changed bool) FileOmission {118 func (b *DiffBudget) ChargeLine(bytes int, changed bool) FileOmission {
111 if changed && b.limits.MaxTotalLines > 0 {119 if changed && b.limits.MaxTotalLines > 0 {
112 b.linesLeft--120 b.linesLeft--
@@ -115,6 +123,8 @@ func (b *DiffBudget) ChargeLine(bytes int, changed bool) FileOmission {
115 switch {123 switch {
116 case b.limits.MaxTotalLines > 0 && b.linesLeft < 0:124 case b.limits.MaxTotalLines > 0 && b.linesLeft < 0:
117 return OmitBudget125 return OmitBudget
126+ case b.bytesLeft < 0 && b.fileBytes < b.limits.MaxTotalBytes:
127+ return OmitBudget
118 case b.bytesLeft < 0:128 case b.bytesLeft < 0:
119 return OmitUnavailable129 return OmitUnavailable
120 }130 }
M internal/domain/budget_test.go +21 -0
@@ -190,6 +190,27 @@ func TestOneFileDiffLimits(t *testing.T) {
190190 if l.From != 3 || l.To != 4 || l.MaxFileLines != 0 || l.MaxTotalLines != 0 || l.MaxFiles != 0 || l.MaxTotalBytes != DefaultDiffLimits().MaxTotalBytes {
191191 t.Fatalf("got %+v", l)
192192 }
193+ // A negative index (an unchecked IndexFunc) selects no file, not the whole diff.
194+ if _, from, to, _ := NewDiffBudget(OneFileDiffLimits(-1)).Window(10); from < to {
195+ t.Fatalf("OneFileDiffLimits(-1) selects files [%d, %d)", from, to)
196+ }
197+}
198+
199+func TestDiffBudgetBytesOfEarlierFiles(t *testing.T) {
200+ b := NewDiffBudget(DiffLimits{MaxTotalBytes: 10})
201+ b.StartFile()
202+ if b.ChargeLine(4, true) != OmitNone {
203+ t.Fatal("the first file fits")
204+ }
205+ b.StartFile()
206+ if got := b.ChargeLine(7, true); got != OmitBudget {
207+ t.Fatalf("a file meeting the bytes of earlier files: got %v, want OmitBudget", got)
208+ }
209+ alone := NewDiffBudget(DiffLimits{MaxTotalBytes: 10})
210+ alone.StartFile()
211+ if got := alone.ChargeLine(11, true); got != OmitUnavailable {
212+ t.Fatalf("a file over the bytes on its own: got %v, want OmitUnavailable", got)
213+ }
193214 }
194215
195216 func TestDiffBudgetContext(t *testing.T) {
@@ -190,6 +190,27 @@ func TestOneFileDiffLimits(t *testing.T) {
190 if l.From != 3 || l.To != 4 || l.MaxFileLines != 0 || l.MaxTotalLines != 0 || l.MaxFiles != 0 || l.MaxTotalBytes != DefaultDiffLimits().MaxTotalBytes {190 if l.From != 3 || l.To != 4 || l.MaxFileLines != 0 || l.MaxTotalLines != 0 || l.MaxFiles != 0 || l.MaxTotalBytes != DefaultDiffLimits().MaxTotalBytes {
191 t.Fatalf("got %+v", l)191 t.Fatalf("got %+v", l)
192 }192 }
193+ // A negative index (an unchecked IndexFunc) selects no file, not the whole diff.
194+ if _, from, to, _ := NewDiffBudget(OneFileDiffLimits(-1)).Window(10); from < to {
195+ t.Fatalf("OneFileDiffLimits(-1) selects files [%d, %d)", from, to)
196+ }
197+}
198+
199+func TestDiffBudgetBytesOfEarlierFiles(t *testing.T) {
200+ b := NewDiffBudget(DiffLimits{MaxTotalBytes: 10})
201+ b.StartFile()
202+ if b.ChargeLine(4, true) != OmitNone {
203+ t.Fatal("the first file fits")
204+ }
205+ b.StartFile()
206+ if got := b.ChargeLine(7, true); got != OmitBudget {
207+ t.Fatalf("a file meeting the bytes of earlier files: got %v, want OmitBudget", got)
208+ }
209+ alone := NewDiffBudget(DiffLimits{MaxTotalBytes: 10})
210+ alone.StartFile()
211+ if got := alone.ChargeLine(11, true); got != OmitUnavailable {
212+ t.Fatalf("a file over the bytes on its own: got %v, want OmitUnavailable", got)
213+ }
193 }214 }
194 215
195 func TestDiffBudgetContext(t *testing.T) {216 func TestDiffBudgetContext(t *testing.T) {
M internal/domain/git.go +6 -1
@@ -592,8 +592,13 @@ type DiffLimits struct {
592592 SkipGenerated bool
593593 }
594594
595-// OneFileDiffLimits reads the hunks of file i only, with no line cap and MaxTotalBytes as the sole guard.
595+// OneFileDiffLimits reads the hunks of file i only, with no line cap and
596+// MaxTotalBytes as the sole guard. i must be at least 0: From -1 with To 0
597+// would mean the whole diff, so a negative i reads no file at all.
596598 func OneFileDiffLimits(i int) DiffLimits {
599+ if i < 0 {
600+ return DiffLimits{From: 1, To: 1, MaxTotalBytes: DefaultDiffLimits().MaxTotalBytes}
601+ }
597602 return DiffLimits{From: i, To: i + 1, MaxTotalBytes: DefaultDiffLimits().MaxTotalBytes}
598603 }
599604
@@ -592,8 +592,13 @@ type DiffLimits struct {
592 SkipGenerated bool592 SkipGenerated bool
593 }593 }
594 594
595-// OneFileDiffLimits reads the hunks of file i only, with no line cap and MaxTotalBytes as the sole guard.595+// OneFileDiffLimits reads the hunks of file i only, with no line cap and
596+// MaxTotalBytes as the sole guard. i must be at least 0: From -1 with To 0
597+// would mean the whole diff, so a negative i reads no file at all.
596 func OneFileDiffLimits(i int) DiffLimits {598 func OneFileDiffLimits(i int) DiffLimits {
599+ if i < 0 {
600+ return DiffLimits{From: 1, To: 1, MaxTotalBytes: DefaultDiffLimits().MaxTotalBytes}
601+ }
597 return DiffLimits{From: i, To: i + 1, MaxTotalBytes: DefaultDiffLimits().MaxTotalBytes}602 return DiffLimits{From: i, To: i + 1, MaxTotalBytes: DefaultDiffLimits().MaxTotalBytes}
598 }603 }
599 604
M internal/readertest/diff.go +14 -4
@@ -89,8 +89,7 @@ func DiffOmissions(t *testing.T, mk Maker) {
8989 f.Lines("z-big.txt", "line", 2500)
9090 f.Commit("change")
9191
92- limits := domain.DiffLimits{MaxTotalBytes: 1 << 20, MaxFileLines: 2000, SkipGenerated: true}
93- limits.MaxTotalBytes = 300
92+ limits := domain.DiffLimits{MaxTotalBytes: 300, MaxFileLines: 2000, SkipGenerated: true}
9493 d, err := g.GetCommit(ctx, f.Name, domain.Revision{}, limits)
9594 require.NoError(t, err)
9695 require.Len(t, d.Diff.Files, 3)
@@ -107,15 +106,26 @@ func DiffOmissions(t *testing.T, mk Maker) {
107106 assert.Empty(t, byPath["z-big.txt"].Hunks)
108107 assert.False(t, d.Diff.Truncated)
109108
109+ // Read, go.sum overflows the bytes that a.txt, read before it, already
110+ // spent: it may fit on its own, so it is OmitBudget, loadable alone.
110111 limits.SkipGenerated = false
111112 d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, limits)
112113 require.NoError(t, err)
113- for _, file := range d.Diff.Files {
114+ sum := -1
115+ for i, file := range d.Diff.Files {
114116 if file.NewPath == "go.sum" {
115- assert.Equal(t, domain.OmitUnavailable, file.Omitted, "read, it overflows the bytes")
117+ sum = i
118+ assert.Equal(t, domain.OmitBudget, file.Omitted, "earlier files spent bytes")
116119 assert.Empty(t, file.Hunks)
117120 }
118121 }
122+ require.GreaterOrEqual(t, sum, 0)
123+ // Alone, it is still over the bytes: OmitUnavailable, a dead end.
124+ alone := domain.DiffLimits{From: sum, To: sum + 1, MaxTotalBytes: 300}
125+ d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, alone)
126+ require.NoError(t, err)
127+ assert.Equal(t, domain.OmitUnavailable, d.Diff.Files[sum].Omitted, "over the bytes on its own")
128+ assert.Empty(t, d.Diff.Files[sum].Hunks)
119129
120130 // OneFileDiffLimits reads a file over 2,000 changed lines, and only it.
121131 idx := -1
@@ -89,8 +89,7 @@ func DiffOmissions(t *testing.T, mk Maker) {
89 f.Lines("z-big.txt", "line", 2500)89 f.Lines("z-big.txt", "line", 2500)
90 f.Commit("change")90 f.Commit("change")
91 91
92- limits := domain.DiffLimits{MaxTotalBytes: 1 << 20, MaxFileLines: 2000, SkipGenerated: true}92+ limits := domain.DiffLimits{MaxTotalBytes: 300, MaxFileLines: 2000, SkipGenerated: true}
93- limits.MaxTotalBytes = 300
94 d, err := g.GetCommit(ctx, f.Name, domain.Revision{}, limits)93 d, err := g.GetCommit(ctx, f.Name, domain.Revision{}, limits)
95 require.NoError(t, err)94 require.NoError(t, err)
96 require.Len(t, d.Diff.Files, 3)95 require.Len(t, d.Diff.Files, 3)
@@ -107,15 +106,26 @@ func DiffOmissions(t *testing.T, mk Maker) {
107 assert.Empty(t, byPath["z-big.txt"].Hunks)106 assert.Empty(t, byPath["z-big.txt"].Hunks)
108 assert.False(t, d.Diff.Truncated)107 assert.False(t, d.Diff.Truncated)
109 108
109+ // Read, go.sum overflows the bytes that a.txt, read before it, already
110+ // spent: it may fit on its own, so it is OmitBudget, loadable alone.
110 limits.SkipGenerated = false111 limits.SkipGenerated = false
111 d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, limits)112 d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, limits)
112 require.NoError(t, err)113 require.NoError(t, err)
113- for _, file := range d.Diff.Files {114+ sum := -1
115+ for i, file := range d.Diff.Files {
114 if file.NewPath == "go.sum" {116 if file.NewPath == "go.sum" {
115- assert.Equal(t, domain.OmitUnavailable, file.Omitted, "read, it overflows the bytes")117+ sum = i
118+ assert.Equal(t, domain.OmitBudget, file.Omitted, "earlier files spent bytes")
116 assert.Empty(t, file.Hunks)119 assert.Empty(t, file.Hunks)
117 }120 }
118 }121 }
122+ require.GreaterOrEqual(t, sum, 0)
123+ // Alone, it is still over the bytes: OmitUnavailable, a dead end.
124+ alone := domain.DiffLimits{From: sum, To: sum + 1, MaxTotalBytes: 300}
125+ d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, alone)
126+ require.NoError(t, err)
127+ assert.Equal(t, domain.OmitUnavailable, d.Diff.Files[sum].Omitted, "over the bytes on its own")
128+ assert.Empty(t, d.Diff.Files[sum].Hunks)
119 129
120 // OneFileDiffLimits reads a file over 2,000 changed lines, and only it.130 // OneFileDiffLimits reads a file over 2,000 changed lines, and only it.
121 idx := -1131 idx := -1

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 44

↑↓ moveenter openesc close