detached 474d85e

feat(gitcli): set receive.maxInputSize on pushes from Config.MaxReceiveBytes

commit
474d85e5f93fd330ba7b40f5194eeb16232ac64f
author
brice · · 2026-10-10 08:11 UTC
parent
857b2f4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +52 -20
M internal/adapters/gitcli/backend.go +4 -0
@@ -33,6 +33,7 @@ func HooksDir(dataDir string) string { return filepath.Join(dataDir, "run", "hoo
3333 // receive.fsckObjects=true
3434 // uploadpack.allowFilter=true
3535 // http.receivepack=true (push only)
36+// receive.maxInputSize=<max> (push only, when configured)
3637 //
3738 // A push also gets REMOTE_USER=owner.
3839 func (g *Git) HTTPBackend(ctx context.Context, push bool) (ports.HTTPBackend, error) {
@@ -47,6 +48,9 @@ func (g *Git) HTTPBackend(ctx context.Context, push bool) (ports.HTTPBackend, er
4748 }
4849 if push {
4950 conf = append(conf, [2]string{"http.receivepack", "true"})
51+ if g.maxRecv > 0 {
52+ conf = append(conf, [2]string{"receive.maxInputSize", strconv.FormatInt(g.maxRecv, 10)})
53+ }
5054 }
5155 env := append([]string(nil), g.env...)
5256 env = append(env,
@@ -33,6 +33,7 @@ func HooksDir(dataDir string) string { return filepath.Join(dataDir, "run", "hoo
33 // receive.fsckObjects=true33 // receive.fsckObjects=true
34 // uploadpack.allowFilter=true34 // uploadpack.allowFilter=true
35 // http.receivepack=true (push only)35 // http.receivepack=true (push only)
36+// receive.maxInputSize=<max> (push only, when configured)
36 //37 //
37 // A push also gets REMOTE_USER=owner.38 // A push also gets REMOTE_USER=owner.
38 func (g *Git) HTTPBackend(ctx context.Context, push bool) (ports.HTTPBackend, error) {39 func (g *Git) HTTPBackend(ctx context.Context, push bool) (ports.HTTPBackend, error) {
@@ -47,6 +48,9 @@ func (g *Git) HTTPBackend(ctx context.Context, push bool) (ports.HTTPBackend, er
47 }48 }
48 if push {49 if push {
49 conf = append(conf, [2]string{"http.receivepack", "true"})50 conf = append(conf, [2]string{"http.receivepack", "true"})
51+ if g.maxRecv > 0 {
52+ conf = append(conf, [2]string{"receive.maxInputSize", strconv.FormatInt(g.maxRecv, 10)})
53+ }
50 }54 }
51 env := append([]string(nil), g.env...)55 env := append([]string(nil), g.env...)
52 env = append(env,56 env = append(env,
M internal/adapters/gitcli/backend_test.go +43 -19
@@ -7,6 +7,7 @@ import (
77 "os"
88 "os/exec"
99 "path/filepath"
10+ "strconv"
1011 "strings"
1112 "testing"
1213
@@ -26,6 +27,41 @@ func envMap(env []string) map[string]string {
2627 return m
2728 }
2829
30+// backendConf returns the GIT_CONFIG_KEY/VALUE pairs of a backend environment
31+// and checks that GIT_CONFIG_COUNT matches them.
32+func 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+
49+func 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+
2965 func TestHTTPBackendEnvironment(t *testing.T) {
3066 ctx := context.Background()
3167 g, data := newGit(t)
@@ -45,34 +81,22 @@ func TestHTTPBackendEnvironment(t *testing.T) {
4581 assert.Equal(t, "1", e["GIT_HTTP_EXPORT_ALL"])
4682 assert.NotContains(t, e, "REMOTE_USER", "a read is not a push")
4783
48- conf := map[string]string{}
49- for i := range 10 {
50- k, ok := e["GIT_CONFIG_KEY_"+string(rune('0'+i))]
51- if !ok {
52- assert.Equal(t, string(rune('0'+i)), e["GIT_CONFIG_COUNT"], "count matches the keys")
53- break
54- }
55- conf[k] = e["GIT_CONFIG_VALUE_"+string(rune('0'+i))]
56- }
5784 assert.Equal(t, map[string]string{
5885 "core.hooksPath": filepath.Join(data, "run", "hooks"),
5986 "receive.fsckObjects": "true",
6087 "uploadpack.allowFilter": "true",
61- }, conf)
88+ }, backendConf(e))
6289
6390 push, err := g.HTTPBackend(ctx, true)
6491 require.NoError(t, err)
6592 pe := envMap(push.Env)
6693 assert.Equal(t, "owner", pe["REMOTE_USER"])
67- assert.Equal(t, "4", pe["GIT_CONFIG_COUNT"])
68- found := false
69- for i := range 4 {
70- if pe["GIT_CONFIG_KEY_"+string(rune('0'+i))] == "http.receivepack" {
71- assert.Equal(t, "true", pe["GIT_CONFIG_VALUE_"+string(rune('0'+i))])
72- found = true
73- }
74- }
75- assert.True(t, found, "http.receivepack only for a push")
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")
76100
77101 // Nothing from the caller's environment leaks.
78102 t.Setenv("GIT_DIR", "/leak")
@@ -7,6 +7,7 @@ import (
7 "os"7 "os"
8 "os/exec"8 "os/exec"
9 "path/filepath"9 "path/filepath"
10+ "strconv"
10 "strings"11 "strings"
11 "testing"12 "testing"
12 13
@@ -26,6 +27,41 @@ func envMap(env []string) map[string]string {
26 return m27 return m
27 }28 }
28 29
30+// backendConf returns the GIT_CONFIG_KEY/VALUE pairs of a backend environment
31+// and checks that GIT_CONFIG_COUNT matches them.
32+func 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+
49+func 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+
29 func TestHTTPBackendEnvironment(t *testing.T) {65 func TestHTTPBackendEnvironment(t *testing.T) {
30 ctx := context.Background()66 ctx := context.Background()
31 g, data := newGit(t)67 g, data := newGit(t)
@@ -45,34 +81,22 @@ func TestHTTPBackendEnvironment(t *testing.T) {
45 assert.Equal(t, "1", e["GIT_HTTP_EXPORT_ALL"])81 assert.Equal(t, "1", e["GIT_HTTP_EXPORT_ALL"])
46 assert.NotContains(t, e, "REMOTE_USER", "a read is not a push")82 assert.NotContains(t, e, "REMOTE_USER", "a read is not a push")
47 83
48- conf := map[string]string{}
49- for i := range 10 {
50- k, ok := e["GIT_CONFIG_KEY_"+string(rune('0'+i))]
51- if !ok {
52- assert.Equal(t, string(rune('0'+i)), e["GIT_CONFIG_COUNT"], "count matches the keys")
53- break
54- }
55- conf[k] = e["GIT_CONFIG_VALUE_"+string(rune('0'+i))]
56- }
57 assert.Equal(t, map[string]string{84 assert.Equal(t, map[string]string{
58 "core.hooksPath": filepath.Join(data, "run", "hooks"),85 "core.hooksPath": filepath.Join(data, "run", "hooks"),
59 "receive.fsckObjects": "true",86 "receive.fsckObjects": "true",
60 "uploadpack.allowFilter": "true",87 "uploadpack.allowFilter": "true",
61- }, conf)88+ }, backendConf(e))
62 89
63 push, err := g.HTTPBackend(ctx, true)90 push, err := g.HTTPBackend(ctx, true)
64 require.NoError(t, err)91 require.NoError(t, err)
65 pe := envMap(push.Env)92 pe := envMap(push.Env)
66 assert.Equal(t, "owner", pe["REMOTE_USER"])93 assert.Equal(t, "owner", pe["REMOTE_USER"])
67- assert.Equal(t, "4", pe["GIT_CONFIG_COUNT"])94+ assert.Equal(t, map[string]string{
68- found := false95+ "core.hooksPath": filepath.Join(data, "run", "hooks"),
69- for i := range 4 {96+ "receive.fsckObjects": "true",
70- if pe["GIT_CONFIG_KEY_"+string(rune('0'+i))] == "http.receivepack" {97+ "uploadpack.allowFilter": "true",
71- assert.Equal(t, "true", pe["GIT_CONFIG_VALUE_"+string(rune('0'+i))])98+ "http.receivepack": "true",
72- found = true99+ }, backendConf(pe), "http.receivepack only for a push; no size limit unless configured")
73- }
74- }
75- assert.True(t, found, "http.receivepack only for a push")
76 100
77 // Nothing from the caller's environment leaks.101 // Nothing from the caller's environment leaks.
78 t.Setenv("GIT_DIR", "/leak")102 t.Setenv("GIT_DIR", "/leak")
M internal/adapters/gitcli/git.go +5 -1
@@ -28,6 +28,9 @@ type Config struct {
2828 DataDir string
2929 // Path is the PATH given to git, which it uses to find its helpers.
3030 Path string
31+ // MaxReceiveBytes is the largest pack a push may send
32+ // (receive.maxInputSize); zero leaves Git unlimited.
33+ MaxReceiveBytes int64
3134 }
3235
3336 // Git is the git CLI adapter for ports.GitRepos.
@@ -35,6 +38,7 @@ type Git struct {
3538 bin string
3639 dataDir string
3740 env []string
41+ maxRecv int64
3842
3943 mu sync.Mutex // guards backend
4044 backend string // git-http-backend, resolved on first use
@@ -42,7 +46,7 @@ type Git struct {
4246
4347 // New builds an adapter. It does not run git: call CheckVersion.
4448 func New(cfg Config) *Git {
45- return &Git{bin: cfg.Bin, dataDir: cfg.DataDir, env: buildEnv(cfg.DataDir, cfg.Path)}
49+ return &Git{bin: cfg.Bin, dataDir: cfg.DataDir, env: buildEnv(cfg.DataDir, cfg.Path), maxRecv: cfg.MaxReceiveBytes}
4650 }
4751
4852 // buildEnv returns the complete environment of every git invocation. Nothing
@@ -28,6 +28,9 @@ type Config struct {
28 DataDir string28 DataDir string
29 // Path is the PATH given to git, which it uses to find its helpers.29 // Path is the PATH given to git, which it uses to find its helpers.
30 Path string30 Path string
31+ // MaxReceiveBytes is the largest pack a push may send
32+ // (receive.maxInputSize); zero leaves Git unlimited.
33+ MaxReceiveBytes int64
31 }34 }
32 35
33 // Git is the git CLI adapter for ports.GitRepos.36 // Git is the git CLI adapter for ports.GitRepos.
@@ -35,6 +38,7 @@ type Git struct {
35 bin string38 bin string
36 dataDir string39 dataDir string
37 env []string40 env []string
41+ maxRecv int64
38 42
39 mu sync.Mutex // guards backend43 mu sync.Mutex // guards backend
40 backend string // git-http-backend, resolved on first use44 backend string // git-http-backend, resolved on first use
@@ -42,7 +46,7 @@ type Git struct {
42 46
43 // New builds an adapter. It does not run git: call CheckVersion.47 // New builds an adapter. It does not run git: call CheckVersion.
44 func New(cfg Config) *Git {48 func New(cfg Config) *Git {
45- return &Git{bin: cfg.Bin, dataDir: cfg.DataDir, env: buildEnv(cfg.DataDir, cfg.Path)}49+ return &Git{bin: cfg.Bin, dataDir: cfg.DataDir, env: buildEnv(cfg.DataDir, cfg.Path), maxRecv: cfg.MaxReceiveBytes}
46 }50 }
47 51
48 // buildEnv returns the complete environment of every git invocation. Nothing52 // buildEnv returns the complete environment of every git invocation. Nothing

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