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.
Read the metadata tree in one git call
5cecc6183eadab8147d11438abc64d54c4b0a78f · Keenan Nemetz · 10d ago · parent 5fce878d
Read the metadata tree in one git call The issue and change-request loaders read the metadata tree with one `git cat-file` per file, so a page render cost as many processes as the branch had issues and comments. On a repository with 30 change requests the overview paid 114 git invocations and 98 ms; the requests list paid 72 and 90 ms. Read every listed blob through a single `cat-file --batch`. The push validators share the same helper, so pre-receive stops paying one process per file as well. The metadata branch is the whole source of truth for issues and changes, so this is the read path that grows with use. Nothing about the format or the size limits changes: an oversized file is still refused before any of it is allocated.
metadata/limits_test.go +55
| @@ -128,3 +128,58 @@ func TestParseRefusesOversizedBody(t *testing.T) { | |||
| 128 | 128 | t.Fatal("ParseComment accepted a body past MaxBodyBytes") | |
| 129 | 129 | } | |
| 130 | 130 | } | |
| 131 | |||
| 132 | // TestLoadRequestsReadsEveryFile guards the batched read of the metadata tree: | ||
| 133 | // many requests and comments come back complete, and two files with identical | ||
| 134 | // bytes (one object in the pack, two paths in the tree) each get their content. | ||
| 135 | // The list page reads this tree on every render, so the loader is also the place | ||
| 136 | // where a per-file git process would show up as a page that costs as much as the | ||
| 137 | // repository has issues. | ||
| 138 | func TestLoadRequestsReadsEveryFile(t *testing.T) { | ||
| 139 | t.Parallel() | ||
| 140 | g := limitsRepo(t) | ||
| 141 | |||
| 142 | const n = 12 | ||
| 143 | // One character distinguishes the ids, which keeps every id a well-formed | ||
| 144 | // ULID of the same length. | ||
| 145 | const ulidPrefix = "01M5JR2JK29KKMAA1T9MDNTRB" | ||
| 146 | id := func(i int) string { | ||
| 147 | return ulidPrefix + string("0123456789ABCDEFGHJKMNPQRSTVWXYZ"[i]) | ||
| 148 | } | ||
| 149 | files := map[string][]byte{} | ||
| 150 | for i := 0; i < n; i++ { | ||
| 151 | // Odd and even requests share a body, so their request.md files are the | ||
| 152 | // same object under different paths. | ||
| 153 | body := requestBody(headSHA) | ||
| 154 | if i%2 == 1 { | ||
| 155 | body = requestBody(sideSHA) | ||
| 156 | } | ||
| 157 | files[RequestsDir+"/"+id(i)+"/request.md"] = []byte(body) | ||
| 158 | files[RequestsDir+"/"+id(i)+"/comments/"+id(i+n)+".md"] = []byte( | ||
| 159 | "---\nauthor: keenan\ncreated: 2026-10-22T22:00:00Z\n---\n\ncomment " + string('a'+rune(i)) + "\n") | ||
| 160 | } | ||
| 161 | if _, err := git.CommitFiles(g, Ref, func(f map[string][]byte) error { | ||
| 162 | for path, body := range files { | ||
| 163 | f[path] = body | ||
| 164 | } | ||
| 165 | return nil | ||
| 166 | }, "requests: batch", "keenan"); err != nil { | ||
| 167 | t.Fatal(err) | ||
| 168 | } | ||
| 169 | |||
| 170 | list, err := LoadRequests(g, Ref) | ||
| 171 | if err != nil { | ||
| 172 | t.Fatalf("LoadRequests: %v", err) | ||
| 173 | } | ||
| 174 | if len(list) != n { | ||
| 175 | t.Fatalf("got %d requests, want %d", len(list), n) | ||
| 176 | } | ||
| 177 | for _, req := range list { | ||
| 178 | if len(req.Comments) != 1 { | ||
| 179 | t.Fatalf("%s has %d comments, want 1", req.ID, len(req.Comments)) | ||
| 180 | } | ||
| 181 | if req.Title != "Land the retry" { | ||
| 182 | t.Fatalf("%s title = %q", req.ID, req.Title) | ||
| 183 | } | ||
| 184 | } | ||
| 185 | } | ||