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/web/seo.go +41−41
| @@ -70,46 +70,46 @@ func (w *Web) Robots(rw http.ResponseWriter, r *http.Request) { | |||
| 70 | 70 | fmt.Fprintf(rw, `User-agent: * | |
| 71 | 71 | Allow: / | |
| 72 | 72 | ||
| 73 | # Account and instance-administration surfaces. | ||
| 74 | Disallow: /account | ||
| 75 | Disallow: /admin | ||
| 76 | Disallow: /login | ||
| 77 | Disallow: /register | ||
| 78 | Disallow: /invite | ||
| 79 | Disallow: /users | ||
| 80 | Disallow: /dev/ | ||
| 81 | Disallow: /repos/new | ||
| 82 | Disallow: /runners | ||
| 83 | Disallow: /healthz | ||
| 84 | Disallow: /search/ | ||
| 73 | # The forge's own pages live under /-/. | ||
| 74 | Disallow: /-/account | ||
| 75 | Disallow: /-/admin | ||
| 76 | Disallow: /-/login | ||
| 77 | Disallow: /-/register | ||
| 78 | Disallow: /-/invite | ||
| 79 | Disallow: /-/users | ||
| 80 | Disallow: /-/dev/ | ||
| 81 | Disallow: /-/repos/new | ||
| 82 | Disallow: /-/runners | ||
| 83 | Disallow: /-/healthz | ||
| 84 | Disallow: /-/search/ | ||
| 85 | 85 | ||
| 86 | 86 | # Repository writes, fragments and machine-readable responses. | |
| 87 | Disallow: /git/ | ||
| 88 | Disallow: /r/*/counts | ||
| 89 | Disallow: /r/*/live | ||
| 90 | Disallow: /r/*/raw/ | ||
| 91 | Disallow: /r/*/comment-form | ||
| 92 | Disallow: /r/*/compare | ||
| 93 | Disallow: /r/*/search | ||
| 94 | Disallow: /r/*/issues/new | ||
| 95 | Disallow: /r/*/issues/*/edit | ||
| 96 | Disallow: /r/*/changes/new | ||
| 97 | Disallow: /r/*/changes/*/edit | ||
| 98 | Disallow: /r/*/changes/*/files | ||
| 99 | Disallow: /r/*/changes/*/runs | ||
| 100 | Disallow: /r/*/changes/*/live | ||
| 101 | Disallow: /r/*/releases/new | ||
| 102 | Disallow: /r/*/releases/edit | ||
| 103 | Disallow: /r/*/releases/*/archive.tar | ||
| 104 | Disallow: /r/*/releases/*/archive.tar.gz | ||
| 105 | Disallow: /r/*/releases/*/artifacts/ | ||
| 106 | Disallow: /r/*/runs | ||
| 107 | Disallow: /r/*/workflows/ | ||
| 108 | Disallow: /r/*/locks | ||
| 109 | Disallow: /r/*/settings | ||
| 87 | Disallow: /*.git/ | ||
| 88 | Disallow: /*/counts | ||
| 89 | Disallow: /*/live | ||
| 90 | Disallow: /*/raw/ | ||
| 91 | Disallow: /*/comment-form | ||
| 92 | Disallow: /*/compare | ||
| 93 | Disallow: /*/search | ||
| 94 | Disallow: /*/issues/new | ||
| 95 | Disallow: /*/issues/*/edit | ||
| 96 | Disallow: /*/changes/new | ||
| 97 | Disallow: /*/changes/*/edit | ||
| 98 | Disallow: /*/changes/*/files | ||
| 99 | Disallow: /*/changes/*/runs | ||
| 100 | Disallow: /*/changes/*/live | ||
| 101 | Disallow: /*/releases/new | ||
| 102 | Disallow: /*/releases/edit | ||
| 103 | Disallow: /*/releases/*/archive.tar | ||
| 104 | Disallow: /*/releases/*/archive.tar.gz | ||
| 105 | Disallow: /*/releases/*/artifacts/ | ||
| 106 | Disallow: /*/runs | ||
| 107 | Disallow: /*/workflows/ | ||
| 108 | Disallow: /*/locks | ||
| 109 | Disallow: /*/settings | ||
| 110 | 110 | ||
| 111 | # /u and /u/{name} stay crawlable on purpose: those pages ask for noindex, and a | ||
| 112 | # crawler cannot see that request through a Disallow line. | ||
| 111 | # /-/u and /-/u/{name} stay crawlable on purpose: those pages ask for noindex, and | ||
| 112 | # a crawler cannot see that request through a Disallow line. | ||
| 113 | 113 | Sitemap: %s/sitemap.xml | |
| 114 | 114 | `, w.canonicalBase(r)) | |
| 115 | 115 | } | |
| @@ -128,7 +128,7 @@ func (w *Web) SitemapIndex(rw http.ResponseWriter, r *http.Request) { | |||
| 128 | 128 | sitemaps = append(sitemaps, sitemapRef{Loc: base + "/sitemap-pages.xml"}) | |
| 129 | 129 | for _, e := range entries { | |
| 130 | 130 | sitemaps = append(sitemaps, sitemapRef{ | |
| 131 | Loc: base + "/r/" + url.PathEscape(e.Name) + "/sitemap.xml", | ||
| 131 | Loc: base + "/" + url.PathEscape(e.Name) + "/sitemap.xml", | ||
| 132 | 132 | }) | |
| 133 | 133 | } | |
| 134 | 134 | if len(sitemaps) > sitemapMaxURLs { | |
| @@ -147,11 +147,11 @@ func (w *Web) SitemapPages(rw http.ResponseWriter, r *http.Request) { | |||
| 147 | 147 | base := w.canonicalBase(r) | |
| 148 | 148 | w.writeSitemap(rw, sitemapURLSet{XMLNS: sitemapNS, URLs: []sitemapURL{ | |
| 149 | 149 | {Loc: base + "/"}, | |
| 150 | {Loc: base + "/activity"}, | ||
| 150 | {Loc: base + "/-/activity"}, | ||
| 151 | 151 | }}) | |
| 152 | 152 | } | |
| 153 | 153 | ||
| 154 | // RepoSitemap serves GET /r/{name}/sitemap.xml. It uses the anonymous read rule, | ||
| 154 | // RepoSitemap serves GET /{name}/sitemap.xml. It uses the anonymous read rule, | ||
| 155 | 155 | // not w.open, so a signed-in maintainer or admin gets the same file as everyone | |
| 156 | 156 | // else and a private repository's sitemap is a 404 rather than a session-scoped | |
| 157 | 157 | // document that a proxy could hand to a stranger. | |
| @@ -182,7 +182,7 @@ func (w *Web) RepoSitemap(rw http.ResponseWriter, r *http.Request) { | |||
| 182 | 182 | } | |
| 183 | 183 | ||
| 184 | 184 | base := w.canonicalBase(r) | |
| 185 | root := "/r/" + url.PathEscape(entry.Name) | ||
| 185 | root := "/" + url.PathEscape(entry.Name) | ||
| 186 | 186 | headMod := sitemapUnix(entry.HeadWhen) | |
| 187 | 187 | urls := []sitemapURL{ | |
| 188 | 188 | {Loc: base + root, LastMod: headMod}, | |