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/secret_test.go +9−9

@@ -147,7 +147,7 @@ func TestSecretsOnTheSettingsPage(t *testing.T) {
147 147
148 148 // The owner is a repository admin, and an admin is a maintainer, so the
149 149 // section is theirs too.
150 resp, body := f.browserGet(t, "/r/"+f.repo+"/settings?tab=secrets", cookie)
150 resp, body := f.browserGet(t, "/"+f.repo+"/settings?tab=secrets", cookie)
151 151 if resp.StatusCode != 200 || !strings.Contains(body, `id="secrets"`) {
152 152 t.Fatalf("settings page has no secrets section: %d\n%s", resp.StatusCode, body)
153 153 }
@@ -158,7 +158,7 @@ func TestSecretsOnTheSettingsPage(t *testing.T) {
158 158 "secret_name": {"PRIVATE_KEY"},
159 159 "secret_value": {strings.ReplaceAll(value, "\n", "\r\n")},
160 160 }
161 resp, body = f.browserPost(t, "/r/"+f.repo+"/settings/secrets", cookie, form)
161 resp, body = f.browserPost(t, "/"+f.repo+"/settings/secrets", cookie, form)
162 162 if resp.StatusCode != 303 {
163 163 t.Fatalf("save = %d: %s", resp.StatusCode, body)
164 164 }
@@ -166,7 +166,7 @@ func TestSecretsOnTheSettingsPage(t *testing.T) {
166 166 t.Fatalf("save redirected to %q", loc)
167 167 }
168 168
169 resp, body = f.browserGet(t, "/r/"+f.repo+"/settings?tab=secrets&secret_saved=PRIVATE_KEY+v1", cookie)
169 resp, body = f.browserGet(t, "/"+f.repo+"/settings?tab=secrets&secret_saved=PRIVATE_KEY+v1", cookie)
170 170 if resp.StatusCode != 200 {
171 171 t.Fatalf("settings after save = %d", resp.StatusCode)
172 172 }
@@ -198,7 +198,7 @@ func TestSecretsOnTheSettingsPage(t *testing.T) {
198 198 }
199 199
200 200 deleteForm := url.Values{"csrf": {owner.CSRF}, "secret_name": {"PRIVATE_KEY"}}
201 resp, body = f.browserPost(t, "/r/"+f.repo+"/settings/secrets/delete", cookie, deleteForm)
201 resp, body = f.browserPost(t, "/"+f.repo+"/settings/secrets/delete", cookie, deleteForm)
202 202 if resp.StatusCode != 303 || !strings.Contains(resp.Header.Get("Location"), "secret_deleted=PRIVATE_KEY") {
203 203 t.Fatalf("delete = %d %q: %s", resp.StatusCode, resp.Header.Get("Location"), body)
204 204 }
@@ -213,7 +213,7 @@ func TestAMaintainerSeesTheSettingsTabForSecrets(t *testing.T) {
213 213 f.addWriter(t)
214 214 _, cookie := f.browserSession(t, f.writerID)
215 215
216 resp, body := f.browserGet(t, "/r/"+f.repo+"/settings", cookie)
216 resp, body := f.browserGet(t, "/"+f.repo+"/settings", cookie)
217 217 if resp.StatusCode != 200 {
218 218 t.Fatalf("a maintainer's settings page = %d\n%s", resp.StatusCode, body)
219 219 }
@@ -234,7 +234,7 @@ func TestAMaintainerSeesTheSettingsTabForSecrets(t *testing.T) {
234 234 t.Fatal(err)
235 235 }
236 236 _, robinCookie := f.browserSession(t, robin.Name)
237 if resp, _ := f.browserGet(t, "/r/"+f.repo+"/settings", robinCookie); resp.StatusCode != 404 {
237 if resp, _ := f.browserGet(t, "/"+f.repo+"/settings", robinCookie); resp.StatusCode != 404 {
238 238 t.Fatalf("a non-maintainer's settings page = %d, want 404", resp.StatusCode)
239 239 }
240 240 }
@@ -273,14 +273,14 @@ jobs:
273 273 return r.Status != runs.StatusQueued && r.Status != runs.StatusRunning
274 274 })
275 275 if done.Status != runs.StatusSuccess {
276 code, body := f.get("/r/" + f.repo + "/runs/" + done.ID + "/log")
276 code, body := f.get("/" + f.repo + "/runs/" + done.ID + "/log")
277 277 t.Fatalf("run = %s (exit %d), log %d:\n%s", done.Status, done.ExitCode, code, body)
278 278 }
279 279 if len(done.Secrets) != 2 || done.Secrets[0] != "DEPLOY_TOKEN" || done.Secrets[1] != "NOT_SET" {
280 280 t.Fatalf("pinned secrets = %v", done.Secrets)
281 281 }
282 282
283 code, log := f.get("/r/" + f.repo + "/runs/" + done.ID + "/log")
283 code, log := f.get("/" + f.repo + "/runs/" + done.ID + "/log")
284 284 if code != 200 {
285 285 t.Fatalf("log = %d", code)
286 286 }
@@ -334,7 +334,7 @@ jobs:
334 334 if done.Status != runs.StatusSuccess {
335 335 t.Fatalf("a job with an unset secret did not run: %s", done.Status)
336 336 }
337 _, log := f.get("/r/" + f.repo + "/runs/" + done.ID + "/log")
337 _, log := f.get("/" + f.repo + "/runs/" + done.ID + "/log")
338 338 if !strings.Contains(log, "value=unset") {
339 339 t.Fatalf("the variable was not left unset:\n%s", log)
340 340 }