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/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/admin_page_test.go +26−26

@@ -36,14 +36,14 @@ func seedAudit(t *testing.T, path string, n int, detail string) {
36 36 func TestAdminPageIsOnePageWithEveryTab(t *testing.T) {
37 37 t.Parallel()
38 38 f := newFixture(t)
39 code, body := f.adminGet(t, "/admin")
39 code, body := f.adminGet(t, "/-/admin")
40 40 if code != http.StatusOK {
41 41 t.Fatalf("GET /admin = %d:\n%s", code, body)
42 42 }
43 43 for _, want := range []string{
44 44 "Overview", "Storage", "Runners", "Configuration", "Accounts", "Audit log",
45 "/admin?tab=storage", "/admin?tab=runners", "/admin?tab=configuration",
46 "/admin?tab=accounts", "/admin?tab=audit",
45 "/-/admin?tab=storage", "/-/admin?tab=runners", "/-/admin?tab=configuration",
46 "/-/admin?tab=accounts", "/-/admin?tab=audit",
47 47 "Storage used", "Page views",
48 48 } {
49 49 if !strings.Contains(body, want) {
@@ -52,18 +52,18 @@ func TestAdminPageIsOnePageWithEveryTab(t *testing.T) {
52 52 }
53 53
54 54 // Accounts is a tab now, not a separate page.
55 code, accounts := f.adminGet(t, "/admin?tab=accounts")
55 code, accounts := f.adminGet(t, "/-/admin?tab=accounts")
56 56 if code != http.StatusOK || !strings.Contains(accounts, "Invite a person") ||
57 !strings.Contains(accounts, `href="/admin?tab=accounts" aria-current="page"`) {
57 !strings.Contains(accounts, `href="/-/admin?tab=accounts" aria-current="page"`) {
58 58 t.Fatalf("accounts tab = %d:\n%s", code, accounts)
59 59 }
60 60 if strings.Contains(accounts, "Storage used") {
61 61 t.Fatalf("accounts tab shows overview content:\n%s", accounts)
62 62 }
63 63
64 code, log := f.adminGet(t, "/admin?tab=audit")
64 code, log := f.adminGet(t, "/-/admin?tab=audit")
65 65 if code != http.StatusOK || !strings.Contains(log, "Audit log") ||
66 !strings.Contains(log, `href="/admin?tab=audit" aria-current="page"`) {
66 !strings.Contains(log, `href="/-/admin?tab=audit" aria-current="page"`) {
67 67 t.Fatalf("audit tab = %d:\n%s", code, log)
68 68 }
69 69 }
@@ -74,12 +74,12 @@ func TestAdminPageRequiresInstanceAdmin(t *testing.T) {
74 74 f.addWriter(t)
75 75 _, writerCookie := f.browserSession(t, f.writerID)
76 76
77 if resp, _ := f.browserGet(t, "/admin", ""); resp.StatusCode != http.StatusSeeOther ||
78 !strings.Contains(resp.Header.Get("Location"), "/login") {
77 if resp, _ := f.browserGet(t, "/-/admin", ""); resp.StatusCode != http.StatusSeeOther ||
78 !strings.Contains(resp.Header.Get("Location"), "/-/login") {
79 79 t.Fatalf("anonymous GET /admin = %d %q", resp.StatusCode, resp.Header.Get("Location"))
80 80 }
81 81 for _, tab := range []string{"", "?tab=accounts", "?tab=audit"} {
82 if resp, _ := f.browserGet(t, "/admin"+tab, writerCookie); resp.StatusCode != http.StatusNotFound {
82 if resp, _ := f.browserGet(t, "/-/admin"+tab, writerCookie); resp.StatusCode != http.StatusNotFound {
83 83 t.Fatalf("non-admin GET /admin%s = %d, want 404", tab, resp.StatusCode)
84 84 }
85 85 }
@@ -90,12 +90,12 @@ func TestAdminPageRequiresInstanceAdmin(t *testing.T) {
90 90 func TestAdminRedirectsKeepOldLinksWorking(t *testing.T) {
91 91 t.Parallel()
92 92 f := newFixture(t)
93 if resp, _ := f.browserGet(t, "/status?tab=storage", ""); resp.StatusCode != http.StatusMovedPermanently ||
94 resp.Header.Get("Location") != "/admin?tab=storage" {
93 if resp, _ := f.browserGet(t, "/-/status?tab=storage", ""); resp.StatusCode != http.StatusMovedPermanently ||
94 resp.Header.Get("Location") != "/-/admin?tab=storage" {
95 95 t.Fatalf("GET /status?tab=storage = %d %q", resp.StatusCode, resp.Header.Get("Location"))
96 96 }
97 if resp, _ := f.browserGet(t, "/status", ""); resp.StatusCode != http.StatusMovedPermanently ||
98 resp.Header.Get("Location") != "/admin" {
97 if resp, _ := f.browserGet(t, "/-/status", ""); resp.StatusCode != http.StatusMovedPermanently ||
98 resp.Header.Get("Location") != "/-/admin" {
99 99 t.Fatalf("GET /status = %d %q", resp.StatusCode, resp.Header.Get("Location"))
100 100 }
101 101
@@ -103,8 +103,8 @@ func TestAdminRedirectsKeepOldLinksWorking(t *testing.T) {
103 103 t.Fatal(err)
104 104 }
105 105 _, cookie := f.browserSession(t, f.ownerID)
106 if resp, _ := f.browserGet(t, "/users", cookie); resp.StatusCode != http.StatusMovedPermanently ||
107 resp.Header.Get("Location") != "/admin?tab=accounts" {
106 if resp, _ := f.browserGet(t, "/-/users", cookie); resp.StatusCode != http.StatusMovedPermanently ||
107 resp.Header.Get("Location") != "/-/admin?tab=accounts" {
108 108 t.Fatalf("GET /users = %d %q", resp.StatusCode, resp.Header.Get("Location"))
109 109 }
110 110 }
@@ -120,7 +120,7 @@ func TestAdminAuditLogPagesNewestFirst(t *testing.T) {
120 120 // audited, and this test is about which end of the log a page shows.
121 121 seedAudit(t, f.cfg.AuditLog(), 150, "paging")
122 122
123 resp, first := f.browserGet(t, "/admin?tab=audit", cookie)
123 resp, first := f.browserGet(t, "/-/admin?tab=audit", cookie)
124 124 code := resp.StatusCode
125 125 if code != http.StatusOK {
126 126 t.Fatalf("audit tab = %d:\n%s", code, first)
@@ -134,14 +134,14 @@ func TestAdminAuditLogPagesNewestFirst(t *testing.T) {
134 134 if strings.Index(first, "paging-149") > strings.Index(first, "paging-50") {
135 135 t.Fatal("the audit tab is not newest first")
136 136 }
137 if !strings.Contains(first, "/admin?tab=audit&amp;page=2") {
137 if !strings.Contains(first, "/-/admin?tab=audit&amp;page=2") {
138 138 t.Fatalf("the first page has no link to older records:\n%s", first)
139 139 }
140 140 if strings.Contains(first, ">Newer</a>") {
141 141 t.Fatalf("the first page offers a newer page:\n%s", first)
142 142 }
143 143
144 resp, second := f.browserGet(t, "/admin?tab=audit&page=2", cookie)
144 resp, second := f.browserGet(t, "/-/admin?tab=audit&page=2", cookie)
145 145 code = resp.StatusCode
146 146 if code != http.StatusOK || !strings.Contains(second, "paging-49") ||
147 147 !strings.Contains(second, "paging-0") {
@@ -150,7 +150,7 @@ func TestAdminAuditLogPagesNewestFirst(t *testing.T) {
150 150 if strings.Contains(second, "paging-50") {
151 151 t.Fatalf("the second page repeated the first:\n%s", second)
152 152 }
153 if !strings.Contains(second, "/admin?tab=audit\"") || !strings.Contains(second, ">Newer</a>") {
153 if !strings.Contains(second, "/-/admin?tab=audit\"") || !strings.Contains(second, ">Newer</a>") {
154 154 t.Fatalf("the second page has no link back to newer records:\n%s", second)
155 155 }
156 156 if strings.Contains(second, ">Older</a>") {
@@ -167,13 +167,13 @@ func TestAdminAuditLogEmptyAndBadPage(t *testing.T) {
167 167 _, cookie := f.browserSession(t, f.ownerID)
168 168 seedAudit(t, f.cfg.AuditLog(), 0, "unused")
169 169
170 resp, body := f.browserGet(t, "/admin?tab=audit", cookie)
170 resp, body := f.browserGet(t, "/-/admin?tab=audit", cookie)
171 171 code := resp.StatusCode
172 172 if code != http.StatusOK || !strings.Contains(body, "No privileged actions have been recorded yet.") {
173 173 t.Fatalf("empty audit log = %d:\n%s", code, body)
174 174 }
175 175
176 resp, body = f.browserGet(t, "/admin?tab=audit&page=3", cookie)
176 resp, body = f.browserGet(t, "/-/admin?tab=audit&page=3", cookie)
177 177 code = resp.StatusCode
178 178 if code != http.StatusOK || !strings.Contains(body, "No entries on this page.") {
179 179 t.Fatalf("past-the-end page = %d:\n%s", code, body)
@@ -192,7 +192,7 @@ func TestAdminAuditLogEscapesDetailAndIsNotCached(t *testing.T) {
192 192 _, cookie := f.browserSession(t, f.ownerID)
193 193 seedAudit(t, f.cfg.AuditLog(), 1, "<script>alert(1)</script>")
194 194
195 resp, body := f.browserGet(t, "/admin?tab=audit", cookie)
195 resp, body := f.browserGet(t, "/-/admin?tab=audit", cookie)
196 196 if resp.StatusCode != http.StatusOK {
197 197 t.Fatalf("audit tab = %d:\n%s", resp.StatusCode, body)
198 198 }
@@ -246,7 +246,7 @@ func TestAdminOverviewSummarizesAccountsAndRecentActivity(t *testing.T) {
246 246 _, cookie := f.browserSession(t, f.ownerID)
247 247 seedAudit(t, f.cfg.AuditLog(), 8, "overview")
248 248
249 resp, body := f.browserGet(t, "/admin", cookie)
249 resp, body := f.browserGet(t, "/-/admin", cookie)
250 250 if resp.StatusCode != http.StatusOK {
251 251 t.Fatalf("GET /admin = %d:\n%s", resp.StatusCode, body)
252 252 }
@@ -287,7 +287,7 @@ func TestAdminOverviewShowsConfigurationWarnings(t *testing.T) {
287 287 }
288 288 _, cookie := f.browserSession(t, f.ownerID)
289 289
290 resp, body := f.browserGet(t, "/admin", cookie)
290 resp, body := f.browserGet(t, "/-/admin", cookie)
291 291 if resp.StatusCode != http.StatusOK {
292 292 t.Fatalf("GET /admin = %d:\n%s", resp.StatusCode, body)
293 293 }
@@ -311,7 +311,7 @@ func TestAdminOverviewOmitsConfigurationAttentionWhenClean(t *testing.T) {
311 311 }
312 312 _, cookie := f.browserSession(t, f.ownerID)
313 313
314 resp, body := f.browserGet(t, "/admin", cookie)
314 resp, body := f.browserGet(t, "/-/admin", cookie)
315 315 if resp.StatusCode != http.StatusOK {
316 316 t.Fatalf("GET /admin = %d:\n%s", resp.StatusCode, body)
317 317 }