Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · forge/config/config.go

Line attribution on main · 399 line(s)

653e0cfc
Keenan Nemetz
18d ago
1 // Package config loads Vole's on-disk TOML configuration. One box, one file.
2 package config
3
4 import (
5 "fmt"
6 "os"
7 "path/filepath"
a63475ce
Keenan Nemetz
17d ago
8 "strings"
b44cbfaf
Keenan Nemetz
18d ago
9 "time"
653e0cfc
Keenan Nemetz
18d ago
10
11 "github.com/BurntSushi/toml"
12 )
13
14 type Config struct {
a5f19b3c
Keenan Nemetz
18d ago
15 DataDir string `toml:"data_dir"`
642e5994
Keenan Nemetz
14d ago
16 // HTTPAddr serves the web UI and anonymous git clone. When HTTPSAddr is
17 // set it instead answers ACME HTTP-01 challenges and redirects to HTTPS.
a5f19b3c
Keenan Nemetz
18d ago
18 HTTPAddr string `toml:"http_addr"`
642e5994
Keenan Nemetz
14d ago
19 // HTTPSAddr enables automatic HTTPS with ACME. Empty leaves TLS to a proxy.
20 HTTPSAddr string `toml:"https_addr"`
21 // ACMEEmail is the contact address given to the certificate authority.
22 ACMEEmail string `toml:"acme_email"`
23 // ACMEDomains are the host names Vole may obtain certificates for. When
24 // empty, the host in PublicURL is used. Each host gets its own certificate.
25 ACMEDomains []string `toml:"acme_domains"`
26 // ACMECacheDir stores the ACME account key and issued certificates. Empty
27 // means <data_dir>/acme.
28 ACMECacheDir string `toml:"acme_cache_dir"`
29 // ACMEDirectoryURL overrides the certificate authority, for a private ACME
30 // server or the Let's Encrypt staging directory while testing.
31 ACMEDirectoryURL string `toml:"acme_directory_url"`
c5902451
Keenan Nemetz
17d ago
32 // SSHAddr is the only write githttp.
a5f19b3c
Keenan Nemetz
18d ago
33 SSHAddr string `toml:"ssh_addr"`
78cd3437
Keenan Nemetz
13d ago
34 // SSHMaxConnections bounds simultaneous connections on the SSH listener. Past
35 // it a connection is closed before the handshake rather than queued, because a
36 // queue is the unbounded thing the cap exists to avoid. Zero means 64: enough
37 // for a team, small enough that a flood cannot take every file descriptor.
38 SSHMaxConnections int `toml:"ssh_max_connections"`
39 // SSHMaxGitProcesses bounds git processes across every SSH connection, so a
40 // burst of sessions cannot turn into a burst of processes. Zero means 16, the
41 // same order as git_http_max_concurrent.
42 SSHMaxGitProcesses int `toml:"ssh_max_git_processes"`
43 // SSHHandshakeTimeoutSeconds bounds how long a connection may take to finish the
44 // SSH handshake, so a client that opens a socket and says nothing cannot hold a
45 // connection slot indefinitely. Zero means 20.
46 SSHHandshakeTimeoutSeconds int `toml:"ssh_handshake_timeout_seconds"`
a5f19b3c
Keenan Nemetz
18d ago
47 // MaxBlobBytes rejects individual objects in pre-receive.
48 MaxBlobBytes int64 `toml:"max_blob_bytes"`
86991e70
Keenan Nemetz
13d ago
49 // GitHTTP enables the anonymous smart-HTTP read transport: clone, fetch and
50 // the LFS downloads that share its URLs. It is on by default because reading a
51 // public repository without an account is the point of a public repository.
52 // An instance whose repositories are all internal can switch it off and get a
53 // smaller anonymous surface; pushes are on SSH either way.
54 GitHTTP bool `toml:"git_http"`
55 // GitHTTPMaxConcurrent bounds simultaneous reads on that transport. Past it a
56 // clone is refused with 503 and Retry-After rather than queued, because a
57 // queue is another unbounded thing to hold.
58 GitHTTPMaxConcurrent int `toml:"git_http_max_concurrent"`
59 // GitHTTPMaxPerClient bounds simultaneous reads from one address. Zero
60 // disables the per-client rule and leaves only the concurrency bound.
61 GitHTTPMaxPerClient int `toml:"git_http_max_per_client"`
62 // GitHTTPClientIPHeader names the request header a trusted proxy sets with the
63 // client address, e.g. "X-Forwarded-For". Empty means the connection address is
64 // the client, which is right when nothing is in front of Vole. Behind a proxy
65 // without this, every visitor shares one identity and one per-client budget.
66 GitHTTPClientIPHeader string `toml:"git_http_client_ip_header"`
67 // GitHTTPTimeoutSeconds bounds one read, including the git process behind it.
68 // A big clone over a slow link is minutes; a stalled one is hours.
69 GitHTTPTimeoutSeconds int `toml:"git_http_timeout_seconds"`
a5f19b3c
Keenan Nemetz
18d ago
70 // ReceiveMaxBytes caps a whole push, enforced by git's own
71 // receive.maxInputSize in each repo rather than by buffering in the proxy.
72 ReceiveMaxBytes int64 `toml:"receive_max_bytes"`
9e8cc155
Keenan Nemetz
13d ago
73 // RepoMaxRefs bounds the branches and tags in one repository. A contributor
74 // can create refs in their own namespace and a maintainer can create tags, so
75 // without a bound a repository's ref list is a number somebody else chooses —
76 // and every listing, comparison and activity read walks it.
77 RepoMaxRefs int `toml:"repo_max_refs"`
78 // RepoMaxMetadataEntries bounds the files on the metadata branch: issues,
79 // requests, comments and approvals. Every page load parses them, so this is the
80 // bound on how slow one repository can make its own pages.
81 RepoMaxMetadataEntries int `toml:"repo_max_metadata_entries"`
82 // RepoMaxMetadataBytes bounds the total size of those files.
83 RepoMaxMetadataBytes int64 `toml:"repo_max_metadata_bytes"`
84 // RepoMaxBytes bounds a repository's on-disk objects. A push that would leave
85 // the repository over this is refused, so the growth of a repository is bounded
86 // by the cap plus one permitted push.
87 RepoMaxBytes int64 `toml:"repo_max_bytes"`
88 // RepoMaxLFSBytes bounds one repository's LFS objects, CI logs and run
89 // artifacts. It is enforced where the bytes arrive, on the upload.
90 RepoMaxLFSBytes int64 `toml:"repo_max_lfs_bytes"`
e5542ac6
Keenan Nemetz
13d ago
91 // ContributorWritesPerHour bounds how many repository writes an untrusted
92 // account may make from the browser in an hour: issues, comments, requests,
93 // labels, state changes. It is deliberately not a per-account budget for
94 // everybody — maintainers and admins are trusted roles that can already change
95 // these rules, push shared branches and run the instance, so budgeting them
96 // would slow their work without bounding anybody who could not do worse.
97 ContributorWritesPerHour int `toml:"contributor_writes_per_hour"`
64ac5071
Keenan Nemetz
18d ago
98 // LFSObjectMaxBytes caps a single LFS object.
99 LFSObjectMaxBytes int64 `toml:"lfs_object_max_bytes"`
100 // LFSWarnBytes makes a push that commits a binary larger than this carry a
101 // suggestion to track it with git-lfs. Zero disables the warning.
102 LFSWarnBytes int64 `toml:"lfs_warn_bytes"`
b44cbfaf
Keenan Nemetz
18d ago
103 // LeaseSeconds is how long a runner may hold a job without a heartbeat before
104 // the job is handed to somebody else.
105 LeaseSeconds int `toml:"lease_seconds"`
8d336083
Keenan Nemetz
17d ago
106 // MaxJobsPerRepo bounds how many jobs one repository may have running at once.
107 // Zero means no bound: fairness is then ordering alone (the lease serves the
108 // least busy repository first), which stops a burst taking the whole fleet but
109 // does not limit one repository's footprint. A cap is what an operator wants
110 // when they need the bound itself — one repository's build saturating every
111 // runner on the instance — and it applies per repository, not per account.
112 MaxJobsPerRepo int `toml:"max_jobs_per_repo"`
d3296cda
Keenan Nemetz
18d ago
113 // PagesDomain is the suffix a repository's site is served under:
114 // <repo>.<pages_domain>. Empty disables site hosting entirely.
115 PagesDomain string `toml:"pages_domain"`
116 // PagesRef is the branch a site is read from. It is a normal branch, pushed
117 // and reviewed like any other, so there is no export step to drift.
118 PagesRef string `toml:"pages_ref"`
119 // PagesProcs is how many `git cat-file --batch` processes serve blobs.
520470e1
Keenan Nemetz
16d ago
120 // Reads parallelise across processes: measured at 12.1k req/s with one and
121 // 24.8k with four.
d3296cda
Keenan Nemetz
18d ago
122 PagesProcs int `toml:"pages_procs"`
123 // PagesCacheBytes caps the in-memory blob cache. The cache is required, not an
124 // optimisation: cold git reads are ~1.2 ms p50, warm ones 0.075 ms.
125 PagesCacheBytes int64 `toml:"pages_cache_bytes"`
f920a9f1
Keenan Nemetz
16d ago
126 // OpenRegistration lets anyone create an account by registering a passkey.
127 // Off by default: an instance with no public contribution policy should not
128 // grow accounts just because it is reachable.
129 OpenRegistration bool `toml:"open_registration"`
5b814e99
Keenan Nemetz
14d ago
130 // OpenRegistrationRole is the instance-wide role a self-registered account
131 // gets. Empty or "none" preserves the old behaviour: the account has no
132 // instance grant and only repository defaults apply. The role applies to
133 // public repositories; "admin" is refused for self-registration.
134 OpenRegistrationRole string `toml:"open_registration_role"`
6ce635a8
Keenan Nemetz
1d ago
135 // InstanceName is the heading on the landing page. Empty uses "Vole".
074ae160
Keenan Nemetz
14d ago
136 InstanceName string `toml:"instance_name"`
6ce635a8
Keenan Nemetz
1d ago
137 // InstanceRepo names the repository whose tree customizes this instance:
138 // profile/README.md, profile/custom.css, profile/favicon.svg and assets/.
139 // Empty disables the customization entirely.
140 InstanceRepo string `toml:"instance_repo"`
6c84d387
Keenan Nemetz
16d ago
141 // PublicURL is where this instance is reachable from a browser:
142 // "https://forge.example.com" behind a proxy, normally. It is optional for a
143 // read-only instance; browser login is enabled only when it is an https URL,
144 // because WebAuthn is bound to that origin. It is also what the CLI uses to
145 // print a link to something it just created.
a63475ce
Keenan Nemetz
17d ago
146 PublicURL string `toml:"public_url"`
f09ef39e
Keenan Nemetz
15d ago
147 // DevLogin is a development-only escape hatch: when set to an account name,
ed181212
Keenan Nemetz
13d ago
148 // a browser request from loopback is signed in as that account. It requires
149 // dev_only, a loopback http_addr, and no public_url or https_addr, and the
150 // instance logs a warning at startup. Never enable it on a shared instance.
f09ef39e
Keenan Nemetz
15d ago
151 DevLogin string `toml:"dev_login"`
ed181212
Keenan Nemetz
13d ago
152 // DevOnly acknowledges that this instance is a development instance. It is what
153 // makes dev_login an explicit decision rather than a setting somebody copied
154 // from an example: without it, dev_login is refused at startup.
155 DevOnly bool `toml:"dev_only"`
0c40a1d3
Keenan Nemetz
7d ago
156 // SecretsKey names the file holding the key that seals repository secrets.
157 // Empty means <data_dir>/secrets.key. It is a separate setting because the key
158 // can then be kept somewhere the data directory's backups do not reach, which
159 // is the difference between a backup of secrets and a backup of ciphertext.
160 SecretsKey string `toml:"secrets_key"`
653e0cfc
Keenan Nemetz
18d ago
161
4506a4d3
Keenan Nemetz
14d ago
162 // Path is the file Load was asked to read. Loaded reports whether it found a
163 // non-empty file there; false means the built-in defaults are in use, which
164 // the status page shows to an instance admin.
165 Path string `toml:"-"`
166 Loaded bool `toml:"-"`
653e0cfc
Keenan Nemetz
18d ago
167 }
168
169 func defaults() *Config {
170 return &Config{
78cd3437
Keenan Nemetz
13d ago
171 DataDir: "./data",
172 HTTPAddr: "127.0.0.1:8080",
173 SSHAddr: "127.0.0.1:2222",
174 SSHMaxConnections: 64,
175 SSHMaxGitProcesses: 16,
176 SSHHandshakeTimeoutSeconds: 20,
177 MaxBlobBytes: 100 << 20, // 100 MiB per blob
178 ReceiveMaxBytes: 1 << 30, // 1 GiB per push
64ac5071
Keenan Nemetz
18d ago
179
86991e70
Keenan Nemetz
13d ago
180 GitHTTP: true,
181 GitHTTPMaxConcurrent: 16,
182 GitHTTPMaxPerClient: 8,
183 GitHTTPTimeoutSeconds: 600,
184
64ac5071
Keenan Nemetz
18d ago
185 LFSObjectMaxBytes: 2 << 30, // 2 GiB per LFS object
186 LFSWarnBytes: 5 << 20, // flag binaries over 5 MiB that are not in LFS
d3296cda
Keenan Nemetz
18d ago
187
5b814e99
Keenan Nemetz
14d ago
188 PagesRef: "pages",
189 PagesProcs: 4,
190 PagesCacheBytes: 32 << 20, // 32 MiB holds several 10k-file sites
191 OpenRegistrationRole: "none",
6ce635a8
Keenan Nemetz
1d ago
192 InstanceRepo: "vole-instance",
653e0cfc
Keenan Nemetz
18d ago
193 }
194 }
195
196 // Load reads path, filling in defaults for anything absent. A missing file is
a5f19b3c
Keenan Nemetz
18d ago
197 // not an error: `vole serve` works out of the box.
653e0cfc
Keenan Nemetz
18d ago
198 func Load(path string) (*Config, error) {
199 c := defaults()
200 c.Path = path
201 if path != "" {
a5f19b3c
Keenan Nemetz
18d ago
202 if st, err := os.Stat(path); err == nil && st.Size() > 0 {
653e0cfc
Keenan Nemetz
18d ago
203 if _, err := toml.DecodeFile(path, c); err != nil {
204 return nil, fmt.Errorf("config %s: %w", path, err)
205 }
4506a4d3
Keenan Nemetz
14d ago
206 c.Loaded = true
a5f19b3c
Keenan Nemetz
18d ago
207 } else if err != nil && !os.IsNotExist(err) {
653e0cfc
Keenan Nemetz
18d ago
208 return nil, err
209 }
210 }
211 return c, nil
212 }
213
c36d5073
Keenan Nemetz
16d ago
214 // DefaultPath returns the config file to use when the operator did not name
215 // one. VOLE_CONFIG wins, then a vole.toml in the current directory, then the
216 // per-user instance at ~/.vole/vole.toml. If none exists it returns "vole.toml",
217 // which Load treats as the built-in defaults.
218 func DefaultPath() string {
219 if p := strings.TrimSpace(os.Getenv("VOLE_CONFIG")); p != "" {
220 return p
221 }
222 if fileExists("vole.toml") {
223 return "vole.toml"
224 }
225 if home, err := os.UserHomeDir(); err == nil {
226 p := filepath.Join(home, ".vole", "vole.toml")
227 if fileExists(p) {
228 return p
229 }
230 }
231 return "vole.toml"
232 }
233
234 // ResolvePath applies DefaultPath when path is empty, so a command that did not
235 // receive --config still finds the instance the operator initialised.
236 func ResolvePath(path string) string {
237 if path != "" {
238 return path
239 }
240 return DefaultPath()
241 }
242
243 func fileExists(path string) bool {
244 st, err := os.Stat(path)
245 return err == nil && !st.IsDir()
246 }
247
197ecaa2
Keenan Nemetz
18d ago
248 func (c *Config) ReposDir() string { return filepath.Join(c.DataDir, "repos") }
249
427c4761
Keenan Nemetz
17d ago
250 // UsersDB is the authoritative user roster: accounts, the names they have ever
251 // held, and their SSH keys. It is not derived and not rebuildable.
252 func (c *Config) UsersDB() string { return filepath.Join(c.DataDir, "users.db") }
197ecaa2
Keenan Nemetz
18d ago
253
427c4761
Keenan Nemetz
17d ago
254 // AuditLog records privileged operations, including every roster change.
b44cbfaf
Keenan Nemetz
18d ago
255 func (c *Config) AuditLog() string { return filepath.Join(c.DataDir, "audit.log") }
256 func (c *Config) IndexDB() string { return filepath.Join(c.DataDir, "index.db") }
257
0c40a1d3
Keenan Nemetz
7d ago
258 // SecretsDB holds each repository's sealed secrets. It is a separate file from
259 // the roster because it is the one database whose contents are encrypted, and a
260 // copy of it without SecretsKeyPath is ciphertext.
261 func (c *Config) SecretsDB() string { return filepath.Join(c.DataDir, "secrets.db") }
262
263 // SecretsKeyPath is the AES-256 key that seals those values. It is created on
264 // first use at 0600; losing it means losing every stored value, and restoring a
265 // copy of the database beside a newly generated key is a restore that cannot read
266 // what it restored.
267 func (c *Config) SecretsKeyPath() string {
268 if path := strings.TrimSpace(c.SecretsKey); path != "" {
269 return path
270 }
271 return filepath.Join(c.DataDir, "secrets.key")
272 }
273
d798eaac
Keenan Nemetz
17d ago
274 // RunsDB holds the CI queue and finished runs. It is authoritative for work that has
275 // not happened yet — a queued or leased job exists only there — while the finished
276 // half is derived from the records on each repository's metadata branch, and pending
277 // work is reconciled from the refs. Deleting it loses the attempts that were in
3abbe749
Keenan Nemetz
16d ago
278 // flight; the current tip of every ref is queued once and `vole admin index rebuild`
d798eaac
Keenan Nemetz
17d ago
279 // restores the finished history.
b44cbfaf
Keenan Nemetz
18d ago
280 func (c *Config) RunsDB() string { return filepath.Join(c.DataDir, "runs.db") }
281
282 // SpoolDir receives accepted push events from post-receive hooks.
283 func (c *Config) SpoolDir() string { return filepath.Join(c.DataDir, "queue", "spool") }
284
64ac5071
Keenan Nemetz
18d ago
285 // LFSDir is the instance-wide content-addressed store. Objects are shared by
286 // every repository: the same artifact pushed twice costs one copy.
287 func (c *Config) LFSDir() string { return filepath.Join(c.DataDir, "lfs") }
288
5ece70a5
Keenan Nemetz
11d ago
289 // LogsDir holds one file per CI attempt. Logs are not content-addressed and are
290 // not shared, so they live outside the LFS pool.
291 func (c *Config) LogsDir() string { return filepath.Join(c.DataDir, "logs") }
292
b44cbfaf
Keenan Nemetz
18d ago
293 // Lease returns how long a runner may hold a job without a heartbeat.
294 func (c *Config) Lease() time.Duration {
295 if c.LeaseSeconds <= 0 {
296 return 60 * time.Second
297 }
298 return time.Duration(c.LeaseSeconds) * time.Second
299 }
5d6db024
Keenan Nemetz
17d ago
300 func (c *Config) SSHHostKey() string { return filepath.Join(c.DataDir, "ssh_host_ed25519_key") }
653e0cfc
Keenan Nemetz
18d ago
301
642e5994
Keenan Nemetz
14d ago
302 // ACMECachePath is where automatic-HTTPS state lives. The ACME account key is a
303 // secret, so it belongs beside the SSH host key rather than in a world-readable
304 // system directory.
305 func (c *Config) ACMECachePath() string {
306 if dir := strings.TrimSpace(c.ACMECacheDir); dir != "" {
307 return dir
308 }
309 return filepath.Join(c.DataDir, "acme")
310 }
311
a63475ce
Keenan Nemetz
17d ago
312 // BaseURL is PublicURL without a trailing slash, so a caller can append
ed03b3d1
Keenan Nemetz
8h ago
313 // "/<repo>/issues/<id>" without thinking about it.
a63475ce
Keenan Nemetz
17d ago
314 func (c *Config) BaseURL() string { return strings.TrimRight(c.PublicURL, "/") }
315
3abbe749
Keenan Nemetz
16d ago
316 // Sample renders a starter config for `vole admin init`.
653e0cfc
Keenan Nemetz
18d ago
317 func Sample() string {
318 return `# Vole configuration
319 data_dir = "./data"
320
642e5994
Keenan Nemetz
14d ago
321 # Read-only: the web UI and anonymous git clone. Leave HTTPS off and terminate
322 # TLS in a proxy in front of this address, keeping the Host header intact because
323 # site requests are routed by hostname.
a5f19b3c
Keenan Nemetz
18d ago
324 http_addr = "127.0.0.1:8080"
653e0cfc
Keenan Nemetz
18d ago
325
642e5994
Keenan Nemetz
14d ago
326 # Or let Vole obtain and renew its own certificates with ACME. Setting https_addr
327 # enables it; http_addr then answers ACME challenges and redirects to HTTPS, so
328 # it must be reachable on port 80. public_url below (or acme_domains) must name
329 # a host that resolves to this server.
330 # https_addr = ":443"
331 # http_addr = ":80"
332 # acme_email = "admin@example.com"
333 # acme_domains = ["forge.example.com"]
334 # acme_cache_dir = "./data/acme"
335 # Use the staging directory while testing to avoid a production rate limit.
336 # acme_directory_url = "https://acme-staging-v02.api.letsencrypt.org/directory"
337
c5902451
Keenan Nemetz
17d ago
338 # The only write githttp. Identity is the SSH key presented at handshake.
a5f19b3c
Keenan Nemetz
18d ago
339 ssh_addr = "127.0.0.1:2222"
653e0cfc
Keenan Nemetz
18d ago
340
6c84d387
Keenan Nemetz
16d ago
341 # Where a browser reads this instance. Set it to the canonical https URL to
342 # enable passkey login; WebAuthn is bound to this origin. The CLI also uses it to
343 # print the page for something it just created. Trailing slash is ignored.
a63475ce
Keenan Nemetz
17d ago
344 # public_url = "https://forge.example.com"
345
6ce635a8
Keenan Nemetz
1d ago
346 # What the home page calls this forge. Empty uses "Vole".
074ae160
Keenan Nemetz
14d ago
347 # instance_name = "Acme Forge"
6ce635a8
Keenan Nemetz
1d ago
348
349 # The repository whose tree customizes this instance: profile/README.md,
350 # profile/custom.css, profile/favicon.svg, and assets/. Empty disables it.
351 # instance_repo = "vole-instance"
074ae160
Keenan Nemetz
14d ago
352
f09ef39e
Keenan Nemetz
15d ago
353 # Development only: sign loopback browser requests in as this account. Refused
354 # unless http_addr is a loopback address. Never enable this on a shared instance.
ed181212
Keenan Nemetz
13d ago
355 # dev_only = true
f09ef39e
Keenan Nemetz
15d ago
356 # dev_login = "you"
357
f920a9f1
Keenan Nemetz
16d ago
358 # Let anyone create an account by registering a passkey. New accounts have no
359 # repository-specific access; each repository's default role still applies.
360 # Off unless you are running an instance that wants public contributions.
361 # open_registration = true
362
5b814e99
Keenan Nemetz
14d ago
363 # The instance-wide role a self-registered account gets. It applies to public
364 # repositories only; an instance admin still has admin access to every
365 # repository. Use "contribute" for an open-source instance. "admin" is refused.
366 # open_registration_role = "contribute"
367
a5f19b3c
Keenan Nemetz
18d ago
368 # Reject individual blobs above this size (pre-receive). Point people at
369 # git-lfs instead.
653e0cfc
Keenan Nemetz
18d ago
370 max_blob_bytes = 104857600
371
64ac5071
Keenan Nemetz
18d ago
372 # LFS objects live in a content-addressed store shared by every repository.
373 # This caps one object; anything larger is refused rather than spooled.
374 lfs_object_max_bytes = 2147483648
375
376 # A push that commits a binary bigger than this, and is not already an LFS
377 # pointer, gets a warning naming the git lfs track command. Zero disables it.
378 lfs_warn_bytes = 5242880
379
8d336083
Keenan Nemetz
17d ago
380 # How many jobs one repository may have running at once. Zero disables the bound,
381 # which leaves fairness as ordering alone: the lease serves the least busy
382 # repository first, so a burst cannot take the whole fleet, but nothing limits how
383 # much of it one repository uses. Set it when one repository's build saturates the
384 # instance and the ordering is not enough.
385 # max_jobs_per_repo = 4
386
d3296cda
Keenan Nemetz
18d ago
387 # Static sites: a repository's pages branch is served at
388 # <repo>.<pages_domain>. Leave empty to serve no sites at all.
389 # pages_domain = "pages.example.com"
390 # pages_ref = "pages"
391 # pages_procs = 4
392 # pages_cache_bytes = 33554432
393
a5f19b3c
Keenan Nemetz
18d ago
394 # Cap a whole push. Enforced by git's receive.maxInputSize in each repo, because
395 # receive-pack indexes a pack in memory and a large push costs its own size in
396 # RSS.
397 receive_max_bytes = 1073741824
653e0cfc
Keenan Nemetz
18d ago
398 `
399 }