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.
|
c717d4eb
Keenan Nemetz
13d ago
|
1 | package server |
| 2 | ||
| 3 | // The HTTP route table, grouped by the surface each route belongs to. | |
| 4 | // | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
5 | // A repository owns the root of the URL space: `/<repository>/issues/12` is a |
| 6 | // page, `/<repository>.git` is its Git transport. The forge's own pages live | |
| 7 | // under `/-/`, a prefix no repository name can start with, so the two spaces can | |
| 8 | // never collide and no route has to know about repository names. | |
| 9 | // | |
|
c717d4eb
Keenan Nemetz
13d ago
|
10 | // The table is split into one function per area rather than written in one list |
| 11 | // because Go's ServeMux ranks patterns by specificity, not by registration | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
12 | // order: when two patterns overlap (`/{name}/releases/{tag...}` and |
| 13 | // `/{name}/releases/{tag}/archive.tar`, say) they have to be read together to | |
|
c717d4eb
Keenan Nemetz
13d ago
|
14 | // know which one wins. Grouping keeps a route next to the routes it competes |
| 15 | // with. | |
| 16 | ||
| 17 | import ( | |
| 18 | "fmt" | |
| 19 | "net/http" | |
| 20 | ) | |
| 21 | ||
| 22 | // routes builds the forge's HTTP surface. The catch-all is registered last for | |
| 23 | // the benefit of the reader; the router itself does not care where it appears. | |
| 24 | func (s *Server) routes() *http.ServeMux { | |
| 25 | mux := http.NewServeMux() | |
| 26 | s.assetRoutes(mux) | |
| 27 | s.globalRoutes(mux) | |
| 28 | s.authRoutes(mux) | |
| 29 | s.adminRoutes(mux) | |
| 30 | s.repoRoutes(mux) | |
| 31 | // Unknown paths get the same styled 404 as a missing or private repository. | |
| 32 | mux.HandleFunc("/{path...}", s.Web.NotFoundPage) | |
| 33 | return mux | |
| 34 | } | |
| 35 | ||
| 36 | // assetRoutes serves the embedded stylesheet, scripts, and favicon. Assets are | |
| 37 | // compiled into the binary, so there is no directory to deploy beside it. | |
| 38 | func (s *Server) assetRoutes(mux *http.ServeMux) { | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
39 | mux.Handle("GET /-/static/", s.Web.Static()) |
|
c717d4eb
Keenan Nemetz
13d ago
|
40 | } |
| 41 | ||
| 42 | // globalRoutes are the destinations that are not scoped to a repository: | |
|
43e681d8
Keenan Nemetz
4d ago
|
43 | // the repository list, the instance feed, the redirects to the Admin page, and the |
| 44 | // probes a reverse proxy or a monitor calls. | |
|
c717d4eb
Keenan Nemetz
13d ago
|
45 | func (s *Server) globalRoutes(mux *http.ServeMux) { |
| 46 | w := s.Web | |
| 47 | mux.HandleFunc("GET /{$}", w.IndexPage) | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
48 | mux.HandleFunc("GET /-/activity", w.InstanceActivity) |
|
80300457
Keenan Nemetz
1d ago
|
49 | // Crawlers discover the public repositories and pages through the sitemap. |
| 50 | // robots.txt names the paths worth fetching and points at the index; the | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
51 | // per-repository files are below /{name}, where their URLs belong. The two |
| 52 | // crawler files stay at the root because a crawler only looks there, which | |
| 53 | // also makes those three names unavailable to a repository. | |
|
80300457
Keenan Nemetz
1d ago
|
54 | mux.HandleFunc("GET /robots.txt", w.Robots) |
| 55 | mux.HandleFunc("GET /sitemap.xml", w.SitemapIndex) | |
| 56 | mux.HandleFunc("GET /sitemap-pages.xml", w.SitemapPages) | |
|
a6b42433
Keenan Nemetz
8d ago
|
57 | // Accounts are public on this instance: the directory lists everybody, and a |
| 58 | // profile answers for any name in it. Their activity is not public — every | |
| 59 | // count on those pages follows the same read rule as the repository it came | |
| 60 | // from — so these are ordinary global reads, not an admin surface. The | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
61 | // directory answers both spellings so `/-/u` does not redirect to `/-/u/`. |
| 62 | mux.HandleFunc("GET /-/u", w.PeoplePage) | |
| 63 | mux.HandleFunc("GET /-/u/{$}", w.PeoplePage) | |
| 64 | mux.HandleFunc("GET /-/u/{name}", w.ProfilePage) | |
| 65 | mux.HandleFunc("GET /-/search/palette", w.SearchPalette) | |
|
43e681d8
Keenan Nemetz
4d ago
|
66 | // The status tabs moved onto the Admin page; old links redirect to the tab they |
| 67 | // named so a bookmark lands where it used to. | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
68 | mux.HandleFunc("GET /-/status", w.StatusRedirect) |
| 69 | mux.HandleFunc("GET /-/healthz", func(rw http.ResponseWriter, r *http.Request) { | |
|
c717d4eb
Keenan Nemetz
13d ago
|
70 | rw.Header().Set("Content-Type", "text/plain") |
| 71 | fmt.Fprint(rw, "ok\n") | |
| 72 | }) | |
|
43e681d8
Keenan Nemetz
4d ago
|
73 | // Runner presence moved to the Admin page's Runners tab; the old URL is |
|
c717d4eb
Keenan Nemetz
13d ago
|
74 | // linked from notes and scripts, so it redirects rather than 404s. |
|
ed03b3d1
Keenan Nemetz
4h ago
|
75 | mux.HandleFunc("GET /-/runners", func(rw http.ResponseWriter, r *http.Request) { |
| 76 | http.Redirect(rw, r, "/-/admin?tab=runners", http.StatusMovedPermanently) | |
|
c717d4eb
Keenan Nemetz
13d ago
|
77 | }) |
| 78 | } | |
| 79 | ||
| 80 | // authRoutes cover the passkey ceremonies, the session, and the account page. | |
| 81 | // The JSON ceremony endpoints answer only POST because the browser posts the | |
| 82 | // credential there; the pages they belong to are GETs. | |
| 83 | func (s *Server) authRoutes(mux *http.ServeMux) { | |
| 84 | w := s.Web | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
85 | mux.HandleFunc("GET /-/login", w.LoginPage) |
| 86 | mux.HandleFunc("GET /-/dev/login", w.DevLogin) | |
| 87 | mux.HandleFunc("GET /-/register", w.RegisterPage) | |
| 88 | mux.HandleFunc("POST /-/auth/register/begin", w.RegisterBegin) | |
| 89 | mux.HandleFunc("POST /-/auth/register/finish", w.RegisterFinish) | |
| 90 | mux.HandleFunc("POST /-/auth/login/begin", w.LoginBegin) | |
| 91 | mux.HandleFunc("POST /-/auth/login/finish", w.LoginFinish) | |
| 92 | mux.HandleFunc("POST /-/logout", w.Logout) | |
| 93 | mux.HandleFunc("GET /-/invite", w.InvitePage) | |
|
edf9db6e
Keenan Nemetz
12d ago
|
94 | // Enrollment links used to carry the token in the path. They still work, but the |
| 95 | // token is moved into the fragment on the way, where no log can see it. | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
96 | mux.HandleFunc("GET /-/invite/{token}", w.LegacyInvitePage) |
| 97 | mux.HandleFunc("POST /-/auth/invite/begin", w.InviteBegin) | |
| 98 | mux.HandleFunc("POST /-/auth/invite/finish", w.InviteFinish) | |
| 99 | mux.HandleFunc("GET /-/account", w.AccountPage) | |
| 100 | mux.HandleFunc("POST /-/account/keys", w.AddAccountKey) | |
| 101 | mux.HandleFunc("POST /-/account/keys/remove", w.RemoveAccountKey) | |
|
7d7718fa
Keenan Nemetz
2h ago
|
102 | mux.HandleFunc("POST /-/account/passkeys/rename", w.RenameAccountPasskey) |
| 103 | mux.HandleFunc("POST /-/account/passkeys/remove", w.RemoveAccountPasskey) | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
104 | mux.HandleFunc("POST /-/account/passkeys/begin", w.AddPasskeyBegin) |
| 105 | mux.HandleFunc("POST /-/account/passkeys/finish", w.AddPasskeyFinish) | |
|
c717d4eb
Keenan Nemetz
13d ago
|
106 | } |
| 107 | ||
| 108 | // adminRoutes are the instance-wide pages. Every handler checks the instance | |
| 109 | // admin role itself; hiding a link is presentation, never authorization. | |
| 110 | func (s *Server) adminRoutes(mux *http.ServeMux) { | |
| 111 | w := s.Web | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
112 | mux.HandleFunc("GET /-/admin", w.AdminPage) |
| 113 | mux.HandleFunc("POST /-/admin/instance/repo", w.CreateInstanceRepo) | |
| 114 | mux.HandleFunc("GET /-/repos/new", w.AdminNewRepoPage) | |
| 115 | mux.HandleFunc("POST /-/repos", w.CreateAdminRepo) | |
|
43e681d8
Keenan Nemetz
4d ago
|
116 | // The roster is the Accounts tab now; /users stays as the URL the account |
| 117 | // detail pages are still below. | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
118 | mux.HandleFunc("GET /-/users", w.AdminUsersPage) |
| 119 | mux.HandleFunc("POST /-/users", w.CreateAdminUser) | |
| 120 | mux.HandleFunc("POST /-/users/bots", w.CreateAdminBot) | |
| 121 | mux.HandleFunc("GET /-/users/{name}", w.AdminUserPage) | |
| 122 | mux.HandleFunc("POST /-/users/{name}/role", w.SetAdminUserRole) | |
| 123 | mux.HandleFunc("POST /-/users/{name}/retire", w.RetireAdminUser) | |
| 124 | mux.HandleFunc("POST /-/users/{name}/invite", w.InviteAdminUser) | |
| 125 | mux.HandleFunc("POST /-/users/{name}/sessions/revoke", w.RevokeAdminUserSessions) | |
| 126 | mux.HandleFunc("POST /-/users/{name}/keys/remove", w.RemoveAdminUserKey) | |
|
c717d4eb
Keenan Nemetz
13d ago
|
127 | } |
| 128 | ||
|
ed03b3d1
Keenan Nemetz
4h ago
|
129 | // repoRoutes are every page and write below /{name}. A private repository |
|
c717d4eb
Keenan Nemetz
13d ago
|
130 | // answers 404 here exactly as a missing one does; the handlers share that rule |
|
ed03b3d1
Keenan Nemetz
4h ago
|
131 | // through web.open. These patterns overlap the forge's `/-/` routes only in |
| 132 | // shape; the literal `-` prefix is more specific, so it always wins. | |
|
c717d4eb
Keenan Nemetz
13d ago
|
133 | func (s *Server) repoRoutes(mux *http.ServeMux) { |
| 134 | w := s.Web | |
| 135 | ||
|
a2ec9561
Keenan Nemetz
9d ago
|
136 | // Repository summary and feed, and the header's open counts. The tabs poll |
| 137 | // the counts while the page is visible. | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
138 | mux.HandleFunc("GET /{name}", w.Overview) |
| 139 | mux.HandleFunc("GET /{name}/counts", w.RepoCounts) | |
| 140 | mux.HandleFunc("GET /{name}/activity", w.Activity) | |
| 141 | mux.HandleFunc("GET /{name}/sitemap.xml", w.RepoSitemap) | |
|
c717d4eb
Keenan Nemetz
13d ago
|
142 | |
| 143 | // Code browsing: one route per view of a ref, with the path as a wildcard | |
| 144 | // because a branch name may contain "/" and so may a file path. | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
145 | mux.HandleFunc("GET /{name}/code", w.Code) |
| 146 | mux.HandleFunc("GET /{name}/search", w.CodeSearch) | |
| 147 | mux.HandleFunc("GET /{name}/compare", w.Compare) | |
| 148 | mux.HandleFunc("GET /{name}/compare/{range...}", w.Compare) | |
| 149 | mux.HandleFunc("GET /{name}/tree/{ref}", w.Tree) | |
| 150 | mux.HandleFunc("GET /{name}/tree/{ref}/{path...}", w.Tree) | |
| 151 | mux.HandleFunc("GET /{name}/blob/{ref}/{path...}", w.Blob) | |
| 152 | mux.HandleFunc("GET /{name}/raw/{ref}/{path...}", w.Raw) | |
| 153 | mux.HandleFunc("GET /{name}/commits/{ref}", w.Commits) | |
| 154 | mux.HandleFunc("GET /{name}/commits/{ref}/{path...}", w.FileHistory) | |
| 155 | mux.HandleFunc("GET /{name}/blame/{ref}/{path...}", w.Blame) | |
| 156 | mux.HandleFunc("GET /{name}/commit/{sha}", w.CommitView) | |
| 157 | mux.HandleFunc("GET /{name}/commit/{sha}/{path...}", w.CommitView) | |
|
c717d4eb
Keenan Nemetz
13d ago
|
158 | |
| 159 | // Issues: the list and detail pages, then the writes that replace a section | |
| 160 | // of the detail page rather than navigating. | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
161 | mux.HandleFunc("GET /{name}/issues", w.Issues) |
| 162 | mux.HandleFunc("GET /{name}/issues/new", w.NewIssuePage) | |
| 163 | mux.HandleFunc("POST /{name}/issues", w.CreateIssue) | |
| 164 | mux.HandleFunc("GET /{name}/issues/{id}", w.IssueView) | |
| 165 | mux.HandleFunc("GET /{name}/issues/{id}/edit", w.IssueEditPage) | |
| 166 | mux.HandleFunc("POST /{name}/issues/{id}/edit", w.UpdateIssueDetails) | |
|
e05f1c48
Keenan Nemetz
13d ago
|
167 | // One alias route for both kinds. `#42` in a body may name an issue or a |
| 168 | // change request, and the index is what knows which. | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
169 | mux.HandleFunc("GET /{name}/n/{num}", w.NumberAlias) |
| 170 | mux.HandleFunc("POST /{name}/issues/{id}/comments", w.AddIssueComment) | |
| 171 | mux.HandleFunc("POST /{name}/issues/{id}/comments/{comment}/edit", w.EditIssueComment) | |
| 172 | mux.HandleFunc("POST /{name}/issues/{id}/comments/{comment}/delete", w.DeleteIssueComment) | |
| 173 | mux.HandleFunc("POST /{name}/issues/{id}/state", w.ChangeIssueState) | |
| 174 | mux.HandleFunc("POST /{name}/issues/{id}/labels", w.SetIssueLabels) | |
| 175 | mux.HandleFunc("POST /{name}/issues/{id}/assignees", w.SetIssueAssignees) | |
|
c717d4eb
Keenan Nemetz
13d ago
|
176 | |
| 177 | // Change requests: conversation, the Files tab, the Runs tab, and the | |
| 178 | // approval and comment writes. | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
179 | mux.HandleFunc("GET /{name}/changes", w.Requests) |
| 180 | mux.HandleFunc("GET /{name}/changes/new", w.NewChangePage) | |
| 181 | mux.HandleFunc("POST /{name}/changes", w.CreateChangeRequest) | |
| 182 | mux.HandleFunc("GET /{name}/changes/{id}", w.RequestView) | |
| 183 | mux.HandleFunc("GET /{name}/changes/{id}/edit", w.RequestEditPage) | |
| 184 | mux.HandleFunc("POST /{name}/changes/{id}/edit", w.UpdateRequestDetails) | |
| 185 | mux.HandleFunc("GET /{name}/changes/{id}/files", w.RequestFiles) | |
| 186 | mux.HandleFunc("GET /{name}/changes/{id}/files/{path...}", w.RequestFiles) | |
| 187 | mux.HandleFunc("GET /{name}/changes/{id}/runs", w.RequestRuns) | |
| 188 | mux.HandleFunc("GET /{name}/changes/{id}/live", w.RequestLive) | |
| 189 | mux.HandleFunc("GET /{name}/changes/{id}/issues/search", w.SearchRequestIssues) | |
| 190 | mux.HandleFunc("GET /{name}/changes/{id}/comment-form", w.RequestCommentForm) | |
| 191 | mux.HandleFunc("POST /{name}/changes/{id}/approve", w.ApproveRequest) | |
| 192 | mux.HandleFunc("POST /{name}/changes/{id}/merge", w.MergeRequest) | |
| 193 | mux.HandleFunc("POST /{name}/changes/{id}/merge/cancel", w.CancelMerge) | |
| 194 | mux.HandleFunc("POST /{name}/changes/{id}/comments", w.AddRequestComment) | |
| 195 | mux.HandleFunc("POST /{name}/changes/{id}/comments/{comment}/edit", w.EditRequestComment) | |
| 196 | mux.HandleFunc("POST /{name}/changes/{id}/comments/{comment}/delete", w.DeleteRequestComment) | |
| 197 | mux.HandleFunc("POST /{name}/changes/{id}/state", w.ChangeRequestState) | |
| 198 | mux.HandleFunc("POST /{name}/changes/{id}/target", w.UpdateRequestTarget) | |
| 199 | mux.HandleFunc("POST /{name}/changes/{id}/issues", w.UpdateRequestIssues) | |
| 200 | mux.HandleFunc("POST /{name}/ci/allow", w.AllowContributorCI) | |
|
c717d4eb
Keenan Nemetz
13d ago
|
201 | |
| 202 | // Releases. A tag may contain "/", so the detail route takes the rest of the | |
| 203 | // path; the archive routes are more specific and win for their suffixes. | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
204 | mux.HandleFunc("GET /{name}/releases", w.Releases) |
| 205 | mux.HandleFunc("GET /{name}/releases/new", w.ReleaseNewPage) | |
| 206 | mux.HandleFunc("POST /{name}/releases", w.CreateRelease) | |
| 207 | mux.HandleFunc("GET /{name}/releases/edit", w.ReleaseEditPage) | |
| 208 | mux.HandleFunc("POST /{name}/releases/edit", w.UpdateRelease) | |
| 209 | mux.HandleFunc("GET /{name}/releases/{tag...}", w.ReleaseView) | |
| 210 | mux.HandleFunc("GET /{name}/releases/{tag}/archive.tar", w.ReleaseTar) | |
| 211 | mux.HandleFunc("GET /{name}/releases/{tag}/archive.tar.gz", w.ReleaseTarGz) | |
| 212 | mux.HandleFunc("GET /{name}/releases/{tag}/artifacts/{artifact}", w.ReleaseArtifact) | |
|
c717d4eb
Keenan Nemetz
13d ago
|
213 | |
|
57dc79af
Keenan Nemetz
6d ago
|
214 | // CI: the run list and one run with its log and artifacts. Cancel and rerun |
| 215 | // are the only writes. | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
216 | mux.HandleFunc("GET /{name}/runs", w.RunsPage) |
| 217 | mux.HandleFunc("GET /{name}/workflows/dispatch", w.WorkflowDispatchPage) | |
| 218 | mux.HandleFunc("POST /{name}/workflows/dispatch", w.WorkflowDispatch) | |
| 219 | mux.HandleFunc("GET /{name}/runs/{id}", w.RunPage) | |
| 220 | mux.HandleFunc("POST /{name}/runs/{id}/cancel", w.CancelRun) | |
| 221 | mux.HandleFunc("POST /{name}/runs/{id}/rerun", w.Rerun) | |
| 222 | mux.HandleFunc("GET /{name}/runs/{id}/log", w.RunLog) | |
| 223 | mux.HandleFunc("GET /{name}/runs/{id}/artifacts/{artifact}", w.RunArtifact) | |
|
c717d4eb
Keenan Nemetz
13d ago
|
224 | |
| 225 | // Locks and settings. Settings carries both the repository's own settings | |
| 226 | // and, for an instance admin, the access rules. | |
|
ed03b3d1
Keenan Nemetz
4h ago
|
227 | mux.HandleFunc("GET /{name}/locks", w.LocksPage) |
| 228 | mux.HandleFunc("GET /{name}/settings", w.SettingsPage) | |
| 229 | mux.HandleFunc("POST /{name}/settings", w.SaveSettings) | |
| 230 | mux.HandleFunc("POST /{name}/settings/secrets", w.SaveSecret) | |
| 231 | mux.HandleFunc("POST /{name}/settings/secrets/delete", w.DeleteSecret) | |
| 232 | mux.HandleFunc("POST /{name}/settings/access", w.SaveRepositoryAccess) | |
| 233 | mux.HandleFunc("POST /{name}/settings/access/add", w.AddRepositoryAccess) | |
| 234 | mux.HandleFunc("POST /{name}/settings/rename", w.RenameRepository) | |
| 235 | mux.HandleFunc("POST /{name}/settings/delete", w.DeleteRepository) | |
| 236 | ||
| 237 | // Git LFS reads share the same read rule as clone. Uploads are refused here | |
| 238 | // and happen over SSH. The transport itself is not a route: <name>.git/… is | |
| 239 | // recognized before the mux (server/gittransport.go). | |
| 240 | mux.HandleFunc("POST /{name}/info/lfs/objects/batch", s.LFSBatch) | |
| 241 | mux.HandleFunc("GET /{name}/info/lfs/objects/{oid}", s.LFSObject) | |
| 242 | mux.HandleFunc("HEAD /{name}/info/lfs/objects/{oid}", s.LFSObject) | |
|
c717d4eb
Keenan Nemetz
13d ago
|
243 | } |