internal/httpd/mractions.go

322 lines · 10910 bytes

18 symbols in this file
  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}