internal/httpd/mractions.go
201 lines · 6471 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 if msg != "" {
26 if len(msg) > 300 {
27 msg = msg[:300]
28 }
29 dest += "?e=" + url.QueryEscape(msg)
30 }
31 http.Redirect(w, r, dest, http.StatusSeeOther)
32}
33
34// mrDiffRedirect returns to the diff view, where the thread controls are.
35func (s *Server) mrDiffRedirect(w http.ResponseWriter, r *http.Request, msg string) {
36 dest := fmt.Sprintf("/%s/%s/mrs/%s?view=diff",
37 r.PathValue("owner"), r.PathValue("repo"), r.PathValue("n"))
38 if msg != "" {
39 if len(msg) > 300 {
40 msg = msg[:300]
41 }
42 dest += "&e=" + url.QueryEscape(msg)
43 }
44 http.Redirect(w, r, dest, http.StatusSeeOther)
45}
46
47// mrArgs builds "<verb> owner/name <n>" for the mr command family.
48func mrArgs(r *http.Request, verb string, extra ...string) []string {
49 repo := r.PathValue("owner") + "/" + r.PathValue("repo")
50 return append([]string{"mr", verb, repo, r.PathValue("n")}, extra...)
51}
52
53func (s *Server) mrReviewSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
54 flag := map[string]string{
55 "approve": "--approve",
56 "request-changes": "--request-changes",
57 "comment": "--comment",
58 }[r.FormValue("verdict")]
59 if flag == "" {
60 s.mrRedirect(w, r, "pick approve, request changes, or comment")
61 return
62 }
63 _, msg, code := s.runControlCode(u, mrArgs(r, "review", flag))
64 s.done(w, r, code, msg, s.mrRedirect)
65}
66
67func (s *Server) mrMergeSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
68 args := []string{}
69 if st := strings.TrimSpace(r.FormValue("strategy")); st != "" && st != "auto" {
70 args = append(args, "--strategy", st)
71 }
72 _, msg, code := s.runControlCode(u, mrArgs(r, "merge", args...))
73 s.done(w, r, code, msg, s.mrRedirect)
74}
75
76func (s *Server) mrCloseSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
77 _, msg, code := s.runControlCode(u, mrArgs(r, "close"))
78 s.done(w, r, code, msg, s.mrRedirect)
79}
80
81// mrDiffCommentSubmit opens a review thread on a diff line, or replies to
82// one. The body goes in on stdin: it is user prose, and argv is visible in
83// /proc.
84func (s *Server) mrDiffCommentSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
85 body := strings.TrimSpace(r.FormValue("body"))
86 if body == "" {
87 s.mrDiffRedirect(w, r, "empty comment")
88 return
89 }
90 var extra []string
91 if reply := strings.TrimSpace(r.FormValue("reply")); reply != "" {
92 if _, err := strconv.ParseInt(reply, 10, 64); err != nil {
93 s.mrDiffRedirect(w, r, "bad thread id")
94 return
95 }
96 extra = []string{"--reply", reply}
97 } else {
98 path := strings.TrimSpace(r.FormValue("path"))
99 line := strings.TrimSpace(r.FormValue("line"))
100 if n, err := strconv.ParseInt(line, 10, 64); path == "" || err != nil || n < 1 {
101 s.mrDiffRedirect(w, r, "pick a line to comment on")
102 return
103 }
104 extra = []string{"--path", path, "--line", line}
105 if r.FormValue("side") == "old" {
106 extra = append(extra, "--old")
107 }
108 }
109 msg, code := s.runControlStdinCode(u, mrArgs(r, "diff-comment", append(extra, "--file", "-")...), body)
110 s.done(w, r, code, msg, s.mrDiffRedirect)
111}
112
113// mrRetargetSubmit moves the merge request onto another branch.
114func (s *Server) mrRetargetSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
115 target := strings.TrimSpace(r.FormValue("target"))
116 if target == "" {
117 s.mrRedirect(w, r, "pick a branch to retarget onto")
118 return
119 }
120 _, msg, code := s.runControlCode(u, mrArgs(r, "retarget", target))
121 s.done(w, r, code, msg, s.mrRedirect)
122}
123
124// mrThreadSubmit resolves or reopens one review thread.
125func (s *Server) mrThreadSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
126 verb := "resolve"
127 if r.FormValue("action") == "unresolve" {
128 verb = "unresolve"
129 }
130 id := strings.TrimSpace(r.FormValue("thread"))
131 if _, err := strconv.ParseInt(id, 10, 64); err != nil {
132 s.mrRedirect(w, r, "bad thread id")
133 return
134 }
135 _, msg, code := s.runControlCode(u, mrArgs(r, verb, id))
136 s.done(w, r, code, msg, s.mrRedirect)
137}
138
139// mrNewPage is the create form: branches to choose from, plus whatever
140// the last attempt had in it so a refusal does not lose the draft.
141type mrNewPage struct {
142 repoPage
143 Branches []gitutil.Ref
144 Source string
145 Target string
146 Title string
147 Body string
148 Notice string
149}
150
151func (s *Server) mrCreateForm(w http.ResponseWriter, r *http.Request, u store.User) {
152 p, ok := s.repoFor(w, r, "")
153 if !ok {
154 return
155 }
156 p.Tab = "merge requests"
157 branches, _ := gitutil.Refs(p.Dir, "heads")
158 q := r.URL.Query()
159 target := q.Get("target")
160 if target == "" {
161 target = p.Repo.DefaultBranch
162 }
163 s.render(w, "mrnew.html", mrNewPage{
164 repoPage: p, Branches: branches,
165 Source: q.Get("source"), Target: target,
166 Title: q.Get("title"), Body: q.Get("body"), Notice: q.Get("e"),
167 })
168}
169
170func (s *Server) mrCreateSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
171 p, ok := s.repoFor(w, r, "")
172 if !ok {
173 return
174 }
175 source := strings.TrimSpace(r.FormValue("source"))
176 target := strings.TrimSpace(r.FormValue("target"))
177 title := strings.TrimSpace(r.FormValue("title"))
178 body := strings.TrimSpace(r.FormValue("body"))
179
180 back := func(msg string) {
181 q := url.Values{"source": {source}, "target": {target}, "title": {title}, "body": {body}, "e": {msg}}
182 http.Redirect(w, r, fmt.Sprintf("/%s/mrs/new?%s", p.Repo.Path(), q.Encode()), http.StatusSeeOther)
183 }
184 if source == "" || title == "" {
185 back("pick a source branch and give the merge request a title")
186 return
187 }
188 argv := []string{"mr", "create", p.Repo.Path(), "--source", source, "--title", title}
189 if target != "" {
190 argv = append(argv, "--target", target)
191 }
192 if body != "" {
193 argv = append(argv, "--body", body)
194 }
195 var created control.MRCreated
196 if msg, ok := s.runControlInto(u, argv, &created); !ok {
197 back(msg)
198 return
199 }
200 http.Redirect(w, r, fmt.Sprintf("/%s/mrs/%d", p.Repo.Path(), created.Number), http.StatusSeeOther)
201}