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/issue_write.go +7

@@ -41,6 +41,9 @@ func NewIssueComment(body, author string, now time.Time) (*Comment, error) {
41 41 if err != nil {
42 42 return nil, err
43 43 }
44 if problems := bodyProblems("comment", body); len(problems) > 0 {
45 return nil, fmt.Errorf("issues: %s", problems[0])
46 }
44 47 return &Comment{ID: id, Author: strings.TrimSpace(author), Created: now.UTC(), Body: body}, nil
45 48 }
46 49
@@ -188,6 +191,9 @@ func EditIssueComment(g *git.Repo, issueID, commentID, body, actor string) (*Com
188 191 if strings.TrimSpace(body) == "" {
189 192 return nil, fmt.Errorf("issues: a comment needs a body")
190 193 }
194 if problems := bodyProblems("comment", body); len(problems) > 0 {
195 return nil, fmt.Errorf("issues: %s", problems[0])
196 }
191 197 c.Body = body
192 198 raw, err := RenderComment(c)
193 199 if err != nil {
@@ -309,6 +315,7 @@ func validateIssueForWrite(iss *Issue) error {
309 315 if len(iss.Title) > maxIssueTitleLen {
310 316 problems = append(problems, fmt.Sprintf("title is %d bytes, limit %d", len(iss.Title), maxIssueTitleLen))
311 317 }
318 problems = append(problems, bodyProblems("body", iss.Description)...)
312 319 if iss.State != "open" && iss.State != "closed" {
313 320 problems = append(problems, fmt.Sprintf("state must be open or closed, got %q", iss.State))
314 321 }