v0.2.0 41cf2ec

backend_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package gitcli_test
4
5import (
6	"context"
7	"os"
8	"os/exec"
9	"path/filepath"
10	"strconv"
11	"strings"
12	"testing"
13
14	"github.com/stretchr/testify/assert"
15	"github.com/stretchr/testify/require"
16
17	"bnema.dev/ermite/internal/adapters/gitcli"
18	"bnema.dev/ermite/internal/domain"
19)
20
21func envMap(env []string) map[string]string {
22	m := map[string]string{}
23	for _, e := range env {
24		k, v, _ := strings.Cut(e, "=")
25		m[k] = v
26	}
27	return m
28}
29
30// backendConf returns the GIT_CONFIG_KEY/VALUE pairs of a backend environment
31// and checks that GIT_CONFIG_COUNT matches them.
32func backendConf(e map[string]string) map[string]string {
33	conf := map[string]string{}
34	n := 0
35	for {
36		k, ok := e["GIT_CONFIG_KEY_"+strconv.Itoa(n)]
37		if !ok {
38			break
39		}
40		conf[k] = e["GIT_CONFIG_VALUE_"+strconv.Itoa(n)]
41		n++
42	}
43	if e["GIT_CONFIG_COUNT"] != strconv.Itoa(n) {
44		panic("GIT_CONFIG_COUNT does not match the keys")
45	}
46	return conf
47}
48
49func TestHTTPBackendReceiveMaxInputSize(t *testing.T) {
50	ctx := context.Background()
51	_, data := newGit(t)
52	bin, err := exec.LookPath("git")
53	require.NoError(t, err)
54	g := gitcli.New(gitcli.Config{Bin: bin, DataDir: data, Path: os.Getenv("PATH"), MaxReceiveBytes: 4 << 30})
55
56	push, err := g.HTTPBackend(ctx, true)
57	require.NoError(t, err)
58	assert.Equal(t, "4294967296", backendConf(envMap(push.Env))["receive.maxInputSize"])
59
60	read, err := g.HTTPBackend(ctx, false)
61	require.NoError(t, err)
62	assert.NotContains(t, backendConf(envMap(read.Env)), "receive.maxInputSize", "a fetch does not receive a pack")
63}
64
65func TestHTTPBackendEnvironment(t *testing.T) {
66	ctx := context.Background()
67	g, data := newGit(t)
68
69	read, err := g.HTTPBackend(ctx, false)
70	require.NoError(t, err)
71	execPath, err := exec.Command("git", "--exec-path").Output()
72	require.NoError(t, err)
73	assert.Equal(t, filepath.Join(strings.TrimSpace(string(execPath)), "git-http-backend"), read.Path)
74	assert.Equal(t, filepath.Join(data, "repos"), read.Dir)
75
76	e := envMap(read.Env)
77	assert.Equal(t, "1", e["GIT_CONFIG_NOSYSTEM"])
78	assert.Equal(t, filepath.Join(data, "run", "git-home"), e["HOME"])
79	assert.Equal(t, "0", e["GIT_TERMINAL_PROMPT"])
80	assert.Equal(t, filepath.Join(data, "repos"), e["GIT_PROJECT_ROOT"])
81	assert.Equal(t, "1", e["GIT_HTTP_EXPORT_ALL"])
82	assert.NotContains(t, e, "REMOTE_USER", "a read is not a push")
83
84	assert.Equal(t, map[string]string{
85		"core.hooksPath":         filepath.Join(data, "run", "hooks"),
86		"receive.fsckObjects":    "true",
87		"uploadpack.allowFilter": "true",
88	}, backendConf(e))
89
90	push, err := g.HTTPBackend(ctx, true)
91	require.NoError(t, err)
92	pe := envMap(push.Env)
93	assert.Equal(t, "owner", pe["REMOTE_USER"])
94	assert.Equal(t, map[string]string{
95		"core.hooksPath":         filepath.Join(data, "run", "hooks"),
96		"receive.fsckObjects":    "true",
97		"uploadpack.allowFilter": "true",
98		"http.receivepack":       "true",
99	}, backendConf(pe), "http.receivepack only for a push; no size limit unless configured")
100
101	// Nothing from the caller's environment leaks.
102	t.Setenv("GIT_DIR", "/leak")
103	again, err := g.HTTPBackend(ctx, true)
104	require.NoError(t, err)
105	assert.NotContains(t, envMap(again.Env), "GIT_DIR")
106
107	// The slice is the caller's: appending does not corrupt the next call.
108	_ = append(read.Env, "X=1")
109	third, err := g.HTTPBackend(ctx, false)
110	require.NoError(t, err)
111	assert.NotContains(t, envMap(third.Env), "X")
112}
113
114func TestHTTPBackendMissing(t *testing.T) {
115	data := t.TempDir()
116	script := writeScript(t, `case "$1" in --exec-path) echo "`+data+`/nowhere";; *) echo "git version 2.50.0";; esac`)
117	g := gitcli.New(gitcli.Config{Bin: script, DataDir: data, Path: "/usr/bin:/bin"})
118	_, err := g.HTTPBackend(context.Background(), false)
119	require.Error(t, err)
120	assert.Contains(t, err.Error(), "git-http-backend")
121
122	// A non-executable file is refused too.
123	require.NoError(t, os.MkdirAll(filepath.Join(data, "exec"), 0o755))
124	require.NoError(t, os.WriteFile(filepath.Join(data, "exec", "git-http-backend"), []byte("x"), 0o644))
125	script = writeScript(t, `echo "`+data+`/exec"`)
126	g = gitcli.New(gitcli.Config{Bin: script, DataDir: data, Path: "/usr/bin:/bin"})
127	_, err = g.HTTPBackend(context.Background(), false)
128	require.Error(t, err)
129	assert.Contains(t, err.Error(), "not an executable")
130}
131
132func TestDefaultBranchAndMaintain(t *testing.T) {
133	ctx := context.Background()
134	g, data := newGit(t)
135	n := name(t, "tools")
136	id, err := g.Stage(ctx)
137	require.NoError(t, err)
138	require.NoError(t, g.Publish(ctx, id, n))
139	dir := filepath.Join(data, "repos", "tools.git")
140
141	branch, exists, err := g.DefaultBranch(ctx, n)
142	require.NoError(t, err)
143	assert.Equal(t, "main", branch)
144	assert.False(t, exists, "no commit yet")
145
146	_, _, err = g.DefaultBranch(ctx, name(t, "ghost"))
147	assert.ErrorIs(t, err, domain.ErrNotFound)
148
149	// Put a commit on "dev" through a work tree.
150	wt := t.TempDir()
151	env := []string{"PATH=" + os.Getenv("PATH"), "HOME=" + t.TempDir(), "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null",
152		"GIT_AUTHOR_NAME=T", "GIT_AUTHOR_EMAIL=t@x", "GIT_COMMITTER_NAME=T", "GIT_COMMITTER_EMAIL=t@x"}
153	run := func(args ...string) {
154		cmd := exec.Command("git", args...)
155		cmd.Dir, cmd.Env = wt, env
156		out, err := cmd.CombinedOutput()
157		require.NoError(t, err, string(out))
158	}
159	run("init", "-q", "--initial-branch=dev")
160	require.NoError(t, os.WriteFile(filepath.Join(wt, "f"), []byte("x"), 0o644))
161	run("add", "f")
162	run("commit", "-q", "-m", "one")
163	run("push", "-q", dir, "dev")
164
165	_, exists, err = g.DefaultBranch(ctx, n)
166	require.NoError(t, err)
167	assert.False(t, exists, "HEAD still points to main")
168
169	require.NoError(t, g.SetDefaultBranch(ctx, n, "dev"))
170	branch, exists, err = g.DefaultBranch(ctx, n)
171	require.NoError(t, err)
172	assert.Equal(t, "dev", branch)
173	assert.True(t, exists)
174	assert.Equal(t, "refs/heads/dev", gitOut(t, dir, "symbolic-ref", "HEAD"))
175
176	for _, bad := range []string{"", "-x", "a b", "a..b"} {
177		assert.Error(t, g.SetDefaultBranch(ctx, n, bad), bad)
178	}
179	assert.ErrorIs(t, g.SetDefaultBranch(ctx, name(t, "ghost"), "dev"), domain.ErrNotFound)
180
181	require.NoError(t, g.Maintain(ctx, n))
182	assert.FileExists(t, filepath.Join(dir, "objects", "info", "commit-graphs", "commit-graph-chain"))
183	assert.ErrorIs(t, g.Maintain(ctx, name(t, "ghost")), domain.ErrNotFound)
184}

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 3

Tags 44

↑↓ moveenter openesc close