feat: add PR diff, PR review, and structured error handling commands
This commit is contained in:
parent
3db03ed5e2
commit
50191cc542
10 changed files with 1008 additions and 13 deletions
229
cmd/pr_review.go
Normal file
229
cmd/pr_review.go
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"codeberg.org/romaintb/fgj/internal/api"
|
||||
"codeberg.org/romaintb/fgj/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
|
||||
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 <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
|
||||
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)")
|
||||
prCommentCmd.Flags().Bool("json", false, "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)")
|
||||
prReviewCmd.Flags().Bool("json", false, "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 := strconv.ParseInt(args[0], 10, 64)
|
||||
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())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
comment, _, err := client.CreateIssueComment(owner, name, prNumber, gitea.CreateIssueCommentOption{
|
||||
Body: body,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create comment: %w", err)
|
||||
}
|
||||
|
||||
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
|
||||
return writeJSON(comment)
|
||||
}
|
||||
|
||||
fmt.Printf("Comment added to PR #%d\n", prNumber)
|
||||
fmt.Printf("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 := strconv.ParseInt(args[0], 10, 64)
|
||||
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())
|
||||
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"
|
||||
}
|
||||
|
||||
review, _, err := client.CreatePullReview(owner, name, prNumber, gitea.CreatePullReviewOptions{
|
||||
State: state,
|
||||
Body: body,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create review: %w", err)
|
||||
}
|
||||
|
||||
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
|
||||
return writeJSON(review)
|
||||
}
|
||||
|
||||
fmt.Printf("PR #%d %s\n", prNumber, action)
|
||||
if review.HTMLURL != "" {
|
||||
fmt.Printf("View at: %s\n", review.HTMLURL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue