detached 973e682

commitobjects.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package readertest
4
5import (
6	"bytes"
7	"context"
8	"fmt"
9	"os"
10	"os/exec"
11	"path/filepath"
12	"strings"
13	"testing"
14
15	"github.com/stretchr/testify/assert"
16	"github.com/stretchr/testify/require"
17
18	"bnema.dev/ermite/internal/domain"
19	"bnema.dev/ermite/internal/ports"
20)
21
22// CommitObjectsMaker builds a CommitObjectReader serving the repositories
23// below data/repos, and returns it with the data directory.
24type CommitObjectsMaker func(t *testing.T) (ports.CommitObjectReader, string)
25
26// CommitObjects runs the CommitObjectReader contract against the readers mk
27// builds: the raw bytes of unsigned and SSH-signed commits equal `git cat-file
28// commit`; missing ids, trees and blobs are left out; a missing repository is
29// domain.ErrNotFound; too many ids are domain.ErrInvalid.
30func CommitObjects(t *testing.T, mk CommitObjectsMaker) {
31	ctx := context.Background()
32	r, data := mk(t)
33	f := NewFixture(t, data, "signed")
34
35	f.Write("a.txt", "one\n")
36	unsigned := f.Commit("unsigned")
37	signed := signedCommit(t, f, "b.txt", "signed with ssh")
38	f.Write("c.txt", "three\n")
39	again := f.Commit("unsigned again")
40	tree := f.Git("rev-parse", signed+"^{tree}")
41	blob := f.Git("rev-parse", signed+":b.txt")
42	missing := ObjectID(t, strings.Repeat("1", 40))
43
44	ids := []domain.ObjectID{ObjectID(t, unsigned), ObjectID(t, signed), ObjectID(t, again)}
45	got, err := r.ReadCommitObjects(ctx, f.Name, ids)
46	require.NoError(t, err)
47	require.Len(t, got, len(ids))
48	for _, id := range ids {
49		assert.Equal(t, f.Git("cat-file", "commit", id.String()), strings.TrimSpace(string(got[id])), "commit %s", id)
50		assert.True(t, bytes.Equal(got[id], rawCommit(t, f, id.String())), "raw bytes of %s", id)
51	}
52	assert.Contains(t, string(got[ids[1]]), "gpgsig -----BEGIN SSH SIGNATURE-----")
53	assert.NotContains(t, string(got[ids[0]]), "gpgsig")
54
55	// Missing ids, a tree and a blob are left out; duplicates are fine.
56	mixed := []domain.ObjectID{ids[1], missing, ObjectID(t, tree), ObjectID(t, blob), ids[1], ids[0]}
57	got, err = r.ReadCommitObjects(ctx, f.Name, mixed)
58	require.NoError(t, err)
59	assert.Len(t, got, 2)
60	assert.Contains(t, got, ids[0])
61	assert.Contains(t, got, ids[1])
62
63	got, err = r.ReadCommitObjects(ctx, f.Name, nil)
64	require.NoError(t, err)
65	assert.Empty(t, got)
66
67	// A repository reads the same after its objects were packed.
68	f.Git("gc", "--quiet")
69	got, err = r.ReadCommitObjects(ctx, f.Name, ids)
70	require.NoError(t, err)
71	assert.Equal(t, rawCommit(t, f, signed), got[ids[1]], "packed")
72
73	t.Run("budget", func(t *testing.T) { commitObjectsBudget(t, r, f) })
74
75	cancelled, cancel := context.WithCancel(ctx)
76	cancel()
77	_, err = r.ReadCommitObjects(cancelled, f.Name, ids)
78	assert.ErrorIs(t, err, context.Canceled)
79
80	_, err = r.ReadCommitObjects(ctx, RepoName(t, "nope"), ids)
81	assert.ErrorIs(t, err, domain.ErrNotFound)
82
83	many := make([]domain.ObjectID, ports.MaxCommitLimit+1)
84	for i := range many {
85		many[i] = missing
86	}
87	_, err = r.ReadCommitObjects(ctx, f.Name, many)
88	assert.ErrorIs(t, err, domain.ErrInvalid)
89	_, err = r.ReadCommitObjects(ctx, f.Name, many[:ports.MaxCommitLimit])
90	require.NoError(t, err)
91}
92
93// commitObjectsBudget checks the aggregate budget: ten commits of about
94// 900 KiB each (under the per-object limit) do not fit in
95// domain.MaxCommitObjectsBytes, so the read stops before the one that would
96// not fit, and the others are left out.
97func commitObjectsBudget(t *testing.T, r ports.CommitObjectReader, f *Fixture) {
98	const n, msgSize = 10, 900 << 10
99	tree := f.Git("rev-parse", "HEAD^{tree}")
100	big := make([]domain.ObjectID, n)
101	for i := range big {
102		msg := fmt.Sprintf("big %d\n", i) + strings.Repeat("x", msgSize-8) + "\n"
103		raw := fmt.Sprintf("tree %s\nauthor A <a@x.org> 1 +0000\ncommitter A <a@x.org> 1 +0000\n\n%s", tree, msg)
104		big[i] = ObjectID(t, f.WriteObject("commit", raw))
105	}
106	got, err := r.ReadCommitObjects(context.Background(), f.Name, big)
107	require.NoError(t, err)
108	var total int
109	for id, raw := range got {
110		assert.Contains(t, big, id)
111		total += len(raw)
112	}
113	assert.LessOrEqual(t, total, domain.MaxCommitObjectsBytes)
114	assert.Len(t, got, domain.MaxCommitObjectsBytes/(msgSize+200), "as many as fit in the budget")
115	assert.Less(t, len(got), n)
116	for _, id := range big[:len(got)] {
117		assert.Contains(t, got, id, "the budget is spent in the order of the ids")
118	}
119}
120
121// ObjectID parses a full object id.
122func ObjectID(t testing.TB, s string) domain.ObjectID {
123	t.Helper()
124	id, err := domain.ParseObjectID(s)
125	require.NoError(t, err)
126	return id
127}
128
129// rawCommit returns `git cat-file commit <id>` byte for byte (Fixture.Git
130// trims its output).
131func rawCommit(t testing.TB, f *Fixture, id string) []byte {
132	t.Helper()
133	cmd := exec.Command("git", "--git-dir="+f.Bare, "cat-file", "commit", id)
134	cmd.Env = []string{"PATH=" + os.Getenv("PATH"), "HOME=" + f.Work, "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null"}
135	out, err := cmd.Output()
136	require.NoError(t, err)
137	return out
138}
139
140// signedCommit writes a file and commits it with an SSH signature (a key made
141// for the test, `git commit -S` with gpg.format=ssh). Without ssh-keygen it
142// skips locally and fails when CI is set.
143func signedCommit(t testing.TB, f *Fixture, file, msg string) string {
144	t.Helper()
145	if _, err := exec.LookPath("ssh-keygen"); err != nil {
146		if os.Getenv("CI") != "" {
147			t.Fatalf("ssh-keygen not found")
148		}
149		t.Skip("ssh-keygen not found")
150	}
151	key := filepath.Join(t.TempDir(), "id_ed25519")
152	out, err := exec.Command("ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "test", "-f", key).CombinedOutput()
153	require.NoError(t, err, string(out))
154	f.Write(file, msg+"\n")
155	f.Git("add", "-A")
156	f.Git("-c", "gpg.format=ssh", "-c", "user.signingkey="+key, "commit", "--quiet", "-S", "-m", msg)
157	return f.Git("rev-parse", "HEAD")
158}

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