vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Bound and stream the anonymous read path
4581690d1ae9d87c272428d101a0fbe3538d7f44 · Keenan Nemetz · 13d ago · parent e05f1c48
Bound and stream the anonymous read path Every anonymous read path read a whole git object before deciding what to do with it: the code browser truncated after allocating, the overview rendered the README it had just read in full, and the site pool allocated the entire object for every request. `max_blob_bytes` allows a 100 MiB blob, so one pushed file made every concurrent request for it a 100 MiB allocation, with no account needed to ask for it. Read the header first instead. `core/git` gains `BlobSize`, `BlobSizes` (one `cat-file --batch-check` for a whole tree) and `BlobStream`, which parses the batch header before touching the payload and stops the process when a caller wants only a prefix. Then use it: `/raw` streams with the object's own `Content-Length`, the inline viewer reads a one-MiB prefix and reports the file's real size, the README is capped and named instead of rendered when it is too large, site blobs are capped at 25 MiB with a 413, and the commit and tree objects read to resolve a site path are capped separately. Metadata gets the same treatment on both sides: 4 MiB per file, 2 MiB per body, 20,000 files and 64 MiB for `issues/` and `requests/` together. The loader asks git for every size in one call and refuses the tree before reading it; the push boundary refuses the same tree, so the author learns why instead of finding that their issue list stopped loading. Counting is now per subtree, so the machine written `runs/` tree no longer eats the issue file budget.
metadata/request_write.go +6
| @@ -44,6 +44,9 @@ func validateRequestForWrite(r *Request) error { | |||
| 44 | 44 | if r.Author == "" { | |
| 45 | 45 | return fmt.Errorf("requests: author is required") | |
| 46 | 46 | } | |
| 47 | if problems := bodyProblems("body", r.Description); len(problems) > 0 { | ||
| 48 | return fmt.Errorf("requests: %s", problems[0]) | ||
| 49 | } | ||
| 47 | 50 | if !assigneeRE.MatchString(r.Author) { | |
| 48 | 51 | return fmt.Errorf("requests: author %q is not an account name", r.Author) | |
| 49 | 52 | } | |
| @@ -553,6 +556,9 @@ func validateComment(c *Comment) error { | |||
| 553 | 556 | if strings.TrimSpace(c.Body) == "" { | |
| 554 | 557 | return fmt.Errorf("requests: a comment needs a body") | |
| 555 | 558 | } | |
| 559 | if problems := bodyProblems("comment", c.Body); len(problems) > 0 { | ||
| 560 | return fmt.Errorf("requests: %s", problems[0]) | ||
| 561 | } | ||
| 556 | 562 | if strings.TrimSpace(c.Author) == "" { | |
| 557 | 563 | return fmt.Errorf("requests: a comment needs an author") | |
| 558 | 564 | } | |