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.

Blame · server/routes.go

Line attribution on main · 243 line(s)

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 }