internal/httpd/mractions.go

9d8fd17820457f0c60320c9f4a8b0da1db45a6b0
gitbay/internal/httpd/mractions.go history · blame · raw

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}