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.
Serve repositories from the root of the URL space
ed03b3d1fa16ef6b55e2773aaf10e3be13282c3a · Keenan Nemetz · 9h ago · parent d37d301f
Serve repositories from the root of the URL space A repository is now /<name> and its Git transport /<name>.git, so the URL a person opens, the URL a clone uses, and the Go module path are the same name. The forge's own pages moved under /-/ (admin, accounts, the directory, the feed, sign-in, assets), a prefix no repository name can start with: names are [a-z0-9...] and cannot begin with "-". That leaves the router with two disjoint spaces and removes the /r/ and /git/ prefixes, which existed only to keep repositories away from the forge's own paths. Routes: repoRoutes lose /r/; global, auth and admin routes gain /-/; assets move to /-/static/ because a subtree pattern like /static/ cannot coexist with one-segment-wildcard repo routes — Go's mux reports them as conflicting neither-more-specific patterns. The catch-all still renders the styled 404. Transport: githttp.Route parses /<name>.git/<rest> instead of a /git/ prefix, and forgepaths.go hands such requests to the proxy before the mux, because a wildcard fills a whole segment and cannot match "<name>.git". LFS stays on the mux (<name>.git/info/lfs/...), and the Go module discovery probe moves to the same pre-router so a package path under a public repository is answered even when its first segments name a page tab. Names: repo.ValidateName now refuses the four root paths the forge must keep — static, robots.txt, sitemap.xml, sitemap-pages.xml. Everything else is an ordinary repository name, including admin, u, and activity. Docs and the packaged URLs follow the new layout; robots.txt, the sitemaps and the instance activity feed point at the new paths. No aliases: the old /r/ and /git/ URLs are gone. Verified with ./vole workflow check and ./vole workflow run verify.
server/releases_test.go +23−23
| @@ -67,7 +67,7 @@ func TestReleaseListAndNotes(t *testing.T) { | |||
| 67 | 67 | t.Parallel() | |
| 68 | 68 | f := releaseFixture(t) | |
| 69 | 69 | ||
| 70 | code, body := f.get("/r/" + f.repo + "/releases") | ||
| 70 | code, body := f.get("/" + f.repo + "/releases") | ||
| 71 | 71 | if code != 200 { | |
| 72 | 72 | t.Fatalf("releases page = %d", code) | |
| 73 | 73 | } | |
| @@ -81,11 +81,11 @@ func TestReleaseListAndNotes(t *testing.T) { | |||
| 81 | 81 | t.Fatalf("releases page does not label the lightweight tag:\n%s", body) | |
| 82 | 82 | } | |
| 83 | 83 | // The repository page counts releases, and counts only the annotated ones. | |
| 84 | if _, repoPage := f.get("/r/" + f.repo); !strings.Contains(repoPage, "/releases") { | ||
| 84 | if _, repoPage := f.get("/" + f.repo); !strings.Contains(repoPage, "/releases") { | ||
| 85 | 85 | t.Fatalf("repository page does not link to releases:\n%s", repoPage) | |
| 86 | 86 | } | |
| 87 | 87 | ||
| 88 | code, page := f.get("/r/" + f.repo + "/releases/v1.0.0") | ||
| 88 | code, page := f.get("/" + f.repo + "/releases/v1.0.0") | ||
| 89 | 89 | if code != 200 { | |
| 90 | 90 | t.Fatalf("release page = %d", code) | |
| 91 | 91 | } | |
| @@ -105,10 +105,10 @@ func TestReleaseListAndNotes(t *testing.T) { | |||
| 105 | 105 | t.Fatalf("release page warns about LFS in a repository that does not use it:\n%s", page) | |
| 106 | 106 | } | |
| 107 | 107 | // The first release has no previous tag to range over, and still renders. | |
| 108 | if code, first := f.get("/r/" + f.repo + "/releases/v0.9.0"); code != 200 || !strings.Contains(first, "beta") { | ||
| 108 | if code, first := f.get("/" + f.repo + "/releases/v0.9.0"); code != 200 || !strings.Contains(first, "beta") { | ||
| 109 | 109 | t.Fatalf("first release page = %d, notes missing", code) | |
| 110 | 110 | } | |
| 111 | if code, _ := f.get("/r/" + f.repo + "/releases/nope"); code != 404 { | ||
| 111 | if code, _ := f.get("/" + f.repo + "/releases/nope"); code != 404 { | ||
| 112 | 112 | t.Fatalf("unknown tag = %d, want 404", code) | |
| 113 | 113 | } | |
| 114 | 114 | } | |
| @@ -132,7 +132,7 @@ func TestReleaseNotesAreSanitised(t *testing.T) { | |||
| 132 | 132 | t.Fatalf("push tag: %s", errs) | |
| 133 | 133 | } | |
| 134 | 134 | ||
| 135 | _, page := f.get("/r/" + f.repo + "/releases/v2.0.0") | ||
| 135 | _, page := f.get("/" + f.repo + "/releases/v2.0.0") | ||
| 136 | 136 | if strings.Contains(page, "<script>alert(1)</script>") { | |
| 137 | 137 | t.Fatalf("release notes execute as markup:\n%s", page) | |
| 138 | 138 | } | |
| @@ -162,7 +162,7 @@ func TestReleasePageWarnsAboutLFSPointers(t *testing.T) { | |||
| 162 | 162 | t.Fatalf("push tag: %s", errs) | |
| 163 | 163 | } | |
| 164 | 164 | ||
| 165 | code, page := f.get("/r/" + f.repo + "/releases/v1.0.0") | ||
| 165 | code, page := f.get("/" + f.repo + "/releases/v1.0.0") | ||
| 166 | 166 | if code != 200 { | |
| 167 | 167 | t.Fatalf("release page = %d", code) | |
| 168 | 168 | } | |
| @@ -176,7 +176,7 @@ func TestReleaseArchiveIsGeneratedFromTheTag(t *testing.T) { | |||
| 176 | 176 | t.Parallel() | |
| 177 | 177 | f := releaseFixture(t) | |
| 178 | 178 | ||
| 179 | code, body := f.get("/r/" + f.repo + "/releases/v1.0.0/archive.tar") | ||
| 179 | code, body := f.get("/" + f.repo + "/releases/v1.0.0/archive.tar") | ||
| 180 | 180 | if code != 200 { | |
| 181 | 181 | t.Fatalf("archive.tar = %d", code) | |
| 182 | 182 | } | |
| @@ -193,13 +193,13 @@ func TestReleaseArchiveIsGeneratedFromTheTag(t *testing.T) { | |||
| 193 | 193 | ||
| 194 | 194 | // The first tag's tarball has the older content: this is what proves the | |
| 195 | 195 | // archive is built from the tag rather than from the branch head. | |
| 196 | _, older := f.get("/r/" + f.repo + "/releases/v0.9.0/archive.tar") | ||
| 196 | _, older := f.get("/" + f.repo + "/releases/v0.9.0/archive.tar") | ||
| 197 | 197 | if got := tarContents(t, strings.NewReader(older), false)[f.repo+"-v0.9.0/readme.md"]; got != "first\n" { | |
| 198 | 198 | t.Fatalf("v0.9.0 tarball readme = %q", got) | |
| 199 | 199 | } | |
| 200 | 200 | ||
| 201 | 201 | // A gzipped variant, decoded, holds the same file. | |
| 202 | code, gz := f.get("/r/" + f.repo + "/releases/v1.0.0/archive.tar.gz") | ||
| 202 | code, gz := f.get("/" + f.repo + "/releases/v1.0.0/archive.tar.gz") | ||
| 203 | 203 | if code != 200 { | |
| 204 | 204 | t.Fatalf("archive.tar.gz = %d", code) | |
| 205 | 205 | } | |
| @@ -209,10 +209,10 @@ func TestReleaseArchiveIsGeneratedFromTheTag(t *testing.T) { | |||
| 209 | 209 | ||
| 210 | 210 | // A lightweight tag has a tarball too: the archive is the tree, and the | |
| 211 | 211 | // absence of notes does not change what the tree is. | |
| 212 | if code, _ := f.get("/r/" + f.repo + "/releases/scratch-tag/archive.tar"); code != 200 { | ||
| 212 | if code, _ := f.get("/" + f.repo + "/releases/scratch-tag/archive.tar"); code != 200 { | ||
| 213 | 213 | t.Fatalf("lightweight tag archive = %d", code) | |
| 214 | 214 | } | |
| 215 | if code, _ := f.get("/r/" + f.repo + "/releases/nope/archive.tar"); code != 404 { | ||
| 215 | if code, _ := f.get("/" + f.repo + "/releases/nope/archive.tar"); code != 404 { | ||
| 216 | 216 | t.Fatalf("unknown tag archive = %d, want 404", code) | |
| 217 | 217 | } | |
| 218 | 218 | } | |
| @@ -224,7 +224,7 @@ func TestReleaseArchiveValidators(t *testing.T) { | |||
| 224 | 224 | t.Parallel() | |
| 225 | 225 | f := releaseFixture(t) | |
| 226 | 226 | ||
| 227 | url := f.http.URL + "/r/" + f.repo + "/releases/v1.0.0/archive.tar" | ||
| 227 | url := f.http.URL + "/" + f.repo + "/releases/v1.0.0/archive.tar" | ||
| 228 | 228 | resp, err := http.Get(url) | |
| 229 | 229 | if err != nil { | |
| 230 | 230 | t.Fatal(err) | |
| @@ -266,9 +266,9 @@ func TestReleasePagesAreNotPublicForPrivateRepos(t *testing.T) { | |||
| 266 | 266 | t.Fatalf("push: %s", errs) | |
| 267 | 267 | } | |
| 268 | 268 | for _, path := range []string{ | |
| 269 | "/r/" + f.repo + "/releases", | ||
| 270 | "/r/" + f.repo + "/releases/main", | ||
| 271 | "/r/" + f.repo + "/releases/main/archive.tar", | ||
| 269 | "/" + f.repo + "/releases", | ||
| 270 | "/" + f.repo + "/releases/main", | ||
| 271 | "/" + f.repo + "/releases/main/archive.tar", | ||
| 272 | 272 | } { | |
| 273 | 273 | if code, _ := f.get(path); code != 404 { | |
| 274 | 274 | t.Fatalf("%s on a private repo = %d, want 404", path, code) | |
| @@ -338,7 +338,7 @@ func TestReleaseArtifactDownload(t *testing.T) { | |||
| 338 | 338 | ||
| 339 | 339 | // The page names the artifact and links to it, so a reader never has to guess | |
| 340 | 340 | // a URL or hold a sha256. | |
| 341 | code, page := f.get("/r/" + f.repo + "/releases/v1.0.0") | ||
| 341 | code, page := f.get("/" + f.repo + "/releases/v1.0.0") | ||
| 342 | 342 | if code != 200 { | |
| 343 | 343 | t.Fatalf("release page = %d", code) | |
| 344 | 344 | } | |
| @@ -348,7 +348,7 @@ func TestReleaseArtifactDownload(t *testing.T) { | |||
| 348 | 348 | } | |
| 349 | 349 | } | |
| 350 | 350 | ||
| 351 | url := f.http.URL + "/r/" + f.repo + "/releases/v1.0.0/artifacts/tool-linux-amd64" | ||
| 351 | url := f.http.URL + "/" + f.repo + "/releases/v1.0.0/artifacts/tool-linux-amd64" | ||
| 352 | 352 | resp, err := http.Get(url) | |
| 353 | 353 | if err != nil { | |
| 354 | 354 | t.Fatal(err) | |
| @@ -401,7 +401,7 @@ func TestReleaseArtifactDownload(t *testing.T) { | |||
| 401 | 401 | t.Fatalf("range request = %d %q, want 206 %q", part.StatusCode, sliced, data[:4]) | |
| 402 | 402 | } | |
| 403 | 403 | ||
| 404 | if code, _ := f.get("/r/" + f.repo + "/releases/v1.0.0/artifacts/nope"); code != 404 { | ||
| 404 | if code, _ := f.get("/" + f.repo + "/releases/v1.0.0/artifacts/nope"); code != 404 { | ||
| 405 | 405 | t.Fatalf("unknown artifact = %d, want 404", code) | |
| 406 | 406 | } | |
| 407 | 407 | } | |
| @@ -424,7 +424,7 @@ func TestReleasePointerWithoutObjectIsRefused(t *testing.T) { | |||
| 424 | 424 | t.Fatalf("the refusal does not name the missing object: %v", err) | |
| 425 | 425 | } | |
| 426 | 426 | // And the release page does not show it, because the ref never moved. | |
| 427 | if code, _ := f.get("/r/" + f.repo + "/releases/v1.0.0/artifacts/ghost"); code != 404 { | ||
| 427 | if code, _ := f.get("/" + f.repo + "/releases/v1.0.0/artifacts/ghost"); code != 404 { | ||
| 428 | 428 | t.Fatalf("ghost artifact = %d, want 404", code) | |
| 429 | 429 | } | |
| 430 | 430 | } | |
| @@ -474,7 +474,7 @@ func TestReleaseArtifactsArePrivateForPrivateRepos(t *testing.T) { | |||
| 474 | 474 | t.Fatalf("push tag: %s", errs) | |
| 475 | 475 | } | |
| 476 | 476 | attachArtifact(t, f, "v1.0.0", "tool", []byte("secret\n")) | |
| 477 | if code, _ := f.get("/r/" + f.repo + "/releases/v1.0.0/artifacts/tool"); code != 404 { | ||
| 477 | if code, _ := f.get("/" + f.repo + "/releases/v1.0.0/artifacts/tool"); code != 404 { | ||
| 478 | 478 | t.Fatalf("private artifact = %d, want 404", code) | |
| 479 | 479 | } | |
| 480 | 480 | ||
| @@ -482,8 +482,8 @@ func TestReleaseArtifactsArePrivateForPrivateRepos(t *testing.T) { | |||
| 482 | 482 | // them and hand them to the next anonymous reader. | |
| 483 | 483 | _, cookie := f.browserSession(t, f.ownerID) | |
| 484 | 484 | for _, path := range []string{ | |
| 485 | "/r/" + f.repo + "/releases/v1.0.0/artifacts/tool", | ||
| 486 | "/r/" + f.repo + "/releases/v1.0.0/archive.tar", | ||
| 485 | "/" + f.repo + "/releases/v1.0.0/artifacts/tool", | ||
| 486 | "/" + f.repo + "/releases/v1.0.0/archive.tar", | ||
| 487 | 487 | } { | |
| 488 | 488 | resp, _ := f.browserGet(t, path, cookie) | |
| 489 | 489 | if resp.StatusCode != http.StatusOK { | |