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/githttp/transport_test.go +10−10
| @@ -20,12 +20,12 @@ func TestRoute(t *testing.T) { | |||
| 20 | 20 | path, name, rest string | |
| 21 | 21 | ok bool | |
| 22 | 22 | }{ | |
| 23 | {"/git/demo.git/info/refs", "demo", "info/refs", true}, | ||
| 24 | {"/git/demo.git/git-upload-pack", "demo", "git-upload-pack", true}, | ||
| 23 | {"/demo.git/info/refs", "demo", "info/refs", true}, | ||
| 24 | {"/demo.git/git-upload-pack", "demo", "git-upload-pack", true}, | ||
| 25 | 25 | // Found by FuzzRoute: an empty name used to pass, and only the store | |
| 26 | 26 | // refused it. | |
| 27 | {"/git/.git/info/refs", "", "", false}, | ||
| 28 | {"/git/demo/info/refs", "", "", false}, | ||
| 27 | {"/.git/info/refs", "", "", false}, | ||
| 28 | {"/demo/info/refs", "", "", false}, | ||
| 29 | 29 | {"/other/demo.git/info/refs", "", "", false}, | |
| 30 | 30 | } { | |
| 31 | 31 | name, rest, ok := Route(c.path) | |
| @@ -112,7 +112,7 @@ func newProxy(t *testing.T, public bool) *Proxy { | |||
| 112 | 112 | func TestPrivateRepoIs404(t *testing.T) { | |
| 113 | 113 | t.Parallel() | |
| 114 | 114 | p := newProxy(t, false) | |
| 115 | req := httptest.NewRequest(http.MethodGet, "/git/demo.git/info/refs?service=git-upload-pack", nil) | ||
| 115 | req := httptest.NewRequest(http.MethodGet, "/demo.git/info/refs?service=git-upload-pack", nil) | ||
| 116 | 116 | rec := httptest.NewRecorder() | |
| 117 | 117 | p.ServeHTTP(rec, req) | |
| 118 | 118 | if rec.Code != http.StatusNotFound { | |
| @@ -123,7 +123,7 @@ func TestPrivateRepoIs404(t *testing.T) { | |||
| 123 | 123 | func TestPublicRepoServesUploadPack(t *testing.T) { | |
| 124 | 124 | t.Parallel() | |
| 125 | 125 | p := newProxy(t, true) | |
| 126 | req := httptest.NewRequest(http.MethodGet, "/git/demo.git/info/refs?service=git-upload-pack", nil) | ||
| 126 | req := httptest.NewRequest(http.MethodGet, "/demo.git/info/refs?service=git-upload-pack", nil) | ||
| 127 | 127 | rec := httptest.NewRecorder() | |
| 128 | 128 | p.ServeHTTP(rec, req) | |
| 129 | 129 | if rec.Code != http.StatusOK { | |
| @@ -138,8 +138,8 @@ func TestPushOverHTTPRefused(t *testing.T) { | |||
| 138 | 138 | t.Parallel() | |
| 139 | 139 | p := newProxy(t, true) | |
| 140 | 140 | for _, target := range []string{ | |
| 141 | "/git/demo.git/info/refs?service=git-receive-pack", | ||
| 142 | "/git/demo.git/git-receive-pack", | ||
| 141 | "/demo.git/info/refs?service=git-receive-pack", | ||
| 142 | "/demo.git/git-receive-pack", | ||
| 143 | 143 | } { | |
| 144 | 144 | req := httptest.NewRequest(http.MethodPost, target, strings.NewReader("x")) | |
| 145 | 145 | req.ContentLength = 1 | |
| @@ -160,7 +160,7 @@ func TestPushOverHTTPRefused(t *testing.T) { | |||
| 160 | 160 | func TestChunkedReadBodyRefused(t *testing.T) { | |
| 161 | 161 | t.Parallel() | |
| 162 | 162 | p := newProxy(t, true) | |
| 163 | req := httptest.NewRequest(http.MethodPost, "/git/demo.git/git-upload-pack", strings.NewReader("want x")) | ||
| 163 | req := httptest.NewRequest(http.MethodPost, "/demo.git/git-upload-pack", strings.NewReader("want x")) | ||
| 164 | 164 | req.ContentLength = -1 | |
| 165 | 165 | rec := httptest.NewRecorder() | |
| 166 | 166 | p.ServeHTTP(rec, req) | |
| @@ -172,7 +172,7 @@ func TestChunkedReadBodyRefused(t *testing.T) { | |||
| 172 | 172 | func TestDumbHTTPRefused(t *testing.T) { | |
| 173 | 173 | t.Parallel() | |
| 174 | 174 | p := newProxy(t, true) | |
| 175 | for _, target := range []string{"/git/demo.git/HEAD", "/git/demo.git/objects/info/packs"} { | ||
| 175 | for _, target := range []string{"/demo.git/HEAD", "/demo.git/objects/info/packs"} { | ||
| 176 | 176 | req := httptest.NewRequest(http.MethodGet, target, nil) | |
| 177 | 177 | rec := httptest.NewRecorder() | |
| 178 | 178 | p.ServeHTTP(rec, req) | |