Add PR checks command, iostreams/text packages for colored table output, top-level run/workflow aliases matching gh CLI structure. Enhance actions, issues, PRs, releases, repos, labels, milestones, and wiki commands with improved flags, JSON output, and error handling.
234 lines
5.9 KiB
Go
234 lines
5.9 KiB
Go
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 <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)")
|
|
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())
|
|
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())
|
|
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
|
|
}
|