fj/cmd/pr_review.go
sid bc43f6e5a5 rename fgj to fj
Module path, binary name, config dir, help text, and docs
all updated from fgj-sid/fgj to fj.
2026-04-26 08:16:52 -06:00

234 lines
5.9 KiB
Go

package cmd
import (
"fmt"
"io"
"os"
"code.gitea.io/sdk/gitea"
"forgejo.zerova.net/public/fj/internal/api"
"forgejo.zerova.net/public/fj/internal/config"
"github.com/spf13/cobra"
)
var prCommentCmd = &cobra.Command{
Use: "comment <number>",
Short: "Add a comment to a pull request",
Long: "Add a comment to an existing pull request.",
Example: ` # Add a comment
fj pr comment 123 -b "Looks good!"
# Comment from a file
fj pr comment 123 --body-file review-notes.md
# Comment from stdin
echo "LGTM" | fj pr comment 123 --body-file -
# Output as JSON
fj pr comment 123 -b "Nice work" --json`,
Args: cobra.ExactArgs(1),
RunE: runPRComment,
}
var prReviewCmd = &cobra.Command{
Use: "review <number>",
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
fj pr review 123 --approve -b "LGTM"
# Request changes
fj pr review 123 --request-changes -b "Please fix the error handling"
# Submit a review comment
fj pr review 123 --comment -b "Some observations"
# Request changes with body from file
fj 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
}