Eric Bower
·
2025-03-28
cli.go
1package git
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "strconv"
8 "strings"
9 "text/tabwriter"
10
11 "github.com/charmbracelet/ssh"
12 "github.com/charmbracelet/wish"
13 "github.com/urfave/cli/v2"
14)
15
16func NewTabWriter(out io.Writer) *tabwriter.Writer {
17 return tabwriter.NewWriter(out, 0, 0, 1, ' ', tabwriter.TabIndent)
18}
19
20func strToInt(str string) (int64, error) {
21 prID, err := strconv.ParseInt(str, 10, 64)
22 return prID, err
23}
24
25func getPatchsetFromOpt(patchsets []*Patchset, optPatchsetID string) (*Patchset, error) {
26 if optPatchsetID == "" {
27 return patchsets[len(patchsets)-1], nil
28 }
29
30 id, err := getPatchsetID(optPatchsetID)
31 if err != nil {
32 return nil, err
33 }
34
35 for _, ps := range patchsets {
36 if ps.ID == id {
37 return ps, nil
38 }
39 }
40
41 return nil, fmt.Errorf("cannot find patchset: %s", optPatchsetID)
42}
43
44func printPatches(sesh ssh.Session, patches []*Patch) {
45 if len(patches) == 1 {
46 wish.Println(sesh, patches[0].RawText)
47 return
48 }
49
50 opatches := patches
51 for idx, patch := range opatches {
52 wish.Println(sesh, patch.RawText)
53 if idx < len(patches)-1 {
54 wish.Printf(sesh, "\n\n\n")
55 }
56 }
57}
58
59func prSummary(be *Backend, pr GitPatchRequest, sesh ssh.Session, prID int64) error {
60 request, err := pr.GetPatchRequestByID(prID)
61 if err != nil {
62 return err
63 }
64
65 repo, err := pr.GetRepoByID(request.RepoID)
66 if err != nil {
67 return err
68 }
69
70 repoUser, err := pr.GetUserByID(repo.UserID)
71 if err != nil {
72 return err
73 }
74
75 wish.Printf(sesh, "Info\n====\n")
76 wish.Printf(sesh, "URL: https://%s/prs/%d\n", be.Cfg.Url, prID)
77 wish.Printf(sesh, "Repo: %s\n\n", be.CreateRepoNs(repoUser.Name, repo.Name))
78
79 writer := NewTabWriter(sesh)
80 fmt.Fprintln(writer, "ID\tName\tStatus\tDate")
81 fmt.Fprintf(
82 writer,
83 "%d\t%s\t[%s]\t%s\n",
84 request.ID, request.Name, request.Status, request.CreatedAt.Format(be.Cfg.TimeFormat),
85 )
86 writer.Flush()
87
88 patchsets, err := pr.GetPatchsetsByPrID(prID)
89 if err != nil {
90 return err
91 }
92
93 wish.Printf(sesh, "\nPatchsets\n====\n")
94
95 writerSet := NewTabWriter(sesh)
96 fmt.Fprintln(writerSet, "ID\tType\tUser\tDate")
97 for _, patchset := range patchsets {
98 user, err := pr.GetUserByID(patchset.UserID)
99 if err != nil {
100 be.Logger.Error("cannot find user for patchset", "err", err)
101 continue
102 }
103 isReview := ""
104 if patchset.Review {
105 isReview = "[review]"
106 }
107
108 fmt.Fprintf(
109 writerSet,
110 "%s\t%s\t%s\t%s\n",
111 getFormattedPatchsetID(patchset.ID),
112 isReview,
113 user.Name,
114 patchset.CreatedAt.Format(be.Cfg.TimeFormat),
115 )
116 }
117 writerSet.Flush()
118
119 latest, err := getPatchsetFromOpt(patchsets, "")
120 if err != nil {
121 return err
122 }
123
124 patches, err := pr.GetPatchesByPatchsetID(latest.ID)
125 if err != nil {
126 return err
127 }
128
129 wish.Printf(sesh, "\nPatches from latest patchset\n====\n")
130
131 opatches := patches
132 w := NewTabWriter(sesh)
133 fmt.Fprintln(w, "Idx\tTitle\tCommit\tAuthor\tDate")
134 for idx, patch := range opatches {
135 timestamp := patch.AuthorDate.Format(be.Cfg.TimeFormat)
136 fmt.Fprintf(
137 w,
138 "%d\t%s\t%s\t%s <%s>\t%s\n",
139 idx,
140 patch.Title,
141 truncateSha(patch.CommitSha),
142 patch.AuthorName,
143 patch.AuthorEmail,
144 timestamp,
145 )
146 }
147 w.Flush()
148 return nil
149}
150
151func printPatchsetFromID(sesh ssh.Session, pr GitPatchRequest, psID int64) error {
152 patches, err := pr.GetPatchesByPatchsetID(psID)
153 if err != nil {
154 return err
155 }
156 printPatches(sesh, patches)
157 return nil
158}
159
160func printPatchsetFromPrID(sesh ssh.Session, pr GitPatchRequest, prID int64) error {
161 patchsets, err := pr.GetPatchsetsByPrID(prID)
162 if err != nil {
163 return err
164 }
165 ps := patchsets[len(patchsets)-1]
166 patches, err := pr.GetPatchesByPatchsetID(ps.ID)
167 if err != nil {
168 return err
169 }
170
171 printPatches(sesh, patches)
172 return nil
173}
174
175func NewCli(sesh ssh.Session, be *Backend, pr GitPatchRequest) *cli.App {
176 desc := fmt.Sprintf(`git-pr: A pastebin supercharged for git collaboration.
177
178Here's how it works:
179 - External contributor clones repo (git-clone)
180 - External contributor makes a code change (git-add & git-commit)
181 - External contributor generates patches (git-format-patch)
182 - External contributor submits a PR to SSH server
183 - Owner receives RSS notification that there's a new PR
184 - Owner applies patches locally (git-am) from SSH server
185 - Owner makes suggestions in code! (git-add & git-commit)
186 - Owner submits review by piping patch to SSH server (git-format-patch)
187 - External contributor receives RSS notification of the PR review
188 - External contributor re-applies patches (git-am)
189 - External contributor reviews and removes comments in code!
190 - External contributor submits another patch (git-format-patch)
191 - Owner applies patches locally (git-am)
192 - Owner marks PR as accepted and pushes code to main (git-push)
193
194To get started, submit a new patch request:
195 git format-patch main --stdout | ssh %s pr create {repo}
196`, be.Cfg.Url)
197
198 pubkey := be.Pubkey(sesh.PublicKey())
199 userName := sesh.User()
200 app := &cli.App{
201 Name: "ssh",
202 Description: desc,
203 Usage: "Collaborate with contributors for your git project",
204 Writer: sesh,
205 ErrWriter: sesh,
206 ExitErrHandler: func(cCtx *cli.Context, err error) {
207 if err != nil {
208 wish.Fatalln(sesh, fmt.Errorf("err: %w", err))
209 }
210 },
211 OnUsageError: func(cCtx *cli.Context, err error, isSubcommand bool) error {
212 if err != nil {
213 wish.Fatalln(sesh, fmt.Errorf("err: %w", err))
214 }
215 return nil
216 },
217 Commands: []*cli.Command{
218 {
219 Name: "logs",
220 Usage: "List event logs with filters",
221 Args: true,
222 Flags: []cli.Flag{
223 &cli.Int64Flag{
224 Name: "pr",
225 Usage: "show all events related to the provided patch request",
226 },
227 &cli.BoolFlag{
228 Name: "pubkey",
229 Usage: "show all events related to your pubkey",
230 },
231 &cli.StringFlag{
232 Name: "repo",
233 Usage: "show all events related to a repo",
234 },
235 },
236 Action: func(cCtx *cli.Context) error {
237 pubkey := be.Pubkey(sesh.PublicKey())
238 user, err := pr.GetUserByPubkey(pubkey)
239 if err != nil {
240 return err
241 }
242 isPubkey := cCtx.Bool("pubkey")
243 prID := cCtx.Int64("pr")
244 repoNs := cCtx.String("repo")
245 var eventLogs []*EventLog
246 if isPubkey {
247 eventLogs, err = pr.GetEventLogsByUserID(user.ID)
248 } else if prID != 0 {
249 eventLogs, err = pr.GetEventLogsByPrID(prID)
250 } else if repoNs != "" {
251 repoUsername, repoName := be.SplitRepoNs(repoNs)
252 var repoUser *User
253 repoUser, err = pr.GetUserByName(repoUsername)
254 if err != nil {
255 return nil
256 }
257 eventLogs, err = pr.GetEventLogsByRepoName(repoUser, repoName)
258 } else {
259 eventLogs, err = pr.GetEventLogs()
260 }
261 if err != nil {
262 return err
263 }
264
265 writer := NewTabWriter(sesh)
266 fmt.Fprintln(writer, "RepoID\tPrID\tPatchsetID\tEvent\tCreated\tData")
267 for _, eventLog := range eventLogs {
268 repo, err := pr.GetRepoByID(eventLog.RepoID.Int64)
269 if err != nil {
270 be.Logger.Error("repo not found", "repo", repo, "err", err)
271 continue
272 }
273 repoUser, err := pr.GetUserByID(repo.UserID)
274 if err != nil {
275 be.Logger.Error("repo user not found", "repo", repo, "err", err)
276 continue
277 }
278 fmt.Fprintf(
279 writer,
280 "%s\t%d\t%s\t%s\t%s\t%s\n",
281 be.CreateRepoNs(repoUser.Name, repo.Name),
282 eventLog.PatchRequestID.Int64,
283 getFormattedPatchsetID(eventLog.PatchsetID.Int64),
284 eventLog.Event,
285 eventLog.CreatedAt.Format(be.Cfg.TimeFormat),
286 eventLog.Data,
287 )
288 }
289 writer.Flush()
290 return nil
291 },
292 },
293 {
294 Name: "ps",
295 Usage: "Mange patchsets",
296 Subcommands: []*cli.Command{
297 {
298 Name: "rm",
299 Usage: "Remove a patchset with its patches",
300 Args: true,
301 ArgsUsage: "[patchsetID]",
302 Action: func(cCtx *cli.Context) error {
303 args := cCtx.Args()
304 if !args.Present() {
305 return fmt.Errorf("must provide a patchset ID")
306 }
307
308 patchsetID, err := getPatchsetID(args.First())
309 if err != nil {
310 return err
311 }
312
313 patchset, err := pr.GetPatchsetByID(patchsetID)
314 if err != nil {
315 return err
316 }
317
318 user, err := pr.GetUserByID(patchset.UserID)
319 if err != nil {
320 return err
321 }
322
323 pk := sesh.PublicKey()
324 isAdmin := be.IsAdmin(pk)
325 isContrib := pubkey == user.Pubkey
326 if !isAdmin && !isContrib {
327 return fmt.Errorf("you are not authorized to delete a patchset")
328 }
329
330 err = pr.DeletePatchsetByID(user.ID, patchset.PatchRequestID, patchsetID)
331 if err != nil {
332 return err
333 }
334 wish.Printf(sesh, "successfully removed patchset: %d\n", patchsetID)
335 return nil
336 },
337 },
338 },
339 },
340 {
341 Name: "repo",
342 Usage: "Manage repos",
343 Subcommands: []*cli.Command{
344 {
345 Name: "create",
346 Usage: "Create a new repo",
347 Args: true,
348 ArgsUsage: "[repoName]",
349 Action: func(cCtx *cli.Context) error {
350 user, err := pr.UpsertUser(pubkey, userName)
351 if err != nil {
352 return err
353 }
354
355 args := cCtx.Args()
356 if !args.Present() {
357 return fmt.Errorf("need repo name argument")
358 }
359 repoName := args.First()
360 repo, _ := pr.GetRepoByName(user, repoName)
361 err = be.CanCreateRepo(repo, user)
362 if err != nil {
363 return err
364 }
365
366 if repo == nil {
367 repo, err = pr.CreateRepo(user, repoName)
368 if err != nil {
369 return err
370 }
371 }
372
373 wish.Printf(sesh, "repo created: %s/%s", user.Name, repo.Name)
374 return nil
375 },
376 },
377 },
378 },
379 {
380 Name: "print",
381 Usage: "Print patches in a patchset",
382 Args: true,
383 ArgsUsage: "[pr-X] or [ps-X]",
384 Action: func(cCtx *cli.Context) error {
385 args := cCtx.Args()
386 raw := args.First()
387 split := strings.Split(raw, "-")
388 if len(split) < 2 {
389 return fmt.Errorf("must provide ID in format: pr-X, ps-X")
390 }
391
392 prefix := split[0]
393 id, err := strToInt(split[1])
394 if err != nil {
395 return err
396 }
397
398 switch prefix {
399 case "pr":
400 err = printPatchsetFromPrID(sesh, pr, id)
401 case "ps":
402 err = printPatchsetFromID(sesh, pr, id)
403 }
404
405 return err
406 },
407 },
408 {
409 Name: "pr",
410 Usage: "Manage Patch Requests (PR)",
411 Subcommands: []*cli.Command{
412 {
413 Name: "ls",
414 Usage: "List all PRs",
415 Args: true,
416 ArgsUsage: "[repoName]",
417 Flags: []cli.Flag{
418 &cli.BoolFlag{
419 Name: "open",
420 Usage: "only show open PRs",
421 },
422 &cli.BoolFlag{
423 Name: "closed",
424 Usage: "only show closed PRs",
425 },
426 &cli.BoolFlag{
427 Name: "accepted",
428 Usage: "only show accepted PRs",
429 },
430 &cli.BoolFlag{
431 Name: "mine",
432 Usage: "only show your own PRs",
433 },
434 },
435 Action: func(cCtx *cli.Context) error {
436 args := cCtx.Args()
437 rawRepoNs := args.First()
438 userName, repoName := be.SplitRepoNs(rawRepoNs)
439 var prs []*PatchRequest
440 var err error
441 if repoName == "" {
442 prs, err = pr.GetPatchRequests()
443 if err != nil {
444 return err
445 }
446 } else {
447 user, err := pr.GetUserByName(userName)
448 if err != nil {
449 return err
450 }
451 repo, err := pr.GetRepoByName(user, repoName)
452 if err != nil {
453 return err
454 }
455 prs, err = pr.GetPatchRequestsByRepoID(repo.ID)
456 if err != nil {
457 return err
458 }
459 }
460
461 onlyOpen := cCtx.Bool("open")
462 onlyAccepted := cCtx.Bool("accepted")
463 onlyClosed := cCtx.Bool("closed")
464 onlyMine := cCtx.Bool("mine")
465
466 writer := NewTabWriter(sesh)
467 fmt.Fprintln(writer, "ID\tRepoID\tName\tStatus\tPatchsets\tUser\tDate")
468 for _, req := range prs {
469 if onlyAccepted && req.Status != "accepted" {
470 continue
471 }
472
473 if onlyClosed && req.Status != "closed" {
474 continue
475 }
476
477 if onlyOpen && req.Status != "open" {
478 continue
479 }
480
481 user, err := pr.GetUserByID(req.UserID)
482 if err != nil {
483 be.Logger.Error("could not get user for pr", "err", err)
484 continue
485 }
486
487 if onlyMine && user.Name != userName {
488 continue
489 }
490
491 patchsets, err := pr.GetPatchsetsByPrID(req.ID)
492 if err != nil {
493 be.Logger.Error("could not get patchsets for pr", "err", err)
494 continue
495 }
496
497 repo, err := pr.GetRepoByID(req.RepoID)
498 if err != nil {
499 be.Logger.Error("could not get repo for pr", "err", err)
500 continue
501 }
502
503 repoUser, err := pr.GetUserByID(repo.UserID)
504 if err != nil {
505 be.Logger.Error("could not get repo user for pr", "err", err)
506 continue
507 }
508
509 fmt.Fprintf(
510 writer,
511 "%d\t%s\t%s\t[%s]\t%d\t%s\t%s\n",
512 req.ID,
513 be.CreateRepoNs(repoUser.Name, repo.Name),
514 req.Name,
515 req.Status,
516 len(patchsets),
517 user.Name,
518 req.CreatedAt.Format(be.Cfg.TimeFormat),
519 )
520 }
521 writer.Flush()
522 return nil
523 },
524 },
525 {
526 Name: "create",
527 Usage: "Submit a new PR",
528 Args: true,
529 ArgsUsage: "[repoName]",
530 Action: func(cCtx *cli.Context) error {
531 user, err := pr.UpsertUser(pubkey, userName)
532 if err != nil {
533 return err
534 }
535
536 args := cCtx.Args()
537 rawRepoNs := "bin"
538 if args.Present() {
539 rawRepoNs = args.First()
540 }
541 repoUsername, repoName := be.SplitRepoNs(rawRepoNs)
542 var repo *Repo
543 if repoUsername == "" {
544 if be.Cfg.CreateRepo == "admin" {
545 // single tenant default user to admin
546 repo, _ = pr.GetRepoByName(nil, repoName)
547 } else {
548 // multi tenant default user to contributor
549 repo, _ = pr.GetRepoByName(user, repoName)
550 }
551 } else {
552 repoUser, err := pr.GetUserByName(repoUsername)
553 if err != nil {
554 return err
555 }
556 repo, _ = pr.GetRepoByName(repoUser, repoName)
557 }
558
559 err = be.CanCreateRepo(repo, user)
560 if err != nil {
561 return err
562 }
563
564 if repo == nil {
565 repo, err = pr.CreateRepo(user, repoName)
566 if err != nil {
567 return err
568 }
569 }
570
571 prq, err := pr.SubmitPatchRequest(repo.ID, user.ID, sesh)
572 if err != nil {
573 return err
574 }
575 wish.Println(
576 sesh,
577 "PR submitted! Use the ID for interacting with this PR.",
578 )
579
580 return prSummary(be, pr, sesh, prq.ID)
581 },
582 },
583 {
584 Name: "summary",
585 Usage: "Display metadata related to a PR",
586 Args: true,
587 ArgsUsage: "[prID]",
588 Action: func(cCtx *cli.Context) error {
589 args := cCtx.Args()
590 if !args.Present() {
591 return fmt.Errorf("must provide a patch request ID")
592 }
593
594 prID, err := strToInt(args.First())
595 if err != nil {
596 return err
597 }
598 return prSummary(be, pr, sesh, prID)
599 },
600 },
601 {
602 Name: "accept",
603 Usage: "Accept a PR",
604 Args: true,
605 ArgsUsage: "[prID], [prID]...",
606 Action: func(cCtx *cli.Context) error {
607 args := cCtx.Args()
608 if !args.Present() {
609 return fmt.Errorf("must provide at least one patch request ID")
610 }
611
612 prIDs := args.Tail()
613 prIDs = append(prIDs, args.First())
614
615 var errs error
616 for _, prIDStr := range prIDs {
617 prID, err := strToInt(prIDStr)
618 if err != nil {
619 wish.Errorln(sesh, err)
620 continue
621 }
622
623 prq, err := pr.GetPatchRequestByID(prID)
624 if err != nil {
625 return err
626 }
627
628 user, err := pr.UpsertUser(pubkey, userName)
629 if err != nil {
630 return err
631 }
632
633 repo, err := pr.GetRepoByID(prq.RepoID)
634 if err != nil {
635 return err
636 }
637
638 acl := be.GetPatchRequestAcl(repo, prq, user)
639 if !acl.CanReview {
640 return fmt.Errorf("you are not authorized to accept a PR")
641 }
642
643 if prq.Status == "accepted" {
644 return fmt.Errorf("PR has already been accepted")
645 }
646
647 err = pr.UpdatePatchRequestStatus(prID, user.ID, "accepted")
648 if err != nil {
649 return err
650 }
651 wish.Printf(sesh, "Accepted PR %s (#%d)\n", prq.Name, prq.ID)
652 err = prSummary(be, pr, sesh, prID)
653 if err != nil {
654 errs = errors.Join(errs, err)
655 }
656 wish.Printf(sesh, "\n\n")
657 }
658
659 return errs
660 },
661 },
662 {
663 Name: "close",
664 Usage: "Close a PR",
665 Args: true,
666 ArgsUsage: "[prID], [prID]...",
667 Action: func(cCtx *cli.Context) error {
668 args := cCtx.Args()
669 if !args.Present() {
670 return fmt.Errorf("must provide a patch request ID")
671 }
672
673 prIDs := args.Tail()
674 prIDs = append(prIDs, args.First())
675
676 var errs error
677 for _, prIDStr := range prIDs {
678 prID, err := strToInt(prIDStr)
679 if err != nil {
680 wish.Errorln(sesh, err)
681 continue
682 }
683
684 prq, err := pr.GetPatchRequestByID(prID)
685 if err != nil {
686 return err
687 }
688
689 patchUser, err := pr.GetUserByID(prq.UserID)
690 if err != nil {
691 return err
692 }
693
694 repo, err := pr.GetRepoByID(prq.RepoID)
695 if err != nil {
696 return err
697 }
698
699 acl := be.GetPatchRequestAcl(repo, prq, patchUser)
700 if !acl.CanModify {
701 return fmt.Errorf("you are not authorized to change PR status")
702 }
703
704 if prq.Status == "closed" {
705 return fmt.Errorf("PR has already been closed")
706 }
707
708 user, err := pr.UpsertUser(pubkey, userName)
709 if err != nil {
710 return err
711 }
712
713 err = pr.UpdatePatchRequestStatus(prID, user.ID, "closed")
714 if err != nil {
715 return err
716 }
717 wish.Printf(sesh, "Closed PR %s (#%d)\n", prq.Name, prq.ID)
718 err = prSummary(be, pr, sesh, prID)
719 if err != nil {
720 errs = errors.Join(errs, err)
721 }
722 wish.Printf(sesh, "\n\n")
723 }
724 return errs
725 },
726 },
727 {
728 Name: "reopen",
729 Usage: "Reopen a PR",
730 Args: true,
731 ArgsUsage: "[prID]",
732 Action: func(cCtx *cli.Context) error {
733 args := cCtx.Args()
734 if !args.Present() {
735 return fmt.Errorf("must provide a patch request ID")
736 }
737
738 prID, err := strToInt(args.First())
739 if err != nil {
740 return err
741 }
742
743 prq, err := pr.GetPatchRequestByID(prID)
744 if err != nil {
745 return err
746 }
747
748 patchUser, err := pr.GetUserByID(prq.UserID)
749 if err != nil {
750 return err
751 }
752
753 repo, err := pr.GetRepoByID(prq.RepoID)
754 if err != nil {
755 return err
756 }
757
758 acl := be.GetPatchRequestAcl(repo, prq, patchUser)
759 if !acl.CanModify {
760 return fmt.Errorf("you are not authorized to change PR status")
761 }
762
763 if prq.Status == "open" {
764 return fmt.Errorf("PR is already open")
765 }
766
767 user, err := pr.UpsertUser(pubkey, userName)
768 if err != nil {
769 return err
770 }
771
772 err = pr.UpdatePatchRequestStatus(prID, user.ID, "open")
773 if err == nil {
774 wish.Printf(sesh, "Reopened PR %s (#%d)\n", prq.Name, prq.ID)
775 }
776 return prSummary(be, pr, sesh, prID)
777 },
778 },
779 {
780 Name: "edit",
781 Usage: "Edit PR title",
782 Args: true,
783 ArgsUsage: "[prID] [title]",
784 Action: func(cCtx *cli.Context) error {
785 args := cCtx.Args()
786 if !args.Present() {
787 return fmt.Errorf("must provide a patch request ID")
788 }
789
790 prID, err := strToInt(args.First())
791 if err != nil {
792 return err
793 }
794 prq, err := pr.GetPatchRequestByID(prID)
795 if err != nil {
796 return err
797 }
798
799 user, err := pr.UpsertUser(pubkey, userName)
800 if err != nil {
801 return err
802 }
803
804 repo, err := pr.GetRepoByID(prq.RepoID)
805 if err != nil {
806 return err
807 }
808
809 acl := be.GetPatchRequestAcl(repo, prq, user)
810 if !acl.CanModify {
811 return fmt.Errorf("you are not authorized to change PR")
812 }
813
814 tail := cCtx.Args().Tail()
815 title := strings.Join(tail, " ")
816 if title == "" {
817 return fmt.Errorf("must provide title")
818 }
819
820 err = pr.UpdatePatchRequestName(
821 prID,
822 user.ID,
823 title,
824 )
825 if err == nil {
826 wish.Printf(sesh, "New title: %s (%d)\n", title, prq.ID)
827 }
828
829 return err
830 },
831 },
832 {
833 Name: "add",
834 Usage: "Add a new patchset to a PR",
835 Args: true,
836 ArgsUsage: "[prID]",
837 Flags: []cli.Flag{
838 &cli.BoolFlag{
839 Name: "review",
840 Usage: "submit patchset mark it as a review",
841 },
842 &cli.BoolFlag{
843 Name: "accept",
844 Usage: "submit patchset and mark PR as accepted",
845 },
846 &cli.BoolFlag{
847 Name: "close",
848 Usage: "submit patchset and mark PR as closed",
849 },
850 },
851 Action: func(cCtx *cli.Context) error {
852 args := cCtx.Args()
853 if !args.Present() {
854 return fmt.Errorf("must provide a patch request ID")
855 }
856
857 prID, err := strToInt(args.First())
858 if err != nil {
859 return err
860 }
861 prq, err := pr.GetPatchRequestByID(prID)
862 if err != nil {
863 return err
864 }
865
866 user, err := pr.UpsertUser(pubkey, userName)
867 if err != nil {
868 return err
869 }
870
871 isReview := cCtx.Bool("review")
872 isAccept := cCtx.Bool("accept")
873 isClose := cCtx.Bool("close")
874
875 repo, err := pr.GetRepoByID(prq.RepoID)
876 if err != nil {
877 return err
878 }
879
880 acl := be.GetPatchRequestAcl(repo, prq, user)
881 if !acl.CanAddPatchset {
882 return fmt.Errorf("you are not authorized to add patchsets to pr")
883 }
884
885 if isReview && !acl.CanReview {
886 return fmt.Errorf("you are not authorized to submit a review to pr")
887 }
888
889 op := OpNormal
890 nextStatus := "open"
891 if isReview {
892 wish.Println(sesh, "Marking patchset as a review")
893 op = OpReview
894 } else if isAccept {
895 wish.Println(sesh, "Marking PR as accepted")
896 nextStatus = "accepted"
897 op = OpAccept
898 } else if isClose {
899 wish.Println(sesh, "Marking PR as closed")
900 nextStatus = "closed"
901 op = OpClose
902 }
903
904 patches, err := pr.SubmitPatchset(prID, user.ID, op, sesh)
905 if err != nil {
906 return err
907 }
908
909 if len(patches) == 0 {
910 wish.Println(sesh, "Patches submitted! However none were saved, probably because they already exist in the system")
911 return nil
912 }
913
914 if prq.Status != nextStatus {
915 err = pr.UpdatePatchRequestStatus(prID, user.ID, nextStatus)
916 if err != nil {
917 return err
918 }
919 }
920
921 wish.Println(sesh, "Patches submitted!")
922 return prSummary(be, pr, sesh, prID)
923 },
924 },
925 },
926 },
927 },
928 }
929
930 return app
931}