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/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)