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.
Serve repositories from the root of the URL space
ed03b3d1fa16ef6b55e2773aaf10e3be13282c3a · Keenan Nemetz · 4h 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/artifacts_test.go +8−8
| @@ -56,7 +56,7 @@ func TestWorkflowArtifactsArePublished(t *testing.T) { | |||
| 56 | 56 | } | |
| 57 | 57 | ||
| 58 | 58 | // The run page names them, so a download has a description of its build. | |
| 59 | code, page := f.get("/r/" + f.repo + "/runs/" + done.ID) | ||
| 59 | code, page := f.get("/" + f.repo + "/runs/" + done.ID) | ||
| 60 | 60 | if code != 200 { | |
| 61 | 61 | t.Fatalf("run page = %d", code) | |
| 62 | 62 | } | |
| @@ -67,7 +67,7 @@ func TestWorkflowArtifactsArePublished(t *testing.T) { | |||
| 67 | 67 | } | |
| 68 | 68 | ||
| 69 | 69 | // The bytes come back out of the store, with the object id as the validator. | |
| 70 | url := f.http.URL + "/r/" + f.repo + "/runs/" + done.ID + "/artifacts/tool-linux" | ||
| 70 | url := f.http.URL + "/" + f.repo + "/runs/" + done.ID + "/artifacts/tool-linux" | ||
| 71 | 71 | resp, err := http.Get(url) | |
| 72 | 72 | if err != nil { | |
| 73 | 73 | t.Fatal(err) | |
| @@ -94,11 +94,11 @@ func TestWorkflowArtifactsArePublished(t *testing.T) { | |||
| 94 | 94 | t.Fatalf("conditional artifact request = %d, want 304", again.StatusCode) | |
| 95 | 95 | } | |
| 96 | 96 | ||
| 97 | if code, _ := f.get("/r/" + f.repo + "/runs/" + done.ID + "/artifacts/nope"); code != 404 { | ||
| 97 | if code, _ := f.get("/" + f.repo + "/runs/" + done.ID + "/artifacts/nope"); code != 404 { | ||
| 98 | 98 | t.Fatalf("unknown artifact = %d, want 404", code) | |
| 99 | 99 | } | |
| 100 | 100 | // The repository page links the run, so the artifacts are reachable by clicking. | |
| 101 | if _, repoPage := f.get("/r/" + f.repo); !strings.Contains(repoPage, "/runs/"+done.ID) { | ||
| 101 | if _, repoPage := f.get("/" + f.repo); !strings.Contains(repoPage, "/runs/"+done.ID) { | ||
| 102 | 102 | t.Fatalf("repository page does not link the run:\n%s", repoPage) | |
| 103 | 103 | } | |
| 104 | 104 | } | |
| @@ -154,8 +154,8 @@ func TestRunArtifactsArePrivateForPrivateRepos(t *testing.T) { | |||
| 154 | 154 | done := f.waitForRun(t, 10*time.Second, func(r *runs.Run) bool { return r.Status == runs.StatusSuccess }) | |
| 155 | 155 | ||
| 156 | 156 | for _, path := range []string{ | |
| 157 | "/r/" + f.repo + "/runs/" + done.ID, | ||
| 158 | "/r/" + f.repo + "/runs/" + done.ID + "/artifacts/tool-linux", | ||
| 157 | "/" + f.repo + "/runs/" + done.ID, | ||
| 158 | "/" + f.repo + "/runs/" + done.ID + "/artifacts/tool-linux", | ||
| 159 | 159 | } { | |
| 160 | 160 | if code, _ := f.get(path); code != 404 { | |
| 161 | 161 | t.Fatalf("%s on a private repo = %d, want 404", path, code) | |
| @@ -166,8 +166,8 @@ func TestRunArtifactsArePrivateForPrivateRepos(t *testing.T) { | |||
| 166 | 166 | // not keep either and serve it to an anonymous reader. | |
| 167 | 167 | _, cookie := f.browserSession(t, f.ownerID) | |
| 168 | 168 | for _, path := range []string{ | |
| 169 | "/r/" + f.repo + "/runs/" + done.ID, | ||
| 170 | "/r/" + f.repo + "/runs/" + done.ID + "/artifacts/tool-linux", | ||
| 169 | "/" + f.repo + "/runs/" + done.ID, | ||
| 170 | "/" + f.repo + "/runs/" + done.ID + "/artifacts/tool-linux", | ||
| 171 | 171 | } { | |
| 172 | 172 | resp, _ := f.browserGet(t, path, cookie) | |
| 173 | 173 | if resp.StatusCode != http.StatusOK { | |