v0.19.0 55a688e

changed_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package gitcli_test
4
5import (
6	"context"
7	"testing"
8
9	"github.com/stretchr/testify/assert"
10	"github.com/stretchr/testify/require"
11
12	"bnema.dev/ermite/internal/domain"
13)
14
15func TestChangedPaths(t *testing.T) {
16	g, data := newGit(t)
17	ctx := context.Background()
18	m := newMergeFixture(t, data)
19	t.Cleanup(func() { _ = g.Close(ctx) })
20
21	// b..d changed a.txt and added c.txt.
22	got, truncated, err := g.ChangedPaths(ctx, m.f.Name, m.b, m.d, 300)
23	require.NoError(t, err)
24	assert.False(t, truncated)
25	assert.ElementsMatch(t, []string{"a.txt", "c.txt"}, got)
26
27	// A zero base lists every file of the commit, as for a new repository.
28	got, _, err = g.ChangedPaths(ctx, m.f.Name, domain.ObjectID{}, m.b, 300)
29	require.NoError(t, err)
30	assert.ElementsMatch(t, []string{"a.txt", "b.txt"}, got)
31
32	// No change is an empty list, not an error.
33	got, truncated, err = g.ChangedPaths(ctx, m.f.Name, m.d, m.d, 300)
34	require.NoError(t, err)
35	assert.Empty(t, got)
36	assert.False(t, truncated)
37
38	// A bound cuts the list and says so.
39	got, truncated, err = g.ChangedPaths(ctx, m.f.Name, domain.ObjectID{}, m.d, 2)
40	require.NoError(t, err)
41	assert.Len(t, got, 2)
42	assert.True(t, truncated)
43	got, truncated, err = g.ChangedPaths(ctx, m.f.Name, domain.ObjectID{}, m.d, 3)
44	require.NoError(t, err)
45	assert.Len(t, got, 3)
46	assert.False(t, truncated, "a list of exactly the bound is complete")
47
48	_, _, err = g.ChangedPaths(ctx, m.f.Name, m.b, m.d, 0)
49	assert.ErrorIs(t, err, domain.ErrInvalid)
50	_, _, err = g.ChangedPaths(ctx, name(t, "missing"), m.b, m.d, 5)
51	assert.ErrorIs(t, err, domain.ErrNotFound)
52	_, _, err = g.ChangedPaths(ctx, m.f.Name, m.b, oidOf(t, "1111111111111111111111111111111111111111"), 5)
53	assert.Error(t, err, "an unknown commit is an error")
54}
55
56func TestChangedPathsKeepsAWholeRenameAndOddNames(t *testing.T) {
57	g, data := newGit(t)
58	ctx := context.Background()
59	m := newMergeFixture(t, data)
60	t.Cleanup(func() { _ = g.Close(ctx) })
61
62	m.f.Git("checkout", "-q", "feat")
63	m.f.Write("dir with space/é.txt", "see\n")
64	m.f.Git("rm", "-q", "c.txt")
65	d2 := oidOf(t, m.f.Commit("rename"))
66	got, _, err := g.ChangedPaths(ctx, m.f.Name, m.featTip, d2, 10)
67	require.NoError(t, err)
68	assert.ElementsMatch(t, []string{"c.txt", "dir with space/é.txt"}, got, "both sides of a rename, verbatim")
69}
70
71func TestCommitsOffBranches(t *testing.T) {
72	g, data := newGit(t)
73	ctx := context.Background()
74	m := newMergeFixture(t, data)
75	t.Cleanup(func() { _ = g.Close(ctx) })
76
77	msgs := func(cs []domain.Commit) []string {
78		var out []string
79		for _, c := range cs {
80			out = append(out, c.Summary)
81		}
82		return out
83	}
84
85	// Every commit of the fixture is on main or feat.
86	got, more, err := g.CommitsOffBranches(ctx, m.f.Name, m.d, 10)
87	require.NoError(t, err)
88	assert.Empty(t, got, "feat has its tip")
89	assert.False(t, more)
90
91	// A commit no branch has, on top of feat: listed alone, then with its
92	// parent when that has no branch either (a branch deleted, a detached tip).
93	m.f.Git("checkout", "--quiet", "--detach", m.d.String())
94	m.f.Write("t1.txt", "one\n")
95	t1 := oidOf(t, m.f.Commit("tagged one"))
96	m.f.Write("t2.txt", "two\n")
97	t2 := oidOf(t, m.f.Commit("tagged two"))
98	m.f.Git("update-ref", "refs/tags/v1", t2.String())
99
100	got, more, err = g.CommitsOffBranches(ctx, m.f.Name, t2, 10)
101	require.NoError(t, err)
102	assert.Equal(t, []string{"tagged two", "tagged one"}, msgs(got), "newest first, down to the commits a branch has")
103	assert.False(t, more)
104	got, _, err = g.CommitsOffBranches(ctx, m.f.Name, t1, 10)
105	require.NoError(t, err)
106	assert.Equal(t, []string{"tagged one"}, msgs(got))
107
108	// A bound cuts the list and says so.
109	got, more, err = g.CommitsOffBranches(ctx, m.f.Name, t2, 1)
110	require.NoError(t, err)
111	assert.Equal(t, []string{"tagged two"}, msgs(got))
112	assert.True(t, more)
113
114	// Another tag is no branch: t2 stays listed although v1 points to it, and
115	// moving a branch onto t1 takes t1 out of the list.
116	m.f.Git("update-ref", "refs/heads/rel", t1.String())
117	got, _, err = g.CommitsOffBranches(ctx, m.f.Name, t2, 10)
118	require.NoError(t, err)
119	assert.Equal(t, []string{"tagged two"}, msgs(got))
120
121	_, _, err = g.CommitsOffBranches(ctx, m.f.Name, domain.ObjectID{}, 10)
122	assert.ErrorIs(t, err, domain.ErrInvalid)
123	_, _, err = g.CommitsOffBranches(ctx, m.f.Name, t2, 0)
124	assert.ErrorIs(t, err, domain.ErrInvalidPageSize)
125	_, _, err = g.CommitsOffBranches(ctx, name(t, "missing"), t2, 5)
126	assert.ErrorIs(t, err, domain.ErrNotFound)
127}

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