detached 9fec184

fix(mr): bound conflict paths with exact-sized copies

commit
9fec1841ad80f37ed1cc6634d011eafbe62f282d
author
brice · · 2026-10-19 14:52 UTC
parent
880001f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +30 -2
M internal/domain/mergerequest.go +12 -2
@@ -540,10 +540,20 @@ type MergeTreeResult struct {
540540 // set when some were dropped. Adapters return the raw result; the bound is
541541 // the domain's.
542542 func (r MergeTreeResult) Bounded() MergeTreeResult {
543- if len(r.ConflictPaths) > MaxConflictPaths {
544- r.ConflictPaths = r.ConflictPaths[:MaxConflictPaths:MaxConflictPaths]
543+ kept := r.ConflictPaths
544+ if len(kept) > MaxConflictPaths {
545+ kept = kept[:MaxConflictPaths]
545546 r.Truncated = true
546547 }
548+ if kept != nil {
549+ // Exact-sized copies: neither the backing array nor the git output the
550+ // paths were cut from stays referenced.
551+ out := make([]string, len(kept))
552+ for i, p := range kept {
553+ out[i] = strings.Clone(p)
554+ }
555+ r.ConflictPaths = out
556+ }
547557 return r
548558 }
549559
@@ -540,10 +540,20 @@ type MergeTreeResult struct {
540 // set when some were dropped. Adapters return the raw result; the bound is540 // set when some were dropped. Adapters return the raw result; the bound is
541 // the domain's.541 // the domain's.
542 func (r MergeTreeResult) Bounded() MergeTreeResult {542 func (r MergeTreeResult) Bounded() MergeTreeResult {
543- if len(r.ConflictPaths) > MaxConflictPaths {543+ kept := r.ConflictPaths
544- r.ConflictPaths = r.ConflictPaths[:MaxConflictPaths:MaxConflictPaths]544+ if len(kept) > MaxConflictPaths {
545+ kept = kept[:MaxConflictPaths]
545 r.Truncated = true546 r.Truncated = true
546 }547 }
548+ if kept != nil {
549+ // Exact-sized copies: neither the backing array nor the git output the
550+ // paths were cut from stays referenced.
551+ out := make([]string, len(kept))
552+ for i, p := range kept {
553+ out[i] = strings.Clone(p)
554+ }
555+ r.ConflictPaths = out
556+ }
547 return r557 return r
548 }558 }
549 559
M internal/domain/mergetree_bound_test.go +18 -0
@@ -7,6 +7,7 @@ import (
77 "testing"
88
99 "github.com/stretchr/testify/assert"
10+ "github.com/stretchr/testify/require"
1011
1112 "bnema.dev/ermite/internal/domain"
1213 )
@@ -33,3 +34,20 @@ func TestBoundBehind(t *testing.T) {
3334 assert.Equal(t, domain.MaxBehindCount, n)
3435 assert.True(t, capped)
3536 }
37+
38+func TestMergeTreeResultBoundedCopiesThePaths(t *testing.T) {
39+ var paths []string
40+ for i := 0; i < domain.MaxConflictPaths+2; i++ {
41+ paths = append(paths, strconv.Itoa(i))
42+ }
43+ got := domain.MergeTreeResult{ConflictPaths: paths}.Bounded()
44+ require.Len(t, got.ConflictPaths, domain.MaxConflictPaths)
45+ assert.Equal(t, len(got.ConflictPaths), cap(got.ConflictPaths))
46+ assert.NotSame(t, &paths[0], &got.ConflictPaths[0], "a new array, not the git output's")
47+ assert.Equal(t, paths[:domain.MaxConflictPaths], got.ConflictPaths)
48+
49+ got = domain.MergeTreeResult{ConflictPaths: paths[:3:10]}.Bounded()
50+ assert.Equal(t, 3, cap(got.ConflictPaths))
51+ assert.False(t, got.Truncated)
52+ assert.Nil(t, domain.MergeTreeResult{}.Bounded().ConflictPaths)
53+}
@@ -7,6 +7,7 @@ import (
7 "testing"7 "testing"
8 8
9 "github.com/stretchr/testify/assert"9 "github.com/stretchr/testify/assert"
10+ "github.com/stretchr/testify/require"
10 11
11 "bnema.dev/ermite/internal/domain"12 "bnema.dev/ermite/internal/domain"
12 )13 )
@@ -33,3 +34,20 @@ func TestBoundBehind(t *testing.T) {
33 assert.Equal(t, domain.MaxBehindCount, n)34 assert.Equal(t, domain.MaxBehindCount, n)
34 assert.True(t, capped)35 assert.True(t, capped)
35 }36 }
37+
38+func TestMergeTreeResultBoundedCopiesThePaths(t *testing.T) {
39+ var paths []string
40+ for i := 0; i < domain.MaxConflictPaths+2; i++ {
41+ paths = append(paths, strconv.Itoa(i))
42+ }
43+ got := domain.MergeTreeResult{ConflictPaths: paths}.Bounded()
44+ require.Len(t, got.ConflictPaths, domain.MaxConflictPaths)
45+ assert.Equal(t, len(got.ConflictPaths), cap(got.ConflictPaths))
46+ assert.NotSame(t, &paths[0], &got.ConflictPaths[0], "a new array, not the git output's")
47+ assert.Equal(t, paths[:domain.MaxConflictPaths], got.ConflictPaths)
48+
49+ got = domain.MergeTreeResult{ConflictPaths: paths[:3:10]}.Bounded()
50+ assert.Equal(t, 3, cap(got.ConflictPaths))
51+ assert.False(t, got.Truncated)
52+ assert.Nil(t, domain.MergeTreeResult{}.Bounded().ConflictPaths)
53+}

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 4

Tags 45

↑↓ moveenter openesc close