internal/httpd/mractions.go
219 lines · 6444 bytes
1package httpd
2
3import (
4 "fmt"
5 "net/http"
6 "net/url"
7 "strconv"
8 "strings"
9
10 "gitbay.org/gitbay/internal/gitutil"
11 "gitbay.org/gitbay/internal/store"
12)
13
14// Merge request actions. Each one runs the control command the CLI runs,
15// so review rules, merge gates, and audit entries have a single
16// implementation; the browser only chooses arguments and shows the
17// result.
18
19// mrRedirect returns to the merge request, carrying a failure message the
20// page renders as a banner.
21func (s *Server) mrRedirect(w http.ResponseWriter, r *http.Request, msg string) {
22 dest := fmt.Sprintf("/%s/%s/mrs/%s",
23 r.PathValue("owner"), r.PathValue("repo"), r.PathValue("n"))
24 if msg != "" {
25 if len(msg) > 300 {
26 msg = msg[:300]
27 }
28 dest += "?e=" + url.QueryEscape(msg)
29 }
30 http.Redirect(w, r, dest, http.StatusSeeOther)
31}
32
33// mrDiffRedirect returns to the diff view, where the thread controls are.
34func (s *Server) mrDiffRedirect(w http.ResponseWriter, r *http.Request, msg string) {
35 dest := fmt.Sprintf("/%s/%s/mrs/%s?view=diff",
36 r.PathValue("owner"), r.PathValue("repo"), r.PathValue("n"))
37 if msg != "" {
38 if len(msg) > 300 {
39 msg = msg[:300]
40 }
41 dest += "&e=" + url.QueryEscape(msg)
42 }
43 http.Redirect(w, r, dest, http.StatusSeeOther)
44}
45
46// mrArgs builds "<verb> owner/name <n>" for the mr command family.
47func mrArgs(r *http.Request, verb string, extra ...string) []string {
48 repo := r.PathValue("owner") + "/" + r.PathValue("repo")
49 return append([]string{"mr", verb, repo, r.PathValue("n")}, extra...)
50}
51
52func (s *Server) mrReviewSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
53 flag := map[string]string{
54 "approve": "--approve",
55 "request-changes": "--request-changes",
56 "comment": "--comment",
57 }[r.FormValue("verdict")]
58 if flag == "" {
59 s.mrRedirect(w, r, "pick approve, request changes, or comment")
60 return
61 }
62 _, msg, ok := s.runControl(u, mrArgs(r, "review", flag))
63 if ok {
64 msg = ""
65 }
66 s.mrRedirect(w, r, msg)
67}
68
69func (s *Server) mrMergeSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
70 args := []string{}
71 if st := strings.TrimSpace(r.FormValue("strategy")); st != "" && st != "auto" {
72 args = append(args, "--strategy", st)
73 }
74 _, msg, ok := s.runControl(u, mrArgs(r, "merge", args...))
75 if ok {
76 msg = ""
77 }
78 s.mrRedirect(w, r, msg)
79}
80
81func (s *Server) mrCloseSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
82 _, msg, ok := s.runControl(u, mrArgs(r, "close"))
83 if ok {
84 msg = ""
85 }
86 s.mrRedirect(w, r, msg)
87}
88
89// mrDiffCommentSubmit opens a review thread on a diff line, or replies to
90// one. The body goes in on stdin: it is user prose, and argv is visible in
91// /proc.
92func (s *Server) mrDiffCommentSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
93 body := strings.TrimSpace(r.FormValue("body"))
94 if body == "" {
95 s.mrDiffRedirect(w, r, "empty comment")
96 return
97 }
98 var extra []string
99 if reply := strings.TrimSpace(r.FormValue("reply")); reply != "" {
100 if _, err := strconv.ParseInt(reply, 10, 64); err != nil {
101 s.mrDiffRedirect(w, r, "bad thread id")
102 return
103 }
104 extra = []string{"--reply", reply}
105 } else {
106 path := strings.TrimSpace(r.FormValue("path"))
107 line := strings.TrimSpace(r.FormValue("line"))
108 if n, err := strconv.ParseInt(line, 10, 64); path == "" || err != nil || n < 1 {
109 s.mrDiffRedirect(w, r, "pick a line to comment on")
110 return
111 }
112 extra = []string{"--path", path, "--line", line}
113 if r.FormValue("side") == "old" {
114 extra = append(extra, "--old")
115 }
116 }
117 msg, ok := s.runControlStdin(u, mrArgs(r, "diff-comment", append(extra, "--file", "-")...), body)
118 if ok {
119 msg = ""
120 }
121 s.mrDiffRedirect(w, r, msg)
122}
123
124// mrRetargetSubmit moves the merge request onto another branch.
125func (s *Server) mrRetargetSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
126 target := strings.TrimSpace(r.FormValue("target"))
127 if target == "" {
128 s.mrRedirect(w, r, "pick a branch to retarget onto")
129 return
130 }
131 _, msg, ok := s.runControl(u, mrArgs(r, "retarget", target))
132 if ok {
133 msg = ""
134 }
135 s.mrRedirect(w, r, msg)
136}
137
138// mrThreadSubmit resolves or reopens one review thread.
139func (s *Server) mrThreadSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
140 verb := "resolve"
141 if r.FormValue("action") == "unresolve" {
142 verb = "unresolve"
143 }
144 id := strings.TrimSpace(r.FormValue("thread"))
145 if _, err := strconv.ParseInt(id, 10, 64); err != nil {
146 s.mrRedirect(w, r, "bad thread id")
147 return
148 }
149 _, msg, ok := s.runControl(u, mrArgs(r, verb, id))
150 if ok {
151 msg = ""
152 }
153 s.mrRedirect(w, r, msg)
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 Notice string
166}
167
168func (s *Server) mrCreateForm(w http.ResponseWriter, r *http.Request, u store.User) {
169 p, ok := s.repoFor(w, r, "")
170 if !ok {
171 return
172 }
173 p.Tab = "merge requests"
174 branches, _ := gitutil.Refs(p.Dir, "heads")
175 q := r.URL.Query()
176 target := q.Get("target")
177 if target == "" {
178 target = p.Repo.DefaultBranch
179 }
180 s.render(w, "mrnew.html", mrNewPage{
181 repoPage: p, Branches: branches,
182 Source: q.Get("source"), Target: target,
183 Title: q.Get("title"), Body: q.Get("body"), Notice: q.Get("e"),
184 })
185}
186
187func (s *Server) mrCreateSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
188 p, ok := s.repoFor(w, r, "")
189 if !ok {
190 return
191 }
192 source := strings.TrimSpace(r.FormValue("source"))
193 target := strings.TrimSpace(r.FormValue("target"))
194 title := strings.TrimSpace(r.FormValue("title"))
195 body := strings.TrimSpace(r.FormValue("body"))
196
197 back := func(msg string) {
198 q := url.Values{"source": {source}, "target": {target}, "title": {title}, "body": {body}, "e": {msg}}
199 http.Redirect(w, r, fmt.Sprintf("/%s/mrs/new?%s", p.Repo.Path(), q.Encode()), http.StatusSeeOther)
200 }
201 if source == "" || title == "" {
202 back("pick a source branch and give the merge request a title")
203 return
204 }
205 argv := []string{"mr", "create", p.Repo.Path(), "--source", source, "--title", title}
206 if target != "" {
207 argv = append(argv, "--target", target)
208 }
209 if body != "" {
210 argv = append(argv, "--body", body)
211 }
212 data, msg, ok := s.runControlJSON(u, argv)
213 if !ok {
214 back(msg)
215 return
216 }
217 n, _ := data["number"].(float64)
218 http.Redirect(w, r, fmt.Sprintf("/%s/mrs/%d", p.Repo.Path(), int64(n)), http.StatusSeeOther)
219}