Commit 4bfcb3893d
Verified · cmc ci/build: success ci/test: success ci/vuln: success
Layout: unified · split
internal/control/issue.go +2 −5
| @@ -153,7 +153,7 @@ func runIssueCreate(c *Ctx, args []string) int { | ||
| 153 | 153 | notifyUsers(c, targets, issueSubject(repo, n, title), |
| 154 | 154 | notifyBody(c, fmt.Sprintf("opened issue #%d", n), b, fmt.Sprintf("%s/issues/%d", repo.Path(), n))) |
| 155 | 155 | } |
| 156 | return c.emit(map[string]any{"number": n}, func(w io.Writer) { | |
| 156 | return c.emit(Created{Number: n}, func(w io.Writer) { | |
| 157 | 157 | fmt.Fprintf(w, "created %s#%d\n", repo.Path(), n) |
| 158 | 158 | }) |
| 159 | 159 | } |
| @@ -216,10 +216,7 @@ func runIssueShow(c *Ctx, args []string) int { | ||
| 216 | 216 | for _, cm := range comments { |
| 217 | 217 | cs = append(cs, commentOut{cm.Author, cm.Body, cm.BodyFormat, cm.CreatedAt}) |
| 218 | 218 | } |
| 219 | d := struct { | |
| 220 | issueOut | |
| 221 | Comments []commentOut `json:"comments,omitempty"` | |
| 222 | }{issueToOut(issue, true), cs} | |
| 219 | d := IssueShow{issueOut: issueToOut(issue, true), Comments: cs} | |
| 223 | 220 | _ = repo |
| 224 | 221 | return c.emit(d, func(w io.Writer) { |
| 225 | 222 | fmt.Fprintf(w, "#%d %s [%s] by %s\n", d.Number, d.Title, d.State, d.Author) |
internal/control/mr.go +12 −38
| @@ -281,16 +281,14 @@ func runMRCreate(c *Ctx, args []string) int { | ||
| 281 | 281 | notifyUsers(c, targets, mrSubject(repo, n, title), |
| 282 | 282 | notifyBody(c, fmt.Sprintf("opened merge request !%d (%s -> %s)", n, source, target), b, fmt.Sprintf("%s/mrs/%d", repo.Path(), n))) |
| 283 | 283 | } |
| 284 | out := map[string]any{"number": n, "head_sha": headSHA} | |
| 285 | var parent *stackRef | |
| 284 | out := MRCreated{Number: n, HeadSHA: headSHA} | |
| 286 | 285 | if p, ok, err := c.Store.OpenMRBySource(repo.ID, target); err == nil && ok { |
| 287 | parent = &stackRef{p.Number, p.Title} | |
| 288 | out["stacked_on"] = parent | |
| 286 | out.StackedOn = &stackRef{p.Number, p.Title} | |
| 289 | 287 | } |
| 290 | 288 | return c.emit(out, func(w io.Writer) { |
| 291 | 289 | fmt.Fprintf(w, "created %s!%d (%s -> %s)\n", repo.Path(), n, source, target) |
| 292 | if parent != nil { | |
| 293 | fmt.Fprintf(w, "stacked on !%d %s\n", parent.Number, parent.Title) | |
| 290 | if out.StackedOn != nil { | |
| 291 | fmt.Fprintf(w, "stacked on !%d %s\n", out.StackedOn.Number, out.StackedOn.Title) | |
| 294 | 292 | } |
| 295 | 293 | }) |
| 296 | 294 | } |
| @@ -449,22 +447,9 @@ func runMRShow(c *Ctx, args []string) int { | ||
| 449 | 447 | if err != nil { |
| 450 | 448 | return c.fail(protocol.ExitFailure, "%v", err) |
| 451 | 449 | } |
| 452 | type reviewOut struct { | |
| 453 | Reviewer string `json:"reviewer"` | |
| 454 | Verdict string `json:"verdict"` | |
| 455 | Stale bool `json:"stale"` | |
| 456 | CreatedAt string `json:"created_at"` | |
| 457 | } | |
| 458 | type checkOut struct { | |
| 459 | Context string `json:"context"` | |
| 460 | State string `json:"state"` | |
| 461 | URL string `json:"url,omitempty"` | |
| 462 | UpdatedAt string `json:"updated_at"` | |
| 463 | Duration string `json:"duration,omitempty"` // CI checks only, once finished | |
| 464 | } | |
| 465 | var checks []checkOut | |
| 450 | var checks []CheckOut | |
| 466 | 451 | for _, st := range statuses { |
| 467 | out := checkOut{Context: st.Context, State: st.State, URL: st.TargetURL, UpdatedAt: st.UpdatedAt} | |
| 452 | out := CheckOut{Context: st.Context, State: st.State, URL: st.TargetURL, UpdatedAt: st.UpdatedAt} | |
| 468 | 453 | if st.Duration > 0 { |
| 469 | 454 | out.Duration = st.Duration.String() |
| 470 | 455 | } |
| @@ -474,16 +459,12 @@ func runMRShow(c *Ctx, args []string) int { | ||
| 474 | 459 | for _, cm := range comments { |
| 475 | 460 | cs = append(cs, commentOut{cm.Author, cm.Body, cm.BodyFormat, cm.CreatedAt}) |
| 476 | 461 | } |
| 477 | var rs []reviewOut | |
| 462 | var rs []ReviewOut | |
| 478 | 463 | for _, r := range reviews { |
| 479 | rs = append(rs, reviewOut{r.Reviewer, r.Verdict, r.Stale, r.CreatedAt}) | |
| 464 | rs = append(rs, ReviewOut{r.Reviewer, r.Verdict, r.Stale, r.CreatedAt}) | |
| 480 | 465 | } |
| 481 | 466 | // The commits this MR carries: base..head, the diff's range. |
| 482 | type commitOut struct { | |
| 483 | SHA string `json:"sha"` | |
| 484 | Subject string `json:"subject"` | |
| 485 | } | |
| 486 | var commits []commitOut | |
| 467 | var commits []CommitOut | |
| 487 | 468 | dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name) |
| 488 | 469 | base := mr.MergedBase |
| 489 | 470 | if base == "" { |
| @@ -500,19 +481,12 @@ func runMRShow(c *Ctx, args []string) int { | ||
| 500 | 481 | subject = parsed.Subject |
| 501 | 482 | } |
| 502 | 483 | } |
| 503 | commits = append(commits, commitOut{sha, subject}) | |
| 484 | commits = append(commits, CommitOut{sha, subject}) | |
| 504 | 485 | } |
| 505 | 486 | } |
| 506 | 487 | } |
| 507 | d := struct { | |
| 508 | mrOut | |
| 509 | Checks []checkOut `json:"checks,omitempty"` | |
| 510 | Combined string `json:"checks_combined,omitempty"` | |
| 511 | UnresolvedThreads int `json:"unresolved_threads,omitempty"` | |
| 512 | Commits []commitOut `json:"commits,omitempty"` | |
| 513 | Comments []commentOut `json:"comments,omitempty"` | |
| 514 | Reviews []reviewOut `json:"reviews,omitempty"` | |
| 515 | }{mrToOut(repo, mr, true), checks, combined, unresolved, commits, cs, rs} | |
| 488 | d := MRShow{mrOut: mrToOut(repo, mr, true), Checks: checks, Combined: combined, | |
| 489 | UnresolvedThreads: unresolved, Commits: commits, Comments: cs, Reviews: rs} | |
| 516 | 490 | d.StackedOn, d.Stacked = stackOf(c, repo, mr) |
| 517 | 491 | return c.emit(d, func(w io.Writer) { |
| 518 | 492 | fmt.Fprintf(w, "!%d %s [%s] by %s\n%s -> %s @ %.10s\n", d.Number, d.Title, d.State, d.Author, d.Source, d.TargetRef, d.HeadSHA) |
internal/control/output.go added +57
| @@ -0,0 +1,57 @@ | ||
| 1 | package control | |
| 2 | ||
| 3 | // Named payloads for the commands other surfaces decode. A command used | |
| 4 | // to declare its output inline, so the web type-asserted map keys and | |
| 5 | // nothing tied what a command emits to what a client reads (#126). | |
| 6 | ||
| 7 | // Created is what issue create, issue comment and mr comment emit. | |
| 8 | type Created struct { | |
| 9 | Number int64 `json:"number"` | |
| 10 | } | |
| 11 | ||
| 12 | // MRCreated is what mr create emits. | |
| 13 | type MRCreated struct { | |
| 14 | Number int64 `json:"number"` | |
| 15 | HeadSHA string `json:"head_sha"` | |
| 16 | StackedOn *stackRef `json:"stacked_on,omitempty"` | |
| 17 | } | |
| 18 | ||
| 19 | // IssueShow is issue show's payload: the issue and its comments. | |
| 20 | type IssueShow struct { | |
| 21 | issueOut | |
| 22 | Comments []commentOut `json:"comments,omitempty"` | |
| 23 | } | |
| 24 | ||
| 25 | // MRShow is mr show's payload. | |
| 26 | type MRShow struct { | |
| 27 | mrOut | |
| 28 | Checks []CheckOut `json:"checks,omitempty"` | |
| 29 | Combined string `json:"checks_combined,omitempty"` | |
| 30 | UnresolvedThreads int `json:"unresolved_threads,omitempty"` | |
| 31 | Commits []CommitOut `json:"commits,omitempty"` | |
| 32 | Comments []commentOut `json:"comments,omitempty"` | |
| 33 | Reviews []ReviewOut `json:"reviews,omitempty"` | |
| 34 | } | |
| 35 | ||
| 36 | // ReviewOut is one review on a merge request. | |
| 37 | type ReviewOut struct { | |
| 38 | Reviewer string `json:"reviewer"` | |
| 39 | Verdict string `json:"verdict"` | |
| 40 | Stale bool `json:"stale"` | |
| 41 | CreatedAt string `json:"created_at"` | |
| 42 | } | |
| 43 | ||
| 44 | // CheckOut is one commit status on a merge request head. | |
| 45 | type CheckOut struct { | |
| 46 | Context string `json:"context"` | |
| 47 | State string `json:"state"` | |
| 48 | URL string `json:"url,omitempty"` | |
| 49 | UpdatedAt string `json:"updated_at"` | |
| 50 | Duration string `json:"duration,omitempty"` // CI checks only, once finished | |
| 51 | } | |
| 52 | ||
| 53 | // CommitOut is one commit a merge request carries. | |
| 54 | type CommitOut struct { | |
| 55 | SHA string `json:"sha"` | |
| 56 | Subject string `json:"subject"` | |
| 57 | } | |
internal/control/thread.go +1 −1
| @@ -110,7 +110,7 @@ func runComment(c *Ctx, args []string, t thread, noun string, | ||
| 110 | 110 | notifyUsers(c, parts, subject, |
| 111 | 111 | notifyBody(c, fmt.Sprintf("commented on %s%d", t.symbol, number), body, fmt.Sprintf("%s/%s/%d", repo.Path(), t.segment, number))) |
| 112 | 112 | } |
| 113 | return c.emit(map[string]any{"number": number}, func(w io.Writer) { | |
| 113 | return c.emit(Created{Number: number}, func(w io.Writer) { | |
| 114 | 114 | fmt.Fprintf(w, "commented on %s%s%d\n", repo.Path(), t.symbol, number) |
| 115 | 115 | }) |
| 116 | 116 | } |
internal/httpd/accounts.go +3 −2
| @@ -268,12 +268,13 @@ func (s *Server) issueCreateForm(w http.ResponseWriter, r *http.Request, u store | ||
| 268 | 268 | func (s *Server) issueCreateSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| 269 | 269 | repoPath := r.PathValue("owner") + "/" + r.PathValue("repo") |
| 270 | 270 | title := strings.TrimSpace(r.FormValue("title")) |
| 271 | code, data, msg := s.dispatchJSON(u, []string{"issue", "create", repoPath, "--title", title, "--file", "-"}, r.FormValue("body")) | |
| 271 | var created control.Created | |
| 272 | code, msg := s.dispatchIntoStdin(u, []string{"issue", "create", repoPath, "--title", title, "--file", "-"}, r.FormValue("body"), &created) | |
| 272 | 273 | if code != protocol.ExitOK { |
| 273 | 274 | http.Error(w, msg, statusForExit(code)) |
| 274 | 275 | return |
| 275 | 276 | } |
| 276 | n := int64(data["number"].(float64)) | |
| 277 | n := created.Number | |
| 277 | 278 | // Labels need write access, matching the SSH rule; the command refuses |
| 278 | 279 | // otherwise and the issue stands without them. |
| 279 | 280 | if args := fieldArgs("--add", r.FormValue("labels")); len(args) > 0 { |
internal/httpd/control.go +7 −1
| @@ -113,6 +113,12 @@ func (s *Server) runControlIntoCode(u store.User, argv []string, target any) (co | ||
| 113 | 113 | } |
| 114 | 114 | |
| 115 | 115 | func (s *Server) dispatchInto(u store.User, argv []string, target any) (int, string) { |
| 116 | return s.dispatchIntoStdin(u, argv, "", target) | |
| 117 | } | |
| 118 | ||
| 119 | // dispatchIntoStdin is dispatchInto with a body on stdin, decoding the | |
| 120 | // command's named payload rather than a map (#126). | |
| 121 | func (s *Server) dispatchIntoStdin(u store.User, argv []string, stdin string, target any) (int, string) { | |
| 116 | 122 | var stdout, stderr bytes.Buffer |
| 117 | 123 | ctx := &control.Ctx{ |
| 118 | 124 | User: u, |
| @@ -120,7 +126,7 @@ func (s *Server) dispatchInto(u store.User, argv []string, target any) (int, str | ||
| 120 | 126 | Scope: "full", |
| 121 | 127 | Store: s.st, |
| 122 | 128 | Cfg: s.cfg, |
| 123 | Stdin: strings.NewReader(""), | |
| 129 | Stdin: strings.NewReader(stdin), | |
| 124 | 130 | Stdout: &stdout, |
| 125 | 131 | Stderr: &stderr, |
| 126 | 132 | JSON: true, |
internal/httpd/mractions.go +4 −4
| @@ -7,6 +7,7 @@ import ( | ||
| 7 | 7 | "strconv" |
| 8 | 8 | "strings" |
| 9 | 9 | |
| 10 | "gitbay.org/gitbay/internal/control" | |
| 10 | 11 | "gitbay.org/gitbay/internal/gitutil" |
| 11 | 12 | "gitbay.org/gitbay/internal/store" |
| 12 | 13 | ) |
| @@ -191,11 +192,10 @@ func (s *Server) mrCreateSubmit(w http.ResponseWriter, r *http.Request, u store. | ||
| 191 | 192 | if body != "" { |
| 192 | 193 | argv = append(argv, "--body", body) |
| 193 | 194 | } |
| 194 | data, msg, ok := s.runControlJSON(u, argv) | |
| 195 | if !ok { | |
| 195 | var created control.MRCreated | |
| 196 | if msg, ok := s.runControlInto(u, argv, &created); !ok { | |
| 196 | 197 | back(msg) |
| 197 | 198 | return |
| 198 | 199 | } |
| 199 | n, _ := data["number"].(float64) | |
| 200 | http.Redirect(w, r, fmt.Sprintf("/%s/mrs/%d", p.Repo.Path(), int64(n)), http.StatusSeeOther) | |
| 200 | http.Redirect(w, r, fmt.Sprintf("/%s/mrs/%d", p.Repo.Path(), created.Number), http.StatusSeeOther) | |
| 201 | 201 | } |