v0.7.0 45bfc5b

freshness_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package ermitegit_test
4
5import (
6	"context"
7	"io"
8	"os"
9	"os/exec"
10	"strings"
11	"testing"
12	"time"
13
14	"github.com/stretchr/testify/assert"
15	"github.com/stretchr/testify/require"
16
17	"bnema.dev/ermite/internal/adapters/ermitegit"
18	"bnema.dev/ermite/internal/domain"
19	"bnema.dev/ermite/internal/ports"
20	"bnema.dev/ermite/internal/readertest"
21)
22
23// push sends the work tree's HEAD to the bare repository of the fixture as a
24// push does: git receive-pack writes a pack (or loose objects) and updates
25// the ref, with no help from the reader.
26func push(t *testing.T, f *readertest.Fixture, remote, branch string, loose bool) {
27	t.Helper()
28	args := []string{"push", "--quiet", remote, "HEAD:refs/heads/" + branch}
29	if loose {
30		args = append([]string{"-c", "receive.unpackLimit=1000"}, args...)
31	}
32	cmd := exec.Command("git", args...)
33	cmd.Dir = f.Work
34	cmd.Env = []string{"PATH=" + os.Getenv("PATH"), "HOME=" + f.Work, "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_DIR=" + f.Bare}
35	out, err := cmd.CombinedOutput()
36	require.NoError(t, err, string(out))
37}
38
39// TestFreshness proves that a read that starts after a push sees the push
40// with the adapter's own Refresh disabled (an interval of one hour: only the
41// first read refreshes): new commits in a pack and as loose objects, new
42// branches, a ref that moves, a repack that replaces the packs, a new
43// commit-graph layer, a deleted branch, tags.
44func TestFreshness(t *testing.T) {
45	ctx := context.Background()
46	a, g, data := newAdapter(t, ermitegit.AllRoutes(ermitegit.ErmiteGit), func(c *ermitegit.Config) { c.RefreshEvery = time.Hour })
47
48	// The work tree is an ordinary clone-like repository; "remote" is the
49	// bare repository the adapter reads.
50	f := readertest.NewFixture(t, data, "pushed")
51	bare := f.Bare
52	// A separate repository makes the commits and pushes them.
53	work := readertest.NewFixture(t, t.TempDir(), "work")
54	work.Write("a.txt", "1\n")
55	work.Commit("first")
56	push(t, work, bare, "main", false)
57
58	head := func(t *testing.T) string {
59		t.Helper()
60		res, err := a.ResolveRevision(ctx, f.Name, domain.Revision{})
61		require.NoError(t, err)
62		return res.CommitID.String()
63	}
64	rev := func(s string) domain.Revision {
65		r, err := domain.ParseRevision(s)
66		require.NoError(t, err)
67		return r
68	}
69	cli := func(t *testing.T) string {
70		t.Helper()
71		res, err := g.ResolveRevision(ctx, f.Name, domain.Revision{})
72		require.NoError(t, err)
73		return res.CommitID.String()
74	}
75	require.Equal(t, work.Git("rev-parse", "HEAD"), head(t), "the first read")
76
77	for i, step := range []struct {
78		name  string
79		loose bool
80		after func()
81	}{
82		{"pack", false, nil},
83		{"loose objects", true, nil},
84		{"repack replaces the packs", false, func() { f.Git("repack", "-adf") }},
85		{"commit-graph layer", false, func() { f.Git("commit-graph", "write", "--reachable", "--changed-paths", "--split") }},
86		{"gc packs the refs", false, func() { f.Git("gc", "--quiet", "--prune=now") }},
87	} {
88		t.Run(step.name, func(t *testing.T) {
89			work.Write("a.txt", strings.Repeat("x", i+2)+"\n").Write("dir/new.txt", step.name+"\n")
90			c := work.Commit("push " + step.name)
91			push(t, work, bare, "main", step.loose)
92			// Not a single Refresh in between: the very next read sees it.
93			assert.Equal(t, c, head(t))
94			page, err := a.ListCommits(ctx, f.Name, ports.CommitQuery{Limit: 3})
95			require.NoError(t, err)
96			require.NotEmpty(t, page.Commits)
97			assert.Equal(t, c, page.Commits[0].ID.String())
98			tree, err := a.GetTree(ctx, f.Name, domain.Revision{}, domain.TreePath{}, true)
99			require.NoError(t, err)
100			require.NotNil(t, tree.Entries[0].LastCommit)
101			assert.Equal(t, c, tree.Entries[0].LastCommit.ID.String(), "dir/ was last touched by the push")
102			_, rc, err := a.OpenBlob(ctx, f.Name, domain.Revision{}, readertest.TreePath(t, "dir/new.txt"))
103			require.NoError(t, err)
104			body, err := io.ReadAll(rc)
105			require.NoError(t, err)
106			require.NoError(t, rc.Close())
107			assert.Equal(t, step.name+"\n", string(body))
108			assert.Equal(t, cli(t), head(t), "the same answer as git")
109
110			if step.after != nil {
111				step.after()
112				assert.Equal(t, c, head(t), "after maintenance")
113				tree, err := a.GetTree(ctx, f.Name, domain.Revision{}, domain.TreePath{}, true)
114				require.NoError(t, err)
115				assert.Equal(t, c, tree.Entries[0].LastCommit.ID.String())
116			}
117		})
118	}
119
120	t.Run("new and deleted refs", func(t *testing.T) {
121		work.Git("branch", "-f", "feature/new", "HEAD")
122		push(t, work, bare, "feature/new", false)
123		res, err := a.ResolveRevision(ctx, f.Name, rev("feature/new"))
124		require.NoError(t, err)
125		assert.Equal(t, "refs/heads/feature/new", res.Ref)
126
127		work.Git("tag", "-a", "-m", "release", "v9")
128		work.Git("push", "--quiet", bare, "refs/tags/v9")
129		refs, err := a.ListRefs(ctx, f.Name)
130		require.NoError(t, err)
131		var names []string
132		for _, r := range refs {
133			names = append(names, r.Name)
134		}
135		assert.Contains(t, names, "refs/tags/v9")
136		want, err := g.ListRefs(ctx, f.Name)
137		require.NoError(t, err)
138		assert.Equal(t, want, refs)
139
140		f.Git("update-ref", "-d", "refs/heads/feature/new")
141		_, err = a.ResolveRevision(ctx, f.Name, rev("feature/new"))
142		assert.ErrorIs(t, err, domain.ErrNotFound)
143		f.Git("pack-refs", "--all")
144		f.Git("update-ref", "-d", "refs/tags/v9")
145		_, err = a.ResolveRevision(ctx, f.Name, rev("v9"))
146		assert.ErrorIs(t, err, domain.ErrNotFound)
147	})
148
149	t.Run("the repository is deleted and recreated", func(t *testing.T) {
150		require.NoError(t, g.Trash(ctx, f.Name))
151		_, err := a.ListRefs(ctx, f.Name)
152		assert.ErrorIs(t, err, domain.ErrNotFound)
153		assert.Equal(t, 0, a.OpenRepos(), "the trash hook dropped the repository")
154
155		again := readertest.NewFixture(t, data, "pushed")
156		again.Write("b.txt", "new repo\n")
157		c := again.Commit("a new repository under the old name")
158		assert.Equal(t, c, head(t), "no trace of the old repository")
159	})
160}
161
162// TestReplacedDirectoryIsNotConfused: the cache is keyed by directory, so a
163// directory replaced by another repository (a restore, a rename onto the same
164// name) must not serve the old one: the inode tells.
165func TestReplacedDirectoryIsNotConfused(t *testing.T) {
166	ctx := context.Background()
167	a, _, data := newAdapter(t, ermitegit.AllRoutes(ermitegit.ErmiteGit), func(c *ermitegit.Config) { c.RefreshEvery = time.Hour })
168	one := readertest.NewFixture(t, data, "one")
169	one.Write("a", "1\n")
170	c1 := one.Commit("one")
171	res, err := a.ResolveRevision(ctx, one.Name, domain.Revision{})
172	require.NoError(t, err)
173	require.Equal(t, c1, res.CommitID.String())
174
175	two := readertest.NewFixture(t, t.TempDir(), "two")
176	two.Write("b", "2\n")
177	c2 := two.Commit("two")
178	require.NoError(t, os.RemoveAll(one.Bare))
179	require.NoError(t, os.Rename(two.Bare, one.Bare))
180	res, err = a.ResolveRevision(ctx, one.Name, domain.Revision{})
181	require.NoError(t, err)
182	assert.Equal(t, c2, res.CommitID.String())
183}

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