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.

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/requests.go +10−19

@@ -134,21 +134,10 @@ func LoadRequests(g *git.Repo, ref string) ([]*Request, error) {
134 134 if commit == "" {
135 135 return nil, nil
136 136 }
137 blobs, err := g.LsTreeR(commit)
137 contents, err := readMetadata(g, commit, RequestsDir)
138 138 if err != nil {
139 139 return nil, err
140 140 }
141 contents := map[string]string{}
142 for _, e := range blobs {
143 if !strings.HasPrefix(e.Name, RequestsDir+"/") {
144 continue
145 }
146 data, err := g.BlobBySHA(e.SHA)
147 if err != nil {
148 return nil, err
149 }
150 contents[e.Name] = string(data)
151 }
152 141 return ParseRequests(contents)
153 142 }
154 143
@@ -336,6 +325,7 @@ func applyRequest(req *Request, raw, path string, problems *[]string) {
336 325 "%s: merged_base, merged_head, merged_by and merged_at only belong on a merged request (state is %q)", path, req.State))
337 326 }
338 327 req.Description = body
328 *problems = append(*problems, bodyProblems(path, body)...)
339 329 }
340 330
341 331 // validBranchName is the file-format check for base and head_ref: one line, no
@@ -439,16 +429,17 @@ func ValidateRequests(g *git.Repo, commit string) []string {
439 429 if err != nil {
440 430 return []string{fmt.Sprintf("cannot read the requests tree: %v", err)}
441 431 }
432 paths, shas := metadataPaths(blobs, RequestsDir)
433 if problems := metadataSizeProblems(paths, shas, g); len(problems) > 0 {
434 return problems
435 }
442 436 contents := map[string]string{}
443 for _, e := range blobs {
444 if !strings.HasPrefix(e.Name, RequestsDir+"/") {
445 continue
446 }
447 data, err := g.BlobBySHA(e.SHA)
437 for i, path := range paths {
438 data, err := g.BlobBySHA(shas[i])
448 439 if err != nil {
449 return []string{fmt.Sprintf("cannot read %s: %v", e.Name, err)}
440 return []string{fmt.Sprintf("cannot read %s: %v", path, err)}
450 441 }
451 contents[e.Name] = string(data)
442 contents[path] = string(data)
452 443 }
453 444 if _, err := ParseRequests(contents); err != nil {
454 445 if ve, ok := err.(*ValidationError); ok {