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.

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 {