internal/httpd/mractions.go

aa6d5bfab86d3163d485be9d89b313c2b0546009
gitbay/internal/httpd/mractions.go history · blame · raw

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}