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