v0.32.0 53e25a3

diff.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package readertest
4
5import (
6	"context"
7	"fmt"
8	"strings"
9	"testing"
10
11	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/require"
13
14	"bnema.dev/ermite/internal/domain"
15)
16
17// DiffChangedLineLimits checks that context is retained without consuming the
18// changed-line limits, but still consumes the byte budget.
19func DiffChangedLineLimits(t *testing.T, mk Maker) {
20	g, data := mk(t)
21	f := NewFixture(t, data, "diff-context-budget")
22	var before, after strings.Builder
23	const hunks = 96
24	for i := range hunks * 10 {
25		line := fmt.Sprintf("context %04d", i)
26		fmt.Fprintln(&before, line)
27		if i%10 == 4 {
28			line = fmt.Sprintf("changed %04d", i)
29		}
30		fmt.Fprintln(&after, line)
31	}
32	f.Write("many", before.String()).Commit("before")
33	f.Write("many", after.String()).Commit("after")
34	limits := domain.DiffLimits{MaxFileLines: hunks * 2, MaxTotalLines: hunks * 2, MaxTotalBytes: 1 << 20}
35	d, err := g.GetCommit(context.Background(), f.Name, domain.Revision{}, limits)
36	require.NoError(t, err)
37	require.Len(t, d.Diff.Files, 1)
38	file := d.Diff.Files[0]
39	require.Equal(t, domain.OmitNone, file.Omitted)
40	assert.False(t, d.Diff.Truncated)
41	require.Len(t, file.Hunks, hunks)
42	var changed, patchLines, textBytes, changedBytes int
43	for _, h := range file.Hunks {
44		for _, l := range h.Lines {
45			patchLines++
46			textBytes += len(l.Text)
47			if l.Kind != domain.LineContext {
48				changed++
49				changedBytes += len(l.Text)
50			}
51		}
52	}
53	assert.Equal(t, hunks*2, changed)
54	// Every hunk is read with no line limit too.
55	free, err := g.GetCommit(context.Background(), f.Name, domain.Revision{}, domain.DiffLimits{MaxTotalBytes: 1 << 20})
56	require.NoError(t, err)
57	require.Len(t, free.Diff.Files[0].Hunks, hunks)
58	assert.Greater(t, patchLines, limits.MaxFileLines)
59	assert.Equal(t, hunks, file.Additions)
60	assert.Equal(t, hunks, file.Deletions)
61	// The exact byte boundary fits; one fewer byte must discard the file even
62	// though every changed line's text alone easily fits.
63	limits.MaxTotalBytes = textBytes
64	d, err = g.GetCommit(context.Background(), f.Name, domain.Revision{}, limits)
65	require.NoError(t, err)
66	assert.Equal(t, domain.OmitNone, d.Diff.Files[0].Omitted)
67	limits.MaxTotalBytes--
68	require.Greater(t, limits.MaxTotalBytes, changedBytes)
69	d, err = g.GetCommit(context.Background(), f.Name, domain.Revision{}, limits)
70	require.NoError(t, err)
71	assert.False(t, d.Diff.Truncated, "only MaxFiles sets Diff.Truncated")
72	assert.Equal(t, domain.OmitUnavailable, d.Diff.Files[0].Omitted)
73	assert.Empty(t, d.Diff.Files[0].Hunks)
74}
75
76// DiffOmissions checks the omission reasons: generated files skipped without
77// reading their hunks, one file read without a line cap, and the MaxFiles
78// overflow being the only thing that sets Diff.Truncated.
79func DiffOmissions(t *testing.T, mk Maker) {
80	g, data := mk(t)
81	ctx := context.Background()
82	f := NewFixture(t, data, "diff-omissions")
83	f.Write("base.txt", "x\n").Commit("base")
84	f.Write("a.txt", "small\n")
85	// The generated file (515 bytes) would overflow the 300-byte budget below
86	// if it were read: OmitGenerated, not OmitUnavailable, proves its hunks
87	// are not read.
88	f.Write("go.sum", strings.Repeat("h1:"+strings.Repeat("z", 100)+"\n", 5))
89	f.Lines("z-big.txt", "line", 2500)
90	f.Commit("change")
91
92	limits := domain.DiffLimits{MaxTotalBytes: 300, MaxFileLines: 2000, SkipGenerated: true}
93	d, err := g.GetCommit(ctx, f.Name, domain.Revision{}, limits)
94	require.NoError(t, err)
95	require.Len(t, d.Diff.Files, 3)
96	byPath := map[string]domain.FileDiff{}
97	for _, file := range d.Diff.Files {
98		byPath[file.NewPath] = file
99	}
100	assert.Equal(t, domain.OmitNone, byPath["a.txt"].Omitted)
101	assert.Len(t, byPath["a.txt"].Hunks, 1)
102	assert.Equal(t, domain.OmitGenerated, byPath["go.sum"].Omitted)
103	assert.Empty(t, byPath["go.sum"].Hunks)
104	assert.Equal(t, 5, byPath["go.sum"].Additions)
105	assert.Equal(t, domain.OmitLarge, byPath["z-big.txt"].Omitted)
106	assert.Empty(t, byPath["z-big.txt"].Hunks)
107	assert.False(t, d.Diff.Truncated)
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.
111	limits.SkipGenerated = false
112	d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, limits)
113	require.NoError(t, err)
114	sum := -1
115	for i, file := range d.Diff.Files {
116		if file.NewPath == "go.sum" {
117			sum = i
118			assert.Equal(t, domain.OmitBudget, file.Omitted, "earlier files spent bytes")
119			assert.Empty(t, file.Hunks)
120		}
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)
129
130	// OneFileDiffLimits reads a file over 2,000 changed lines, and only it.
131	idx := -1
132	for i, file := range d.Diff.Files {
133		if file.NewPath == "z-big.txt" {
134			idx = i
135		}
136	}
137	require.GreaterOrEqual(t, idx, 0)
138	d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, domain.OneFileDiffLimits(idx))
139	require.NoError(t, err)
140	for i, file := range d.Diff.Files {
141		assert.Equal(t, domain.OmitNone, file.Omitted)
142		if i == idx {
143			require.Len(t, file.Hunks, 1)
144			assert.Len(t, file.Hunks[0].Lines, 2500)
145		} else {
146			assert.Empty(t, file.Hunks)
147		}
148	}
149	assert.False(t, d.Diff.Truncated)
150
151	// MaxFiles alone sets Diff.Truncated.
152	d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, domain.DiffLimits{MaxFiles: 2, MaxTotalBytes: 1 << 20})
153	require.NoError(t, err)
154	assert.Len(t, d.Diff.Files, 2)
155	assert.True(t, d.Diff.Truncated)
156	for _, file := range d.Diff.Files {
157		assert.Equal(t, domain.OmitNone, file.Omitted)
158	}
159
160	// A one-file read is capped at MaxOneFileLines changed lines: the cap
161	// itself loads, one more line is OmitUnavailable (it cannot be loaded).
162	f.Lines("cap.txt", "c", domain.MaxOneFileLines).Lines("over.txt", "o", domain.MaxOneFileLines+1).Commit("caps")
163	d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, domain.DiffLimits{MaxTotalBytes: 1})
164	require.NoError(t, err)
165	capIdx, overIdx := -1, -1
166	for i, file := range d.Diff.Files {
167		switch file.NewPath {
168		case "cap.txt":
169			capIdx = i
170		case "over.txt":
171			overIdx = i
172		}
173	}
174	require.GreaterOrEqual(t, capIdx, 0)
175	require.GreaterOrEqual(t, overIdx, 0)
176	d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, domain.OneFileDiffLimits(capIdx))
177	require.NoError(t, err)
178	assert.Equal(t, domain.OmitNone, d.Diff.Files[capIdx].Omitted)
179	require.Len(t, d.Diff.Files[capIdx].Hunks, 1)
180	assert.Len(t, d.Diff.Files[capIdx].Hunks[0].Lines, domain.MaxOneFileLines)
181	d, err = g.GetCommit(ctx, f.Name, domain.Revision{}, domain.OneFileDiffLimits(overIdx))
182	require.NoError(t, err)
183	assert.Equal(t, domain.OmitUnavailable, d.Diff.Files[overIdx].Omitted)
184	assert.Empty(t, d.Diff.Files[overIdx].Hunks)
185}

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 45

↑↓ moveenter openesc close