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 · 3h 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/workflows_test.go +14−14

@@ -42,7 +42,7 @@ jobs:
42 42 pushed.JobName != "Unit tests" || pushed.JobDescription != "Runs the test suite." {
43 43 t.Fatalf("pinned display metadata = %+v", pushed)
44 44 }
45 code, body := f.get("/r/" + f.repo + "/runs/" + pushed.ID)
45 code, body := f.get("/" + f.repo + "/runs/" + pushed.ID)
46 46 if code != 200 {
47 47 t.Fatalf("pushed run page = %d", code)
48 48 }
@@ -51,11 +51,11 @@ jobs:
51 51 t.Fatalf("run page does not show %q:\n%s", want, body)
52 52 }
53 53 }
54 workflowURL := "/r/" + f.repo + "/blob/" + pushed.SHA + "/.vole/workflows/ci.yaml"
54 workflowURL := "/" + f.repo + "/blob/" + pushed.SHA + "/.vole/workflows/ci.yaml"
55 55 if !strings.Contains(body, `href="`+workflowURL+`"`) {
56 56 t.Fatalf("run page does not link the workflow definition at %s:\n%s", workflowURL, body)
57 57 }
58 if code, _ := f.get("/r/" + f.repo + "/workflows"); code != http.StatusNotFound {
58 if code, _ := f.get("/" + f.repo + "/workflows"); code != http.StatusNotFound {
59 59 t.Fatalf("the removed workflows page = %d, want 404", code)
60 60 }
61 61
@@ -79,7 +79,7 @@ jobs:
79 79 enqueue("01M5R8XR12EZ5WYJDM0F1928D1", runs.StatusQueued)
80 80
81 81 sess, cookie := f.browserSession(t, f.ownerID)
82 resp, body := f.browserGet(t, "/r/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1", cookie)
82 resp, body := f.browserGet(t, "/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1", cookie)
83 83 if resp.StatusCode != 200 || !strings.Contains(body, "Cancel queued run") || strings.Contains(body, ">Rerun<") {
84 84 t.Fatalf("queued run actions = %d:\n%s", resp.StatusCode, body)
85 85 }
@@ -87,7 +87,7 @@ jobs:
87 87 t.Fatalf("queued run page does not show its services:\n%s", body)
88 88 }
89 89
90 resp, _ = f.browserPost(t, "/r/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1/cancel", cookie,
90 resp, _ = f.browserPost(t, "/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1/cancel", cookie,
91 91 url.Values{"csrf": {sess.CSRF}})
92 92 if resp.StatusCode != 303 {
93 93 t.Fatalf("cancel = %d", resp.StatusCode)
@@ -96,16 +96,16 @@ jobs:
96 96 if err != nil || run.Status != runs.StatusCanceled {
97 97 t.Fatalf("canceled run = %+v, %v", run, err)
98 98 }
99 if resp, body := f.browserGet(t, "/r/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1", cookie); resp.StatusCode != 200 || !strings.Contains(body, "Rerun") {
99 if resp, body := f.browserGet(t, "/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1", cookie); resp.StatusCode != 200 || !strings.Contains(body, "Rerun") {
100 100 t.Fatalf("canceled run page = %d:\n%s", resp.StatusCode, body)
101 101 }
102 102
103 resp, _ = f.browserPost(t, "/r/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1/rerun", cookie,
103 resp, _ = f.browserPost(t, "/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1/rerun", cookie,
104 104 url.Values{"csrf": {sess.CSRF}})
105 105 if resp.StatusCode != 303 {
106 106 t.Fatalf("rerun = %d", resp.StatusCode)
107 107 }
108 if resp.Header.Get("Location") != "/r/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1" {
108 if resp.Header.Get("Location") != "/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1" {
109 109 t.Fatalf("rerun location = %q", resp.Header.Get("Location"))
110 110 }
111 111 next, err := f.srv.Runs.Get("01M5R8XR12EZ5WYJDM0F1928D1")
@@ -117,12 +117,12 @@ jobs:
117 117 t.Fatalf("rerun labels = %v, %v", labels, err)
118 118 }
119 119
120 resp, _ = f.browserPost(t, "/r/"+f.repo+"/runs/"+next.ID+"/rerun", cookie,
120 resp, _ = f.browserPost(t, "/"+f.repo+"/runs/"+next.ID+"/rerun", cookie,
121 121 url.Values{"csrf": {sess.CSRF}})
122 122 if resp.StatusCode != 400 {
123 123 t.Fatalf("rerun queued = %d, want 400", resp.StatusCode)
124 124 }
125 resp, _ = f.browserPost(t, "/r/"+f.repo+"/runs/"+next.ID+"/cancel", cookie, url.Values{})
125 resp, _ = f.browserPost(t, "/"+f.repo+"/runs/"+next.ID+"/cancel", cookie, url.Values{})
126 126 if resp.StatusCode != 403 {
127 127 t.Fatalf("cancel without CSRF = %d, want 403", resp.StatusCode)
128 128 }
@@ -156,12 +156,12 @@ func TestStoppingARunningRunFromTheBrowser(t *testing.T) {
156 156 }
157 157
158 158 sess, cookie := f.browserSession(t, f.ownerID)
159 resp, body := f.browserGet(t, "/r/"+f.repo+"/runs/"+id, cookie)
159 resp, body := f.browserGet(t, "/"+f.repo+"/runs/"+id, cookie)
160 160 if resp.StatusCode != 200 || !strings.Contains(body, "Stop running run") {
161 161 t.Fatalf("running run page = %d:\n%s", resp.StatusCode, body)
162 162 }
163 163
164 resp, _ = f.browserPost(t, "/r/"+f.repo+"/runs/"+id+"/cancel", cookie,
164 resp, _ = f.browserPost(t, "/"+f.repo+"/runs/"+id+"/cancel", cookie,
165 165 url.Values{"csrf": {sess.CSRF}})
166 166 if resp.StatusCode != 303 {
167 167 t.Fatalf("stop running run = %d", resp.StatusCode)
@@ -175,7 +175,7 @@ func TestStoppingARunningRunFromTheBrowser(t *testing.T) {
175 175 }
176 176
177 177 // The page says the stop is on its way, and does not offer the button again.
178 resp, body = f.browserGet(t, "/r/"+f.repo+"/runs/"+id, cookie)
178 resp, body = f.browserGet(t, "/"+f.repo+"/runs/"+id, cookie)
179 179 if resp.StatusCode != 200 || !strings.Contains(body, "Stopping") ||
180 180 !strings.Contains(body, f.ownerID) || strings.Contains(body, "Stop running run") {
181 181 t.Fatalf("page while stopping = %d:\n%s", resp.StatusCode, body)
@@ -186,7 +186,7 @@ func TestStoppingARunningRunFromTheBrowser(t *testing.T) {
186 186 if ok, err := f.srv.Runs.Finish(id, f.ownerID, 1, now.Add(time.Minute)); err != nil || !ok {
187 187 t.Fatalf("finish = %t, %v", ok, err)
188 188 }
189 resp, body = f.browserGet(t, "/r/"+f.repo+"/runs/"+id, cookie)
189 resp, body = f.browserGet(t, "/"+f.repo+"/runs/"+id, cookie)
190 190 if resp.StatusCode != 200 || !strings.Contains(body, "canceled") ||
191 191 !strings.Contains(body, "Stopped while running") || !strings.Contains(body, f.ownerID) {
192 192 t.Fatalf("page after the stop = %d:\n%s", resp.StatusCode, body)