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/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 }