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/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+"/"):