Closes #13.
What was wrong
Nothing bounded a repository except the per-push receive_max_bytes and the
per-object caps. A contributor could add refs in their own namespace without
limit, any writer could add issues and comments, and a maintainer could add tags;
every listing, comparison and page load grew with the count. A single account
could make an anonymous reader wait and could grow the disk until an admin cleaned
up by hand.
What this does
Repository quotas in pre-receive (server/hook/quotas.go). They describe the
repository the push would leave, not the one it found, and a refusal moves no refs:
| setting | default | what it counts |
|---|---|---|
repo_max_refs |
500 | branches and tags, with the refs the push creates or deletes |
repo_max_metadata_entries |
2000 | documents on the metadata branch: issues, requests, comments, approvals, runs, releases |
repo_max_metadata_bytes |
32 MiB | their total size |
repo_max_bytes |
5 GiB | git objects on disk |
repo_max_lfs_bytes |
10 GiB | LFS objects, logs and artifacts, enforced on upload with 413 |
The values arrive the same way the blob limit already does: instance config →
SSH server environment → hook, so there is one setting, not a second policy.
Byte growth is bounded by the cap plus one permitted push: a repository that is
already over repo_max_bytes refuses further pushes, and each push is bounded by
receive_max_bytes.
Account write budget (account_writes_per_hour, 300). Keyed by account, not
address, because the account is what a write is attributed to. Applies in
beginWrite, so issues, comments, requests, labels and state changes share one
budget; the refusal is a form-readable 429 with Retry-After.
Read caps. for-each-ref listings (branches in the repo shell, compare,
settings, releases, CI) and tags are capped at 500; the quota check uses
RefCount, which sees every ref. Activity feeds were already capped at 100
events and 200 state changes — verified, not changed.
Open registration baseline. Defaults are unchanged (false, role none). The
role is now restricted to none, read or contribute: a self-registered
account must not be able to move shared branches or approve reviews. Registration
was already rate-limited per address by #55.
Notes
repo_max_lfs_byteswalks the repository’s object links once per upload. That is deliberate: it is the only place the bytes arrive, and an upload is a large object by definition. If it becomes hot, a cached per-repository counter is the next step, not removing the check.- The metadata count includes
runs/. A repository at its metadata cap will refuse a runner’s record push too; the message says which quota, and an operator can raise it or prune records. That is the intended behaviour for a hard cap. - Quotas are off when the values are zero (tests, offline commands); the instance always sets them from config.
Verification
gofmt,go vet ./...,go test -race ./...clean../vole workflow checkand./vole workflow run verifypass.
Tests
TestRefQuotaRefusesAPushThatWouldExceedIt: a second branch is refused at a limit of one, and moving an existing branch is not counted as a new ref.TestMetadataQuotaCountsEntriesAndBytes: a two-issue tree is refused at a limit of one entry and at a small byte limit, and passes at a generous one.TestRepoSizeQuotaRefusesAFullRepository,TestLFSQuotaRefusesAFullStore.TestTransferRefusesAnUploadOverTheRepoQuota: a 413 on the wire, object not stored.TestAccountWritesAreBudgeted: per-account budget, 429 +Retry-After, and an independent budget for another account.TestValidationRefusesUnsafeCombinations:open_registration_role = maintainrefused alongside the existingadmincase.
Docs: docs/configuration.md (six settings and the role restriction),
docs/operations.md (“Quotas”: what each bounds and what to do when one bites),
docs/accounts-and-access.md (registration role).