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/seo_test.go +21−21

@@ -26,11 +26,11 @@ func TestRobotsTxtDescribesThePublicSurface(t *testing.T) {
26 26 for _, want := range []string{
27 27 "User-agent: *",
28 28 "Allow: /",
29 "Disallow: /admin",
30 "Disallow: /account",
31 "Disallow: /r/*/settings",
32 "Disallow: /r/*/issues/new",
33 "Disallow: /git/",
29 "Disallow: /-/admin",
30 "Disallow: /-/account",
31 "Disallow: /*/settings",
32 "Disallow: /*/issues/new",
33 "Disallow: /*.git/",
34 34 "Sitemap: " + f.http.URL + "/sitemap.xml",
35 35 } {
36 36 if !strings.Contains(body, want) {
@@ -39,7 +39,7 @@ func TestRobotsTxtDescribesThePublicSurface(t *testing.T) {
39 39 }
40 40 // Profiles are noindex pages, not disallowed ones: a crawler has to be able
41 41 // to fetch them to see the request.
42 for _, unwanted := range []string{"Disallow: /u\n", "Disallow: /u/"} {
42 for _, unwanted := range []string{"Disallow: /-/u\n", "Disallow: /-/u/"} {
43 43 if strings.Contains(body, unwanted) {
44 44 t.Fatalf("robots.txt hides a noindex page with %q:\n%s", unwanted, body)
45 45 }
@@ -69,7 +69,7 @@ func TestSitemapListsPublicRepositoriesAndPages(t *testing.T) {
69 69 for _, want := range []string{
70 70 `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`,
71 71 "<loc>" + f.http.URL + "/sitemap-pages.xml</loc>",
72 "<loc>" + f.http.URL + "/r/demo/sitemap.xml</loc>",
72 "<loc>" + f.http.URL + "/demo/sitemap.xml</loc>",
73 73 } {
74 74 if !strings.Contains(body, want) {
75 75 t.Fatalf("sitemap index is missing %q:\n%s", want, body)
@@ -85,10 +85,10 @@ func TestSitemapListsPublicRepositoriesAndPages(t *testing.T) {
85 85 if _, signedIn := f.browserGet(t, "/sitemap.xml", cookie); strings.Contains(signedIn, "secret") {
86 86 t.Fatalf("a signed-in sitemap names a private repository:\n%s", signedIn)
87 87 }
88 if code, _ := f.get("/r/secret/sitemap.xml"); code != http.StatusNotFound {
88 if code, _ := f.get("/secret/sitemap.xml"); code != http.StatusNotFound {
89 89 t.Fatalf("private repository sitemap = %d, want 404", code)
90 90 }
91 resp, _ = f.browserGet(t, "/r/secret/sitemap.xml", cookie)
91 resp, _ = f.browserGet(t, "/secret/sitemap.xml", cookie)
92 92 if resp.StatusCode != http.StatusNotFound {
93 93 t.Fatalf("signed-in private repository sitemap = %d, want 404", resp.StatusCode)
94 94 }
@@ -96,7 +96,7 @@ func TestSitemapListsPublicRepositoriesAndPages(t *testing.T) {
96 96 _, body = f.browserGet(t, "/sitemap-pages.xml", "")
97 97 for _, want := range []string{
98 98 "<loc>" + f.http.URL + "/</loc>",
99 "<loc>" + f.http.URL + "/activity</loc>",
99 "<loc>" + f.http.URL + "/-/activity</loc>",
100 100 } {
101 101 if !strings.Contains(body, want) {
102 102 t.Fatalf("page sitemap is missing %q:\n%s", want, body)
@@ -132,23 +132,23 @@ func TestRepoSitemapListsIssuesChangesAndReleases(t *testing.T) {
132 132 t.Fatalf("metadata push: %v", err)
133 133 }
134 134
135 resp, body := f.browserGet(t, "/r/"+f.repo+"/sitemap.xml", "")
135 resp, body := f.browserGet(t, "/"+f.repo+"/sitemap.xml", "")
136 136 if resp.StatusCode != http.StatusOK {
137 137 t.Fatalf("repository sitemap = %d\n%s", resp.StatusCode, body)
138 138 }
139 139 base := f.http.URL
140 140 for _, want := range []string{
141 141 `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`,
142 "<loc>" + base + "/r/demo</loc>",
143 "<loc>" + base + "/r/demo/activity</loc>",
144 "<loc>" + base + "/r/demo/code</loc>",
145 "<loc>" + base + "/r/demo/issues</loc>",
146 "<loc>" + base + "/r/demo/issues/" + issueID + "</loc>",
147 "<loc>" + base + "/r/demo/changes</loc>",
148 "<loc>" + base + "/r/demo/changes/" + changeID + "</loc>",
149 "<loc>" + base + "/r/demo/releases</loc>",
150 "<loc>" + base + "/r/demo/releases/v1.0.0</loc>",
151 "<loc>" + base + "/r/demo/commits/main</loc>",
142 "<loc>" + base + "/demo</loc>",
143 "<loc>" + base + "/demo/activity</loc>",
144 "<loc>" + base + "/demo/code</loc>",
145 "<loc>" + base + "/demo/issues</loc>",
146 "<loc>" + base + "/demo/issues/" + issueID + "</loc>",
147 "<loc>" + base + "/demo/changes</loc>",
148 "<loc>" + base + "/demo/changes/" + changeID + "</loc>",
149 "<loc>" + base + "/demo/releases</loc>",
150 "<loc>" + base + "/demo/releases/v1.0.0</loc>",
151 "<loc>" + base + "/demo/commits/main</loc>",
152 152 "<lastmod>",
153 153 } {
154 154 if !strings.Contains(body, want) {