package cmd import ( "fmt" "io" "os" "code.gitea.io/sdk/gitea" "forgejo.zerova.net/sid/fgj-sid/internal/api" "forgejo.zerova.net/sid/fgj-sid/internal/config" "github.com/spf13/cobra" ) var prCommentCmd = &cobra.Command{ Use: "comment ", Short: "Add a comment to a pull request", Long: "Add a comment to an existing pull request.", Example: ` # Add a comment fgj pr comment 123 -b "Looks good!" # Comment from a file fgj pr comment 123 --body-file review-notes.md # Comment from stdin echo "LGTM" | fgj pr comment 123 --body-file - # Output as JSON fgj pr comment 123 -b "Nice work" --json`, Args: cobra.ExactArgs(1), RunE: runPRComment, } var prReviewCmd = &cobra.Command{ Use: "review ", Short: "Submit a review on a pull request", Long: "Submit a review on a pull request. Exactly one of --approve, --request-changes, or --comment must be specified.", Example: ` # Approve a PR fgj pr review 123 --approve -b "LGTM" # Request changes fgj pr review 123 --request-changes -b "Please fix the error handling" # Submit a review comment fgj pr review 123 --comment -b "Some observations" # Request changes with body from file fgj pr review 123 --request-changes --body-file feedback.md`, Args: cobra.ExactArgs(1), RunE: runPRReview, } func init() { prCmd.AddCommand(prCommentCmd) prCmd.AddCommand(prReviewCmd) prCommentCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format") prCommentCmd.Flags().StringP("body", "b", "", "Comment body") prCommentCmd.Flags().String("body-file", "", "Read body from file (use \"-\" for stdin)") addJSONFlags(prCommentCmd, "Output created comment as JSON") prReviewCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format") prReviewCmd.Flags().BoolP("approve", "a", false, "Approve the pull request") prReviewCmd.Flags().BoolP("request-changes", "r", false, "Request changes on the pull request") prReviewCmd.Flags().BoolP("comment", "c", false, "Submit as a review comment") prReviewCmd.Flags().StringP("body", "b", "", "Review body/message") prReviewCmd.Flags().String("body-file", "", "Read body from file (use \"-\" for stdin)") addJSONFlags(prReviewCmd, "Output created review as JSON") } // readBody resolves the body text from --body and --body-file flags. // --body takes precedence; if --body-file is set and --body is empty, the file // (or stdin when the path is "-") is read instead. func readBody(cmd *cobra.Command) (string, error) { body, _ := cmd.Flags().GetString("body") bodyFile, _ := cmd.Flags().GetString("body-file") if body != "" && bodyFile != "" { return "", fmt.Errorf("use either --body or --body-file, not both") } if bodyFile != "" { var data []byte var err error if bodyFile == "-" { data, err = io.ReadAll(os.Stdin) } else { data, err = os.ReadFile(bodyFile) } if err != nil { return "", fmt.Errorf("failed to read body file: %w", err) } body = string(data) } return body, nil } func runPRComment(cmd *cobra.Command, args []string) error { repo, _ := cmd.Flags().GetString("repo") prNumber, err := parseIssueArg(args[0]) if err != nil { return fmt.Errorf("invalid pull request number: %w", err) } body, err := readBody(cmd) if err != nil { return err } if body == "" { return fmt.Errorf("comment body is required (use --body or --body-file)") } owner, name, err := parseRepo(repo) if err != nil { return err } cfg, err := config.Load() if err != nil { return err } client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } ios.StartSpinner("Adding comment...") comment, _, err := client.CreateIssueComment(owner, name, prNumber, gitea.CreateIssueCommentOption{ Body: body, }) ios.StopSpinner() if err != nil { return fmt.Errorf("failed to create comment: %w", err) } if wantJSON(cmd) { return outputJSON(cmd, comment) } cs := ios.ColorScheme() fmt.Fprintf(ios.Out, "%s Comment added to PR #%d\n", cs.SuccessIcon(), prNumber) fmt.Fprintf(ios.Out, "View at: %s\n", comment.HTMLURL) return nil } func runPRReview(cmd *cobra.Command, args []string) error { repo, _ := cmd.Flags().GetString("repo") approve, _ := cmd.Flags().GetBool("approve") requestChanges, _ := cmd.Flags().GetBool("request-changes") commentReview, _ := cmd.Flags().GetBool("comment") prNumber, err := parseIssueArg(args[0]) if err != nil { return fmt.Errorf("invalid pull request number: %w", err) } // Validate exactly one review type is specified count := 0 if approve { count++ } if requestChanges { count++ } if commentReview { count++ } if count != 1 { return fmt.Errorf("exactly one of --approve, --request-changes, or --comment must be specified") } body, err := readBody(cmd) if err != nil { return err } if requestChanges && body == "" { return fmt.Errorf("body is required when requesting changes (use --body or --body-file)") } owner, name, err := parseRepo(repo) if err != nil { return err } cfg, err := config.Load() if err != nil { return err } client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } var state gitea.ReviewStateType var action string switch { case approve: state = gitea.ReviewStateApproved action = "approved" case requestChanges: state = gitea.ReviewStateRequestChanges action = "reviewed with requested changes" case commentReview: state = gitea.ReviewStateComment action = "reviewed with comment" } ios.StartSpinner("Submitting review...") review, _, err := client.CreatePullReview(owner, name, prNumber, gitea.CreatePullReviewOptions{ State: state, Body: body, }) ios.StopSpinner() if err != nil { return fmt.Errorf("failed to create review: %w", err) } if wantJSON(cmd) { return outputJSON(cmd, review) } cs := ios.ColorScheme() fmt.Fprintf(ios.Out, "%s PR #%d %s\n", cs.SuccessIcon(), prNumber, action) if review.HTMLURL != "" { fmt.Fprintf(ios.Out, "View at: %s\n", review.HTMLURL) } return nil }