Commit 467699a1ed
Verified · cmc ci/build: success ci/test: success ci/vuln: success
Layout: unified · split
e2e/webwrites_test.go +6
| @@ -50,6 +50,12 @@ func TestWebWritesGoThroughRegistry(t *testing.T) { | ||
| 50 | 50 | t.Fatalf("repo created past the quota from the web: exit %d, want 3", code) |
| 51 | 51 | } |
| 52 | 52 | |
| 53 | // A form action on an issue that does not exist is the 404 page, not a | |
| 54 | // redirect carrying a message (#106). | |
| 55 | if status, _ := browserPost(t, browser, inst.base()+"/alice/first/issues/999/state", url.Values{"action": {"close"}}); status != 404 { | |
| 56 | t.Fatalf("closing a missing issue from the web: %d, want 404", status) | |
| 57 | } | |
| 58 | ||
| 53 | 59 | // An archived repository refuses a comment from the web as it does |
| 54 | 60 | // over ssh. |
| 55 | 61 | if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "archive", "alice/first"); code != 0 { |
internal/httpd/control.go +35 −4
| @@ -20,6 +20,14 @@ import ( | ||
| 20 | 20 | // ViaAPI is set, which refuses SSHOnly commands: anything whose input is a |
| 21 | 21 | // credential (secrets, mirror tokens, session minting) stays on SSH. |
| 22 | 22 | func (s *Server) runControl(u store.User, argv []string) (out string, msg string, ok bool) { |
| 23 | out, msg, code := s.runControlCode(u, argv) | |
| 24 | return out, msg, code == protocol.ExitOK | |
| 25 | } | |
| 26 | ||
| 27 | // runControlCode is runControl with the exit code, for handlers that | |
| 28 | // answer a form: not-found and denied deserve their own statuses rather | |
| 29 | // than a redirect carrying the message (#106). | |
| 30 | func (s *Server) runControlCode(u store.User, argv []string) (out string, msg string, code int) { | |
| 23 | 31 | var stdout, stderr bytes.Buffer |
| 24 | 32 | ctx := &control.Ctx{ |
| 25 | 33 | User: u, |
| @@ -32,12 +40,30 @@ func (s *Server) runControl(u store.User, argv []string) (out string, msg string | ||
| 32 | 40 | Stderr: &stderr, |
| 33 | 41 | ViaAPI: true, |
| 34 | 42 | } |
| 35 | code := control.Dispatch(ctx, argv) | |
| 43 | code = control.Dispatch(ctx, argv) | |
| 36 | 44 | m := strings.TrimSpace(stderr.String()) |
| 37 | 45 | if m == "" { |
| 38 | 46 | m = strings.TrimSpace(stdout.String()) |
| 39 | 47 | } |
| 40 | return stdout.String(), m, code == protocol.ExitOK | |
| 48 | return stdout.String(), m, code | |
| 49 | } | |
| 50 | ||
| 51 | // done finishes a form action by exit code: back to the page on success, | |
| 52 | // the 404 page when the thing does not exist, and back to the page with | |
| 53 | // the message for anything else. A refusal is feedback on the page a | |
| 54 | // person was looking at, whether it is a merge gate, a permission they | |
| 55 | // lack, or a field they got wrong; only a thing that does not exist has | |
| 56 | // no page to go back to. | |
| 57 | func (s *Server) done(w http.ResponseWriter, r *http.Request, code int, msg string, | |
| 58 | redirect func(http.ResponseWriter, *http.Request, string)) { | |
| 59 | switch code { | |
| 60 | case protocol.ExitOK: | |
| 61 | redirect(w, r, "") | |
| 62 | case protocol.ExitNotFound: | |
| 63 | s.notFound(w, r) | |
| 64 | default: | |
| 65 | redirect(w, r, msg) | |
| 66 | } | |
| 41 | 67 | } |
| 42 | 68 | |
| 43 | 69 | // runControlStdin is runControl for the handful of commands whose input |
| @@ -46,6 +72,11 @@ func (s *Server) runControl(u store.User, argv []string) (out string, msg string | ||
| 46 | 72 | // tokens and mirror credentials remain SSHOnly and are refused by the |
| 47 | 73 | // dispatcher. |
| 48 | 74 | func (s *Server) runControlStdin(u store.User, argv []string, stdin string) (msg string, ok bool) { |
| 75 | msg, code := s.runControlStdinCode(u, argv, stdin) | |
| 76 | return msg, code == protocol.ExitOK | |
| 77 | } | |
| 78 | ||
| 79 | func (s *Server) runControlStdinCode(u store.User, argv []string, stdin string) (msg string, code int) { | |
| 49 | 80 | var stdout, stderr bytes.Buffer |
| 50 | 81 | ctx := &control.Ctx{ |
| 51 | 82 | User: u, |
| @@ -58,12 +89,12 @@ func (s *Server) runControlStdin(u store.User, argv []string, stdin string) (msg | ||
| 58 | 89 | Stderr: &stderr, |
| 59 | 90 | ViaAPI: true, |
| 60 | 91 | } |
| 61 | code := control.Dispatch(ctx, argv) | |
| 92 | code = control.Dispatch(ctx, argv) | |
| 62 | 93 | m := strings.TrimSpace(stderr.String()) |
| 63 | 94 | if m == "" { |
| 64 | 95 | m = strings.TrimSpace(stdout.String()) |
| 65 | 96 | } |
| 66 | return m, code == protocol.ExitOK | |
| 97 | return m, code | |
| 67 | 98 | } |
| 68 | 99 | |
| 69 | 100 | // runControlInto runs a command in JSON mode and decodes its data into |
internal/httpd/issueactions.go +8 −20
| @@ -36,11 +36,8 @@ func (s *Server) issueStateSubmit(w http.ResponseWriter, r *http.Request, u stor | ||
| 36 | 36 | if r.FormValue("action") == "reopen" { |
| 37 | 37 | verb = "reopen" |
| 38 | 38 | } |
| 39 | _, msg, ok := s.runControl(u, issueArgs(r, verb)) | |
| 40 | if ok { | |
| 41 | msg = "" | |
| 42 | } | |
| 43 | s.issueRedirect(w, r, msg) | |
| 39 | _, msg, code := s.runControlCode(u, issueArgs(r, verb)) | |
| 40 | s.done(w, r, code, msg, s.issueRedirect) | |
| 44 | 41 | } |
| 45 | 42 | |
| 46 | 43 | // fieldArgs turns a space-separated form value into repeated flags, the |
| @@ -59,11 +56,8 @@ func (s *Server) issueLabelSubmit(w http.ResponseWriter, r *http.Request, u stor | ||
| 59 | 56 | s.issueRedirect(w, r, "name at least one label") |
| 60 | 57 | return |
| 61 | 58 | } |
| 62 | _, msg, ok := s.runControl(u, issueArgs(r, "label", args...)) | |
| 63 | if ok { | |
| 64 | msg = "" | |
| 65 | } | |
| 66 | s.issueRedirect(w, r, msg) | |
| 59 | _, msg, code := s.runControlCode(u, issueArgs(r, "label", args...)) | |
| 60 | s.done(w, r, code, msg, s.issueRedirect) | |
| 67 | 61 | } |
| 68 | 62 | |
| 69 | 63 | func (s *Server) issueAssignSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| @@ -72,11 +66,8 @@ func (s *Server) issueAssignSubmit(w http.ResponseWriter, r *http.Request, u sto | ||
| 72 | 66 | s.issueRedirect(w, r, "name at least one person") |
| 73 | 67 | return |
| 74 | 68 | } |
| 75 | _, msg, ok := s.runControl(u, issueArgs(r, "assign", args...)) | |
| 76 | if ok { | |
| 77 | msg = "" | |
| 78 | } | |
| 79 | s.issueRedirect(w, r, msg) | |
| 69 | _, msg, code := s.runControlCode(u, issueArgs(r, "assign", args...)) | |
| 70 | s.done(w, r, code, msg, s.issueRedirect) | |
| 80 | 71 | } |
| 81 | 72 | |
| 82 | 73 | func (s *Server) issueMilestoneSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| @@ -84,9 +75,6 @@ func (s *Server) issueMilestoneSubmit(w http.ResponseWriter, r *http.Request, u | ||
| 84 | 75 | if title == "" { |
| 85 | 76 | title = "none" |
| 86 | 77 | } |
| 87 | _, msg, ok := s.runControl(u, issueArgs(r, "milestone", title)) | |
| 88 | if ok { | |
| 89 | msg = "" | |
| 90 | } | |
| 91 | s.issueRedirect(w, r, msg) | |
| 78 | _, msg, code := s.runControlCode(u, issueArgs(r, "milestone", title)) | |
| 79 | s.done(w, r, code, msg, s.issueRedirect) | |
| 92 | 80 | } |
internal/httpd/mractions.go +12 −30
| @@ -59,11 +59,8 @@ func (s *Server) mrReviewSubmit(w http.ResponseWriter, r *http.Request, u store. | ||
| 59 | 59 | s.mrRedirect(w, r, "pick approve, request changes, or comment") |
| 60 | 60 | return |
| 61 | 61 | } |
| 62 | _, msg, ok := s.runControl(u, mrArgs(r, "review", flag)) | |
| 63 | if ok { | |
| 64 | msg = "" | |
| 65 | } | |
| 66 | s.mrRedirect(w, r, msg) | |
| 62 | _, msg, code := s.runControlCode(u, mrArgs(r, "review", flag)) | |
| 63 | s.done(w, r, code, msg, s.mrRedirect) | |
| 67 | 64 | } |
| 68 | 65 | |
| 69 | 66 | func (s *Server) mrMergeSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| @@ -71,19 +68,13 @@ func (s *Server) mrMergeSubmit(w http.ResponseWriter, r *http.Request, u store.U | ||
| 71 | 68 | if st := strings.TrimSpace(r.FormValue("strategy")); st != "" && st != "auto" { |
| 72 | 69 | args = append(args, "--strategy", st) |
| 73 | 70 | } |
| 74 | _, msg, ok := s.runControl(u, mrArgs(r, "merge", args...)) | |
| 75 | if ok { | |
| 76 | msg = "" | |
| 77 | } | |
| 78 | s.mrRedirect(w, r, msg) | |
| 71 | _, msg, code := s.runControlCode(u, mrArgs(r, "merge", args...)) | |
| 72 | s.done(w, r, code, msg, s.mrRedirect) | |
| 79 | 73 | } |
| 80 | 74 | |
| 81 | 75 | func (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) | |
| 76 | _, msg, code := s.runControlCode(u, mrArgs(r, "close")) | |
| 77 | s.done(w, r, code, msg, s.mrRedirect) | |
| 87 | 78 | } |
| 88 | 79 | |
| 89 | 80 | // mrDiffCommentSubmit opens a review thread on a diff line, or replies to |
| @@ -114,11 +105,8 @@ func (s *Server) mrDiffCommentSubmit(w http.ResponseWriter, r *http.Request, u s | ||
| 114 | 105 | extra = append(extra, "--old") |
| 115 | 106 | } |
| 116 | 107 | } |
| 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) | |
| 108 | msg, code := s.runControlStdinCode(u, mrArgs(r, "diff-comment", append(extra, "--file", "-")...), body) | |
| 109 | s.done(w, r, code, msg, s.mrDiffRedirect) | |
| 122 | 110 | } |
| 123 | 111 | |
| 124 | 112 | // mrRetargetSubmit moves the merge request onto another branch. |
| @@ -128,11 +116,8 @@ func (s *Server) mrRetargetSubmit(w http.ResponseWriter, r *http.Request, u stor | ||
| 128 | 116 | s.mrRedirect(w, r, "pick a branch to retarget onto") |
| 129 | 117 | return |
| 130 | 118 | } |
| 131 | _, msg, ok := s.runControl(u, mrArgs(r, "retarget", target)) | |
| 132 | if ok { | |
| 133 | msg = "" | |
| 134 | } | |
| 135 | s.mrRedirect(w, r, msg) | |
| 119 | _, msg, code := s.runControlCode(u, mrArgs(r, "retarget", target)) | |
| 120 | s.done(w, r, code, msg, s.mrRedirect) | |
| 136 | 121 | } |
| 137 | 122 | |
| 138 | 123 | // mrThreadSubmit resolves or reopens one review thread. |
| @@ -146,11 +131,8 @@ func (s *Server) mrThreadSubmit(w http.ResponseWriter, r *http.Request, u store. | ||
| 146 | 131 | s.mrRedirect(w, r, "bad thread id") |
| 147 | 132 | return |
| 148 | 133 | } |
| 149 | _, msg, ok := s.runControl(u, mrArgs(r, verb, id)) | |
| 150 | if ok { | |
| 151 | msg = "" | |
| 152 | } | |
| 153 | s.mrRedirect(w, r, msg) | |
| 134 | _, msg, code := s.runControlCode(u, mrArgs(r, verb, id)) | |
| 135 | s.done(w, r, code, msg, s.mrRedirect) | |
| 154 | 136 | } |
| 155 | 137 | |
| 156 | 138 | // mrNewPage is the create form: branches to choose from, plus whatever |
internal/httpd/releaseactions.go +4 −10
| @@ -45,11 +45,8 @@ func (s *Server) releaseSubmit(w http.ResponseWriter, r *http.Request, u store.U | ||
| 45 | 45 | if notes != "" || verb == "edit" { |
| 46 | 46 | argv = append(argv, "--notes", notes) |
| 47 | 47 | } |
| 48 | _, msg, ok := s.runControl(u, argv) | |
| 49 | if ok { | |
| 50 | msg = "" | |
| 51 | } | |
| 52 | s.backTo(w, r, "releases", msg) | |
| 48 | _, msg, code := s.runControlCode(u, argv) | |
| 49 | s.done(w, r, code, msg, func(w http.ResponseWriter, r *http.Request, msg string) { s.backTo(w, r, "releases", msg) }) | |
| 53 | 50 | } |
| 54 | 51 | |
| 55 | 52 | func (s *Server) buildTriggerSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| @@ -59,9 +56,6 @@ func (s *Server) buildTriggerSubmit(w http.ResponseWriter, r *http.Request, u st | ||
| 59 | 56 | s.backTo(w, r, "builds", "pick a job") |
| 60 | 57 | return |
| 61 | 58 | } |
| 62 | _, msg, ok := s.runControl(u, []string{"build", "trigger", repo, job}) | |
| 63 | if ok { | |
| 64 | msg = "" | |
| 65 | } | |
| 66 | s.backTo(w, r, "builds", msg) | |
| 59 | _, msg, code := s.runControlCode(u, []string{"build", "trigger", repo, job}) | |
| 60 | s.done(w, r, code, msg, func(w http.ResponseWriter, r *http.Request, msg string) { s.backTo(w, r, "builds", msg) }) | |
| 67 | 61 | } |