internal/httpd/mractions.go
322 lines · 10910 bytes
1package httpd
2
3import (
4 "fmt"
5 "net/http"
6 "net/url"
7 "strconv"
8 "strings"
9
10 "gitbay.org/gitbay/internal/control"
11 "gitbay.org/gitbay/internal/gitutil"
12 "gitbay.org/gitbay/internal/policy"
13 "gitbay.org/gitbay/internal/store"
14)
15
16// Merge request actions. Each one runs the control command the CLI runs,
17// so review rules, merge gates, and audit entries have a single
18// implementation; the browser only chooses arguments and shows the
19// result.
20
21// mrRedirect returns to the merge request, carrying a failure message the
22// page renders as a banner.
23func (s *Server) mrRedirect(w http.ResponseWriter, r *http.Request, msg string) {
24 dest := fmt.Sprintf("/%s/%s/mrs/%s",
25 r.PathValue("owner"), r.PathValue("repo"), r.PathValue("n"))
26 s.setFlash(w, msg)
27 http.Redirect(w, r, dest, http.StatusSeeOther)
28}
29
30// mrDiffRedirect returns to the diff view, where the thread controls are.
31func (s *Server) mrDiffRedirect(w http.ResponseWriter, r *http.Request, msg string) {
32 dest := fmt.Sprintf("/%s/%s/mrs/%s?view=diff",
33 r.PathValue("owner"), r.PathValue("repo"), r.PathValue("n"))
34 s.setFlash(w, msg)
35 http.Redirect(w, r, dest, http.StatusSeeOther)
36}
37
38// mrArgs builds "<verb> owner/name <n>" for the mr command family.
39func mrArgs(r *http.Request, verb string, extra ...string) []string {
40 repo := r.PathValue("owner") + "/" + r.PathValue("repo")
41 return append([]string{"mr", verb, repo, r.PathValue("n")}, extra...)
42}
43
44func (s *Server) mrReviewSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
45 flag := map[string]string{
46 "approve": "--approve",
47 "request-changes": "--request-changes",
48 "comment": "--comment",
49 }[r.FormValue("verdict")]
50 if flag == "" {
51 s.mrRedirect(w, r, "pick approve, request changes, or comment")
52 return
53 }
54 _, msg, code := s.runControlCode(u, mrArgs(r, "review", flag))
55 s.done(w, r, code, msg, s.mrRedirect)
56}
57
58func (s *Server) mrReviewRequestSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
59 args := append(fieldArgs("--add", r.FormValue("add")), fieldArgs("--remove", r.FormValue("remove"))...)
60 if len(args) == 0 {
61 s.mrRedirect(w, r, "name at least one person")
62 return
63 }
64 repo := r.PathValue("owner") + "/" + r.PathValue("repo")
65 _, msg, code := s.runControlCode(u, append([]string{"mr", "review", "request", repo, r.PathValue("n")}, args...))
66 s.done(w, r, code, msg, s.mrRedirect)
67}
68
69func (s *Server) mrLabelSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
70 args := append(fieldArgs("--add", r.FormValue("add")), fieldArgs("--remove", r.FormValue("remove"))...)
71 if len(args) == 0 {
72 s.mrRedirect(w, r, "name at least one label")
73 return
74 }
75 _, msg, code := s.runControlCode(u, mrArgs(r, "label", args...))
76 s.done(w, r, code, msg, s.mrRedirect)
77}
78
79func (s *Server) mrMergeSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
80 args := []string{}
81 if r.FormValue("cancel") == "on" {
82 args = append(args, "--cancel")
83 } else {
84 if st := strings.TrimSpace(r.FormValue("strategy")); st != "" && st != "auto" {
85 args = append(args, "--strategy", st)
86 }
87 if r.FormValue("when_ready") == "on" {
88 args = append(args, "--when-ready")
89 }
90 }
91 _, msg, code := s.runControlCode(u, mrArgs(r, "merge", args...))
92 s.done(w, r, code, msg, s.mrRedirect)
93}
94
95func (s *Server) mrCloseSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
96 args := []string{}
97 if by := strings.TrimPrefix(strings.TrimSpace(r.FormValue("by")), "!"); by != "" {
98 if _, err := strconv.ParseInt(by, 10, 64); err != nil {
99 s.mrRedirect(w, r, "the superseding request is a number")
100 return
101 }
102 args = append(args, "--by", by)
103 }
104 _, msg, code := s.runControlCode(u, mrArgs(r, "close", args...))
105 s.done(w, r, code, msg, s.mrRedirect)
106}
107
108// mrDraftSubmit toggles the draft mark. The form says which way it is
109// going, so a stale page cannot flip the wrong one.
110func (s *Server) mrDraftSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
111 verb := "ready"
112 if r.FormValue("draft") == "on" {
113 verb = "draft"
114 }
115 _, msg, code := s.runControlCode(u, mrArgs(r, verb))
116 s.done(w, r, code, msg, s.mrRedirect)
117}
118
119// mrDiffCommentSubmit opens a review thread on a diff line, or replies to
120// one. The body goes in on stdin: it is user prose, and argv is visible in
121// /proc.
122func (s *Server) mrDiffCommentSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
123 body := strings.TrimSpace(r.FormValue("body"))
124 if body == "" {
125 s.mrDiffRedirect(w, r, "empty comment")
126 return
127 }
128 var extra []string
129 if reply := strings.TrimSpace(r.FormValue("reply")); reply != "" {
130 if _, err := strconv.ParseInt(reply, 10, 64); err != nil {
131 s.mrDiffRedirect(w, r, "bad thread id")
132 return
133 }
134 extra = []string{"--reply", reply}
135 } else {
136 path := strings.TrimSpace(r.FormValue("path"))
137 line := strings.TrimSpace(r.FormValue("line"))
138 if n, err := strconv.ParseInt(line, 10, 64); path == "" || err != nil || n < 1 {
139 s.mrDiffRedirect(w, r, "pick a line to comment on")
140 return
141 }
142 extra = []string{"--path", path, "--line", line}
143 if start := strings.TrimSpace(r.FormValue("start_line")); start != "" {
144 if n, err := strconv.ParseInt(start, 10, 64); err != nil || n < 1 {
145 s.mrDiffRedirect(w, r, "the first line is a line number")
146 return
147 }
148 extra = append(extra, "--start-line", start)
149 }
150 if r.FormValue("side") == "old" {
151 extra = append(extra, "--old")
152 }
153 }
154 // "Add to review" holds the comment back until the verdict; "Comment"
155 // posts it now, which is what the form did before there was a choice.
156 if r.FormValue("pending") == "on" {
157 extra = append(extra, "--pending")
158 }
159 msg, code := s.runControlStdinCode(u, mrArgs(r, "diff-comment", append(extra, "--file", "-")...), body)
160 s.done(w, r, code, msg, s.mrDiffRedirect)
161}
162
163// mrRetargetSubmit moves the merge request onto another branch.
164func (s *Server) mrRetargetSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
165 target := strings.TrimSpace(r.FormValue("target"))
166 if target == "" {
167 s.mrRedirect(w, r, "pick a branch to retarget onto")
168 return
169 }
170 _, msg, code := s.runControlCode(u, mrArgs(r, "retarget", target))
171 s.done(w, r, code, msg, s.mrRedirect)
172}
173
174// mrThreadSubmit resolves or reopens one review thread.
175func (s *Server) mrThreadSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
176 verb := "resolve"
177 if r.FormValue("action") == "unresolve" {
178 verb = "unresolve"
179 }
180 id := strings.TrimSpace(r.FormValue("thread"))
181 if _, err := strconv.ParseInt(id, 10, 64); err != nil {
182 s.mrRedirect(w, r, "bad thread id")
183 return
184 }
185 _, msg, code := s.runControlCode(u, mrArgs(r, verb, id))
186 s.done(w, r, code, msg, s.mrRedirect)
187}
188
189// mrSuggestionSubmit commits a thread's suggestion to the source branch.
190func (s *Server) mrSuggestionSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
191 id := strings.TrimSpace(r.FormValue("thread"))
192 if _, err := strconv.ParseInt(id, 10, 64); err != nil {
193 s.mrDiffRedirect(w, r, "bad thread id")
194 return
195 }
196 _, msg, code := s.runControlCode(u, mrArgs(r, "apply-suggestion", id))
197 s.done(w, r, code, msg, s.mrDiffRedirect)
198}
199
200// mrNewPage is the create form: branches to choose from, plus whatever
201// the last attempt had in it so a refusal does not lose the draft.
202type mrNewPage struct {
203 repoPage
204 Branches []gitutil.Ref
205 Sources []string
206 Source string
207 Target string
208 Title string
209 Body string
210 Format string
211 Notice string
212 Draft *draft
213}
214
215// writableForks lists the forks of repo that u can push to — the source
216// half of what a merge request may be opened from, alongside the
217// repository's own branches. Write is the filter because a contributor
218// proposes from a fork they own; the command still checks the source for
219// itself (#168).
220func (s *Server) writableForks(u store.User, repo store.Repo) []store.Repo {
221 if u.ID == 0 {
222 return nil
223 }
224 forks, _ := s.st.ListForks(repo.ID)
225 var out []store.Repo
226 for _, f := range forks {
227 grant, _ := s.st.AccessRole(f.ID, u.ID)
228 if policy.CanWrite(u, f, grant) {
229 out = append(out, f)
230 }
231 }
232 return out
233}
234
235// mrSources lists the branches a merge request may be opened from, in the
236// form the command takes: this repository's branches by name, and those of
237// any writable fork as "owner/name:branch".
238func (s *Server) mrSources(u store.User, p repoPage) []string {
239 var out []string
240 branches, _ := gitutil.Refs(p.Dir, "heads")
241 for _, b := range branches {
242 out = append(out, b.Name)
243 }
244 for _, f := range s.writableForks(u, p.Repo) {
245 dir := control.RepoDir(s.cfg.Server.Root, f.OwnerName, f.Name)
246 refs, _ := gitutil.Refs(dir, "heads")
247 for _, b := range refs {
248 out = append(out, f.Path()+":"+b.Name)
249 }
250 }
251 return out
252}
253
254func (s *Server) mrCreateForm(w http.ResponseWriter, r *http.Request, u store.User) {
255 p, ok := s.repoFor(w, r, "")
256 if !ok {
257 return
258 }
259 p.Tab = "merge requests"
260 branches, _ := gitutil.Refs(p.Dir, "heads")
261 q := r.URL.Query()
262 target := q.Get("target")
263 if target == "" {
264 target = p.Repo.DefaultBranch
265 }
266 format := q.Get("format")
267 if format != "org" {
268 format = "md"
269 }
270 s.render(w, "mrnew.html", mrNewPage{
271 repoPage: p, Branches: branches, Sources: s.mrSources(u, p),
272 Source: q.Get("source"), Target: target,
273 Title: q.Get("title"), Body: q.Get("body"), Format: format, Notice: s.takeFlash(w, r),
274 })
275}
276
277func (s *Server) mrCreateSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
278 p, ok := s.repoFor(w, r, "")
279 if !ok {
280 return
281 }
282 source := strings.TrimSpace(r.FormValue("source"))
283 target := strings.TrimSpace(r.FormValue("target"))
284 title := strings.TrimSpace(r.FormValue("title"))
285 body := strings.TrimSpace(r.FormValue("body"))
286 format := bodyFormat(r)
287
288 // Preview: the same page back with the draft intact, nothing opened.
289 if wantsPreview(r) {
290 p.Tab = "merge requests"
291 branches, _ := gitutil.Refs(p.Dir, "heads")
292 s.render(w, "mrnew.html", mrNewPage{
293 repoPage: p, Branches: branches, Sources: s.mrSources(u, p),
294 Source: source, Target: target, Title: title, Body: body, Format: format,
295 Draft: s.draftFor(r, p.Repo, "body", "body", format),
296 })
297 return
298 }
299
300 back := func(msg string) {
301 q := url.Values{"source": {source}, "target": {target}, "title": {title}, "body": {body}, "format": {format}}
302 s.setFlash(w, msg)
303 http.Redirect(w, r, fmt.Sprintf("/%s/mrs/new?%s", p.Repo.Path(), q.Encode()), http.StatusSeeOther)
304 }
305 if source == "" || title == "" {
306 back("pick a source branch and give the merge request a title")
307 return
308 }
309 argv := []string{"mr", "create", p.Repo.Path(), "--source", source, "--title", title, "--format", format}
310 if target != "" {
311 argv = append(argv, "--target", target)
312 }
313 if body != "" {
314 argv = append(argv, "--body", body)
315 }
316 var created control.MRCreated
317 if msg, ok := s.runControlInto(u, argv, &created); !ok {
318 back(msg)
319 return
320 }
321 http.Redirect(w, r, fmt.Sprintf("/%s/mrs/%d", p.Repo.Path(), created.Number), http.StatusSeeOther)
322}