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/issues.go +15−17
| @@ -144,25 +144,10 @@ func LoadIssues(g *git.Repo, ref string) ([]*Issue, error) { | |||
| 144 | 144 | if commit == "" { | |
| 145 | 145 | return nil, nil | |
| 146 | 146 | } | |
| 147 | blobs, err := g.LsTreeR(commit) | ||
| 147 | contents, err := readMetadata(g, commit, IssuesDir) | ||
| 148 | 148 | if err != nil { | |
| 149 | 149 | return nil, err | |
| 150 | 150 | } | |
| 151 | if len(blobs) > maxIssuesScan { | ||
| 152 | return nil, fmt.Errorf("issues: refusing to scan %d objects", len(blobs)) | ||
| 153 | } | ||
| 154 | |||
| 155 | contents := map[string]string{} | ||
| 156 | for _, e := range blobs { | ||
| 157 | if !strings.HasPrefix(e.Name, IssuesDir+"/") { | ||
| 158 | continue // unrelated files on the issues branch are ignored | ||
| 159 | } | ||
| 160 | data, err := g.BlobBySHA(e.SHA) | ||
| 161 | if err != nil { | ||
| 162 | return nil, err | ||
| 163 | } | ||
| 164 | contents[e.Name] = string(data) | ||
| 165 | } | ||
| 166 | 151 | return ParseIssues(contents) | |
| 167 | 152 | } | |
| 168 | 153 | ||
| @@ -288,6 +273,7 @@ func applyIssue(iss *Issue, raw, path string, problems *[]string) { | |||
| 288 | 273 | iss.Assignees = append(iss.Assignees, a) | |
| 289 | 274 | } | |
| 290 | 275 | iss.Description = body | |
| 276 | *problems = append(*problems, bodyProblems(path, body)...) | ||
| 291 | 277 | sort.Strings(iss.Labels) | |
| 292 | 278 | sort.Strings(iss.Assignees) | |
| 293 | 279 | } | |
| @@ -332,6 +318,9 @@ func ParseComment(id, raw, path string) (Comment, string) { | |||
| 332 | 318 | } | |
| 333 | 319 | c.Deleted = m.Deleted | |
| 334 | 320 | c.Body = strings.TrimLeft(body, "\n") | |
| 321 | if problems := bodyProblems(path, c.Body); len(problems) > 0 { | ||
| 322 | return c, problems[0] | ||
| 323 | } | ||
| 335 | 324 | if c.Deleted && strings.TrimSpace(c.Body) != "" { | |
| 336 | 325 | return c, fmt.Sprintf("%s: a deleted comment keeps no body", path) | |
| 337 | 326 | } | |
| @@ -446,8 +435,17 @@ func ValidateIssues(g *git.Repo, commit string) []string { | |||
| 446 | 435 | if err != nil { | |
| 447 | 436 | return []string{fmt.Sprintf("cannot read the metadata tree: %v", err)} | |
| 448 | 437 | } | |
| 449 | contents := map[string]string{} | ||
| 438 | // Refuse an oversized tree before reading any of it: a rejected push should | ||
| 439 | // cost the receiver nothing either. | ||
| 450 | 440 | var problems []string | |
| 441 | for _, prefix := range []string{IssuesDir, RequestsDir} { | ||
| 442 | paths, shas := metadataPaths(blobs, prefix) | ||
| 443 | problems = append(problems, metadataSizeProblems(paths, shas, g)...) | ||
| 444 | } | ||
| 445 | if len(problems) > 0 { | ||
| 446 | return problems | ||
| 447 | } | ||
| 448 | contents := map[string]string{} | ||
| 451 | 449 | for _, e := range blobs { | |
| 452 | 450 | switch { | |
| 453 | 451 | case strings.HasPrefix(e.Name, IssuesDir+"/"): | |