package cmd import ( "fmt" "strconv" "rickub.com/rickub/cli/internal/api" "github.com/spf13/cobra" ) var ( prRepo string prState string prBase string prHead string prTitle string prBody string prHeadOwner string prHeadRepo string prMergeMethod string ) func init() { prCmd := &cobra.Command{ Use: "pr", Aliases: []string{"mr", "pull"}, Short: "Work with merge requests (pull requests)", } // --repo is shared by all subcommands; defaults to the cwd git remote. prCmd.PersistentFlags().StringVarP(&prRepo, "repo", "R", "", "target repo as owner/repo (default: current git remote)") listCmd := &cobra.Command{ Use: "list", Short: "List merge requests", Args: cobra.NoArgs, RunE: runPRList, } listCmd.Flags().StringVar(&prState, "state", "open", "open | closed | merged | all") addPaging(listCmd) viewCmd := &cobra.Command{ Use: "view ", Short: "Show a merge request with comments and reviews", Args: cobra.ExactArgs(1), RunE: runPRView, } createCmd := &cobra.Command{ Use: "create", Short: "Open a merge request", Args: cobra.NoArgs, RunE: runPRCreate, } createCmd.Flags().StringVar(&prBase, "base", "", "base branch (required)") createCmd.Flags().StringVar(&prHead, "head", "", "head branch (required)") createCmd.Flags().StringVarP(&prTitle, "title", "t", "", "title (required)") createCmd.Flags().StringVarP(&prBody, "body", "b", "", "description body") createCmd.Flags().StringVar(&prHeadOwner, "head-owner", "", "fork owner for a cross-repo MR") createCmd.Flags().StringVar(&prHeadRepo, "head-repo", "", "fork name for a cross-repo MR") mergeCmd := &cobra.Command{ Use: "merge ", Short: "Merge a merge request", Args: cobra.ExactArgs(1), RunE: runPRMerge, } mergeCmd.Flags().StringVar(&prMergeMethod, "method", "merge", "merge | squash | ff-only") closeCmd := &cobra.Command{ Use: "close ", Short: "Close a merge request", Args: cobra.ExactArgs(1), RunE: runPRClose, } reopenCmd := &cobra.Command{ Use: "reopen ", Short: "Reopen a closed merge request", Args: cobra.ExactArgs(1), RunE: runPRReopen, } commentCmd := &cobra.Command{ Use: "comment ", Short: "Comment on a merge request", Args: cobra.ExactArgs(1), RunE: runPRComment, } commentCmd.Flags().StringVarP(&prBody, "body", "b", "", "comment body (required)") prCmd.AddCommand(listCmd, viewCmd, createCmd, mergeCmd, closeCmd, reopenCmd, commentCmd) rootCmd.AddCommand(prCmd) } func prNumber(arg string) (int, error) { n, err := strconv.Atoi(arg) if err != nil || n <= 0 { return 0, fmt.Errorf("invalid merge-request number %q", arg) } return n, nil } func runPRList(cmd *cobra.Command, _ []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := resolveRepo(prRepo) if err != nil { return err } page, err := client.ListMergeRequests(cmd.Context(), owner, repo, prState, pageFlag, perPageFlag) if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), page) } if len(page.Items) == 0 { fmt.Fprintln(cmd.OutOrStdout(), "No merge requests.") return nil } tw := newTabw(cmd.OutOrStdout()) fmt.Fprintln(tw, "#\tSTATE\tTITLE\tHEAD→BASE\tAUTHOR") for _, mr := range page.Items { fmt.Fprintf(tw, "%d\t%s\t%s\t%s→%s\t%s\n", mr.Number, mr.State, mr.Title, mr.HeadBranch, mr.BaseBranch, mr.Author) } tw.Flush() printPageFooter(cmd, page.Page) return nil } func runPRView(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := resolveRepo(prRepo) if err != nil { return err } n, err := prNumber(args[0]) if err != nil { return err } mr, err := client.GetMergeRequest(cmd.Context(), owner, repo, n) if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), mr) } out := cmd.OutOrStdout() fmt.Fprintf(out, "#%d %s [%s]\n", mr.Number, mr.Title, mr.State) fmt.Fprintf(out, "%s wants to merge %s → %s\n", mr.Author, mr.HeadBranch, mr.BaseBranch) if mr.CrossRepo { fmt.Fprintf(out, "cross-repo from %s/%s\n", mr.HeadOwner, mr.HeadRepo) } if mr.Body != "" { fmt.Fprintf(out, "\n%s\n", mr.Body) } if len(mr.Reviewers) > 0 { fmt.Fprintf(out, "\nReviewers: %s\n", joinSubjects(mr.Reviewers)) } if len(mr.Assignees) > 0 { fmt.Fprintf(out, "Assignees: %s\n", joinSubjects(mr.Assignees)) } if len(mr.Reviews) > 0 { fmt.Fprintln(out, "\nReviews:") for _, r := range mr.Reviews { fmt.Fprintf(out, " %s: %s\n", r.Reviewer, r.Verdict) } } if len(mr.Comments) > 0 { fmt.Fprintln(out, "\nComments:") for _, c := range mr.Comments { fmt.Fprintf(out, " %s (%s):\n %s\n", c.Author, humanTime(c.CreatedAt), c.Body) } } return nil } func joinSubjects(subs []api.Subject) string { out := "" for i, s := range subs { if i > 0 { out += ", " } label := s.Name if s.Type == "team" { label = "@team/" + s.Name } out += label } return out } func runPRCreate(cmd *cobra.Command, _ []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := resolveRepo(prRepo) if err != nil { return err } if prBase == "" || prHead == "" || prTitle == "" { return fmt.Errorf("--base, --head, and --title are required") } mr, err := client.CreateMergeRequest(cmd.Context(), owner, repo, api.MergeRequestCreate{ Base: prBase, Head: prHead, Title: prTitle, Body: prBody, HeadOwner: prHeadOwner, HeadRepo: prHeadRepo, }) if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), mr) } fmt.Fprintf(cmd.OutOrStdout(), "Opened merge request #%d: %s\n", mr.Number, mr.Title) return nil } func runPRMerge(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := resolveRepo(prRepo) if err != nil { return err } n, err := prNumber(args[0]) if err != nil { return err } mr, err := client.MergeMergeRequest(cmd.Context(), owner, repo, n, prMergeMethod) if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), mr) } fmt.Fprintf(cmd.OutOrStdout(), "Merged #%d (%s) → %s\n", mr.Number, prMergeMethod, dash(mr.MergeSHA)) return nil } func runPRClose(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := resolveRepo(prRepo) if err != nil { return err } n, err := prNumber(args[0]) if err != nil { return err } mr, err := client.CloseMergeRequest(cmd.Context(), owner, repo, n) if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), mr) } fmt.Fprintf(cmd.OutOrStdout(), "Closed #%d\n", mr.Number) return nil } func runPRReopen(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := resolveRepo(prRepo) if err != nil { return err } n, err := prNumber(args[0]) if err != nil { return err } mr, err := client.ReopenMergeRequest(cmd.Context(), owner, repo, n) if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), mr) } fmt.Fprintf(cmd.OutOrStdout(), "Reopened #%d\n", mr.Number) return nil } func runPRComment(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := resolveRepo(prRepo) if err != nil { return err } n, err := prNumber(args[0]) if err != nil { return err } if prBody == "" { return fmt.Errorf("--body is required") } c, err := client.CommentMergeRequest(cmd.Context(), owner, repo, n, prBody) if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), c) } fmt.Fprintf(cmd.OutOrStdout(), "Commented on #%d\n", n) return nil }