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/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 }