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/home_test.go +16−16

@@ -30,8 +30,8 @@ func TestHomeIsALandingPage(t *testing.T) {
30 30 for _, want := range []string{
31 31 `class="hero"`, "<h1>Acme Forge</h1>",
32 32 `readme-card`, `class="md readme-body"`, "What this forge is for.",
33 "Repositories", `class="repo-card"`, `href="/r/demo"`,
34 "Latest", `href="/activity"`,
33 "Repositories", `class="repo-card"`, `href="/demo"`,
34 "Latest", `href="/-/activity"`,
35 35 } {
36 36 if !strings.Contains(body, want) {
37 37 t.Fatalf("home is missing %q:\n%s", want, body)
@@ -80,7 +80,7 @@ func TestHomePutsNewRepositoryInTheHero(t *testing.T) {
80 80 _, body := f.browserGet(t, "/", cookie)
81 81
82 82 hero := strings.Index(body, `class="hero"`)
83 button := strings.Index(body, `href="/repos/new"`)
83 button := strings.Index(body, `href="/-/repos/new"`)
84 84 grid := strings.Index(body, `class="home-grid"`)
85 85 if hero < 0 || button < 0 || grid < 0 || hero > button || button > grid {
86 86 t.Fatalf("the New repository action is not in the page header (hero=%d button=%d grid=%d):\n%s", hero, button, grid, body)
@@ -94,14 +94,14 @@ func TestHomeHasTheCleanTopBar(t *testing.T) {
94 94
95 95 _, body := f.get("/")
96 96 for _, want := range []string{
97 `class="topbar"`, `class="topnav"`, `href="/activity"`,
98 `class="topsearch"`, `class="topsearch-box"`, `href="/login"`,
97 `class="topbar"`, `class="topnav"`, `href="/-/activity"`,
98 `class="topsearch"`, `class="topsearch-box"`, `href="/-/login"`,
99 99 } {
100 100 if !strings.Contains(body, want) {
101 101 t.Fatalf("home is missing the top-bar marker %q:\n%s", want, body)
102 102 }
103 103 }
104 for _, unwanted := range []string{`class="rail"`, `src="/static/nav.js"`, `>Projects</a>`, `>Repositories</a>`, `href="/runners"`, `href="/admin"`} {
104 for _, unwanted := range []string{`class="rail"`, `src="/-/static/nav.js"`, `>Projects</a>`, `>Repositories</a>`, `href="/-/runners"`, `href="/-/admin"`} {
105 105 if strings.Contains(body, unwanted) {
106 106 t.Fatalf("home still has %q:\n%s", unwanted, body)
107 107 }
@@ -109,12 +109,12 @@ func TestHomeHasTheCleanTopBar(t *testing.T) {
109 109
110 110 _, cookie := f.browserSession(t, f.ownerID)
111 111 _, body = f.browserGet(t, "/", cookie)
112 for _, want := range []string{`class="account-menu"`, `class="account-pop"`, `href="/account"`, `action="/logout"`} {
112 for _, want := range []string{`class="account-menu"`, `class="account-pop"`, `href="/-/account"`, `action="/-/logout"`} {
113 113 if !strings.Contains(body, want) {
114 114 t.Fatalf("signed-in top bar is missing %q:\n%s", want, body)
115 115 }
116 116 }
117 if strings.Contains(body, `href="/admin"`) {
117 if strings.Contains(body, `href="/-/admin"`) {
118 118 t.Fatalf("non-admin sees the Status link:\n%s", body)
119 119 }
120 120
@@ -122,7 +122,7 @@ func TestHomeHasTheCleanTopBar(t *testing.T) {
122 122 t.Fatal(err)
123 123 }
124 124 _, body = f.browserGet(t, "/", cookie)
125 if !strings.Contains(body, `href="/admin"`) {
125 if !strings.Contains(body, `href="/-/admin"`) {
126 126 t.Fatalf("instance admin does not see the Status link:\n%s", body)
127 127 }
128 128 }
@@ -133,7 +133,7 @@ func TestHomeSearchShowsProjectCards(t *testing.T) {
133 133 f.publish(f.repo)
134 134
135 135 _, body := f.get("/?q=demo")
136 for _, want := range []string{"Results", `class="repo-card"`, `href="/r/demo"`, "clear search"} {
136 for _, want := range []string{"Results", `class="repo-card"`, `href="/demo"`, "clear search"} {
137 137 if !strings.Contains(body, want) {
138 138 t.Fatalf("search results are missing %q:\n%s", want, body)
139 139 }
@@ -194,26 +194,26 @@ func TestInstanceActivityListsOnlyVisibleRepositories(t *testing.T) {
194 194 t.Fatalf("private issue: %v\n%s", err, errs)
195 195 }
196 196
197 code, body := f.get("/activity")
197 code, body := f.get("/-/activity")
198 198 if code != 200 {
199 199 t.Fatalf("activity = %d", code)
200 200 }
201 for _, want := range []string{`href="/r/demo"`, "public-issue-marker", "v1.0.0", "released"} {
201 for _, want := range []string{`href="/demo"`, "public-issue-marker", "v1.0.0", "released"} {
202 202 if !strings.Contains(body, want) {
203 203 t.Fatalf("activity feed is missing %q:\n%s", want, body)
204 204 }
205 205 }
206 for _, unwanted := range []string{"private-issue-marker", `href="/r/secret"`, "public-commit", "private-commit", "committed"} {
206 for _, unwanted := range []string{"private-issue-marker", `href="/secret"`, "public-commit", "private-commit", "committed"} {
207 207 if strings.Contains(body, unwanted) {
208 208 t.Fatalf("activity feed contains %q:\n%s", unwanted, body)
209 209 }
210 210 }
211 211
212 _, body = f.get("/activity?kind=issues")
212 _, body = f.get("/-/activity?kind=issues")
213 213 if !strings.Contains(body, "public-issue-marker") || strings.Contains(body, "v1.0.0") {
214 214 t.Fatalf("issue filter is wrong:\n%s", body)
215 215 }
216 _, body = f.get("/activity?kind=releases")
216 _, body = f.get("/-/activity?kind=releases")
217 217 if !strings.Contains(body, "v1.0.0") || strings.Contains(body, "public-issue-marker") {
218 218 t.Fatalf("release filter is wrong:\n%s", body)
219 219 }
@@ -244,7 +244,7 @@ func TestFirstRunOnboardingIsAnInstanceAdminPage(t *testing.T) {
244 244 if resp.StatusCode != 200 {
245 245 t.Fatalf("home = %d", resp.StatusCode)
246 246 }
247 for _, want := range []string{"No repositories yet", "New repository", "Or from the instance directory", "--owner keenan", `href="/repos/new"`} {
247 for _, want := range []string{"No repositories yet", "New repository", "Or from the instance directory", "--owner keenan", `href="/-/repos/new"`} {
248 248 if !strings.Contains(body, want) {
249 249 t.Fatalf("first-run page is missing %q:\n%s", want, body)
250 250 }