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) | |