internal/httpd/mractions.go
228 lines · 7633 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/store"
13)
14
15// Merge request actions. Each one runs the control command the CLI runs,
16// so review rules, merge gates, and audit entries have a single
17// implementation; the browser only chooses arguments and shows the
18// result.
19
20// mrRedirect returns to the merge request, carrying a failure message the
21// page renders as a banner.
22func (s *Server) mrRedirect(w http.ResponseWriter, r *http.Request, msg string) {
23 dest := fmt.Sprintf("/%s/%s/mrs/%s",
24 r.PathValue("owner"), r.PathValue("repo"), r.PathValue("n"))
25 s.setFlash(w, msg)
26 http.Redirect(w, r, dest, http.StatusSeeOther)
27}
28
29// mrDiffRedirect returns to the diff view, where the thread controls are.
30func (s *Server) mrDiffRedirect(w http.ResponseWriter, r *http.Request, msg string) {
31 dest := fmt.Sprintf("/%s/%s/mrs/%s?view=diff",
32 r.PathValue("owner"), r.PathValue("repo"), r.PathValue("n"))
33 s.setFlash(w, msg)
34 http.Redirect(w, r, dest, http.StatusSeeOther)
35}
36
37// mrArgs builds "<verb> owner/name <n>" for the mr command family.
38func mrArgs(r *http.Request, verb string, extra ...string) []string {
39 repo := r.PathValue("owner") + "/" + r.PathValue("repo")
40 return append([]string{"mr", verb, repo, r.PathValue("n")}, extra...)
41}
42
43func (s *Server) mrReviewSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
44 flag := map[string]string{
45 "approve": "--approve",
46 "request-changes": "--request-changes",
47 "comment": "--comment",
48 }[r.FormValue("verdict")]
49 if flag == "" {
50 s.mrRedirect(w, r, "pick approve, request changes, or comment")
51 return
52 }
53 _, msg, code := s.runControlCode(u, mrArgs(r, "review", flag))
54 s.done(w, r, code, msg, s.mrRedirect)
55}
56
57func (s *Server) mrReviewRequestSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
58 args := append(fieldArgs("--add", r.FormValue("add")), fieldArgs("--remove", r.FormValue("remove"))...)
59 if len(args) == 0 {
60 s.mrRedirect(w, r, "name at least one person")
61 return
62 }
63 repo := r.PathValue("owner") + "/" + r.PathValue("repo")
64 _, msg, code := s.runControlCode(u, append([]string{"mr", "review", "request", repo, r.PathValue("n")}, args...))
65 s.done(w, r, code, msg, s.mrRedirect)
66}
67
68func (s *Server) mrMergeSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
69 args := []string{}
70 if st := strings.TrimSpace(r.FormValue("strategy")); st != "" && st != "auto" {
71 args = append(args, "--strategy", st)
72 }
73 _, msg, code := s.runControlCode(u, mrArgs(r, "merge", args...))
74 s.done(w, r, code, msg, s.mrRedirect)
75}
76
77func (s *Server) mrCloseSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
78 _, msg, code := s.runControlCode(u, mrArgs(r, "close"))
79 s.done(w, r, code, msg, s.mrRedirect)
80}
81
82// mrDraftSubmit toggles the draft mark. The form says which way it is
83// going, so a stale page cannot flip the wrong one.
84func (s *Server) mrDraftSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
85 verb := "ready"
86 if r.FormValue("draft") == "on" {
87 verb = "draft"
88 }
89 _, msg, code := s.runControlCode(u, mrArgs(r, verb))
90 s.done(w, r, code, msg, s.mrRedirect)
91}
92
93// mrDiffCommentSubmit opens a review thread on a diff line, or replies to
94// one. The body goes in on stdin: it is user prose, and argv is visible in
95// /proc.
96func (s *Server) mrDiffCommentSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
97 body := strings.TrimSpace(r.FormValue("body"))
98 if body == "" {
99 s.mrDiffRedirect(w, r, "empty comment")
100 return
101 }
102 var extra []string
103 if reply := strings.TrimSpace(r.FormValue("reply")); reply != "" {
104 if _, err := strconv.ParseInt(reply, 10, 64); err != nil {
105 s.mrDiffRedirect(w, r, "bad thread id")
106 return
107 }
108 extra = []string{"--reply", reply}
109 } else {
110 path := strings.TrimSpace(r.FormValue("path"))
111 line := strings.TrimSpace(r.FormValue("line"))
112 if n, err := strconv.ParseInt(line, 10, 64); path == "" || err != nil || n < 1 {
113 s.mrDiffRedirect(w, r, "pick a line to comment on")
114 return
115 }
116 extra = []string{"--path", path, "--line", line}
117 if r.FormValue("side") == "old" {
118 extra = append(extra, "--old")
119 }
120 }
121 // "Add to review" holds the comment back until the verdict; "Comment"
122 // posts it now, which is what the form did before there was a choice.
123 if r.FormValue("pending") == "on" {
124 extra = append(extra, "--pending")
125 }
126 msg, code := s.runControlStdinCode(u, mrArgs(r, "diff-comment", append(extra, "--file", "-")...), body)
127 s.done(w, r, code, msg, s.mrDiffRedirect)
128}
129
130// mrRetargetSubmit moves the merge request onto another branch.
131func (s *Server) mrRetargetSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
132 target := strings.TrimSpace(r.FormValue("target"))
133 if target == "" {
134 s.mrRedirect(w, r, "pick a branch to retarget onto")
135 return
136 }
137 _, msg, code := s.runControlCode(u, mrArgs(r, "retarget", target))
138 s.done(w, r, code, msg, s.mrRedirect)
139}
140
141// mrThreadSubmit resolves or reopens one review thread.
142func (s *Server) mrThreadSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
143 verb := "resolve"
144 if r.FormValue("action") == "unresolve" {
145 verb = "unresolve"
146 }
147 id := strings.TrimSpace(r.FormValue("thread"))
148 if _, err := strconv.ParseInt(id, 10, 64); err != nil {
149 s.mrRedirect(w, r, "bad thread id")
150 return
151 }
152 _, msg, code := s.runControlCode(u, mrArgs(r, verb, id))
153 s.done(w, r, code, msg, s.mrRedirect)
154}
155
156// mrNewPage is the create form: branches to choose from, plus whatever
157// the last attempt had in it so a refusal does not lose the draft.
158type mrNewPage struct {
159 repoPage
160 Branches []gitutil.Ref
161 Source string
162 Target string
163 Title string
164 Body string
165 Format string
166 Notice string
167}
168
169func (s *Server) mrCreateForm(w http.ResponseWriter, r *http.Request, u store.User) {
170 p, ok := s.repoFor(w, r, "")
171 if !ok {
172 return
173 }
174 p.Tab = "merge requests"
175 branches, _ := gitutil.Refs(p.Dir, "heads")
176 q := r.URL.Query()
177 target := q.Get("target")
178 if target == "" {
179 target = p.Repo.DefaultBranch
180 }
181 format := q.Get("format")
182 if format != "org" {
183 format = "md"
184 }
185 s.render(w, "mrnew.html", mrNewPage{
186 repoPage: p, Branches: branches,
187 Source: q.Get("source"), Target: target,
188 Title: q.Get("title"), Body: q.Get("body"), Format: format, Notice: s.takeFlash(w, r),
189 })
190}
191
192func (s *Server) mrCreateSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
193 p, ok := s.repoFor(w, r, "")
194 if !ok {
195 return
196 }
197 source := strings.TrimSpace(r.FormValue("source"))
198 target := strings.TrimSpace(r.FormValue("target"))
199 title := strings.TrimSpace(r.FormValue("title"))
200 body := strings.TrimSpace(r.FormValue("body"))
201 format := r.FormValue("format")
202 if format != "org" {
203 format = "md"
204 }
205
206 back := func(msg string) {
207 q := url.Values{"source": {source}, "target": {target}, "title": {title}, "body": {body}, "format": {format}}
208 s.setFlash(w, msg)
209 http.Redirect(w, r, fmt.Sprintf("/%s/mrs/new?%s", p.Repo.Path(), q.Encode()), http.StatusSeeOther)
210 }
211 if source == "" || title == "" {
212 back("pick a source branch and give the merge request a title")
213 return
214 }
215 argv := []string{"mr", "create", p.Repo.Path(), "--source", source, "--title", title, "--format", format}
216 if target != "" {
217 argv = append(argv, "--target", target)
218 }
219 if body != "" {
220 argv = append(argv, "--body", body)
221 }
222 var created control.MRCreated
223 if msg, ok := s.runControlInto(u, argv, &created); !ok {
224 back(msg)
225 return
226 }
227 http.Redirect(w, r, fmt.Sprintf("/%s/mrs/%d", p.Repo.Path(), created.Number), http.StatusSeeOther)
228}