Second pass of tea-parity work:
- fgj webhook {list,create,update,delete}: full CRUD over repo webhooks.
Create supports all standard hook types (gitea, slack, discord, etc.),
event selection, content type, secret, branch filter, auth header.
Update is partial — flags you omit leave existing config unchanged.
- fgj repo delete: type-to-confirm deletion; --yes skips for scripts;
refuses without a TTY unless --yes is passed.
- fgj repo search: SDK SearchRepos with query, topic/description,
private/archived, --type (source/fork/mirror), owner, sort/order.
- fgj admin user list: admin-gated user enumeration.
- fgj pr clean: delete the local branch from 'pr checkout'. Refuses
if the PR is still open (use --force) or if the branch is currently
checked out.
- fgj pr review-comments: list inline review comments across every
review on a PR (ListPullReviews + ListPullReviewComments per review).
- fgj pr resolve / unresolve: mark review comments as (un)resolved.
Uses raw POST since SDK v0.22.1 predates these endpoints; requires
Forgejo 8.x+ / Gitea 1.22+ server-side.
All share the standard parseRepo + config.Load + NewClientFromConfig
pattern; list commands support --json / --jq.
171 lines
4.4 KiB
Go
171 lines
4.4 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"code.gitea.io/sdk/gitea"
|
|
"forgejo.zerova.net/public/fgj-sid/internal/api"
|
|
"forgejo.zerova.net/public/fgj-sid/internal/config"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var prReviewCommentsCmd = &cobra.Command{
|
|
Use: "review-comments <number>",
|
|
Aliases: []string{"rc"},
|
|
Short: "List review comments on a pull request",
|
|
Long: "List all review comments (inline code comments) across every review on a PR.",
|
|
Example: ` # List review comments on PR #42
|
|
fgj pr review-comments 42
|
|
|
|
# As JSON
|
|
fgj pr review-comments 42 --json`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runPRReviewComments,
|
|
}
|
|
|
|
var prResolveCmd = &cobra.Command{
|
|
Use: "resolve <comment-id>",
|
|
Short: "Resolve a PR review comment",
|
|
Long: `Mark a pull request review comment as resolved. Comment IDs are shown
|
|
in the output of 'fgj pr review-comments'.
|
|
|
|
Requires Forgejo 8.x+ / Gitea 1.22+.`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runPRResolveComment(true),
|
|
}
|
|
|
|
var prUnresolveCmd = &cobra.Command{
|
|
Use: "unresolve <comment-id>",
|
|
Short: "Unresolve a PR review comment",
|
|
Long: "Reopen a previously-resolved pull request review comment.",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runPRResolveComment(false),
|
|
}
|
|
|
|
func init() {
|
|
prCmd.AddCommand(prReviewCommentsCmd)
|
|
prCmd.AddCommand(prResolveCmd)
|
|
prCmd.AddCommand(prUnresolveCmd)
|
|
|
|
addRepoFlags(prReviewCommentsCmd)
|
|
addJSONFlags(prReviewCommentsCmd, "Output as JSON")
|
|
|
|
addRepoFlags(prResolveCmd)
|
|
addRepoFlags(prUnresolveCmd)
|
|
}
|
|
|
|
func runPRReviewComments(cmd *cobra.Command, args []string) error {
|
|
prNumber, err := parseIssueArg(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid pull request number: %w", err)
|
|
}
|
|
|
|
client, owner, name, err := newPRCommentClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reviews, _, err := client.ListPullReviews(owner, name, prNumber, gitea.ListPullReviewsOptions{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list reviews: %w", err)
|
|
}
|
|
|
|
var all []*gitea.PullReviewComment
|
|
for _, r := range reviews {
|
|
comments, _, err := client.ListPullReviewComments(owner, name, prNumber, r.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list comments for review %d: %w", r.ID, err)
|
|
}
|
|
all = append(all, comments...)
|
|
}
|
|
|
|
if wantJSON(cmd) {
|
|
return outputJSON(cmd, all)
|
|
}
|
|
|
|
if len(all) == 0 {
|
|
fmt.Fprintln(ios.Out, "No review comments on this PR.")
|
|
return nil
|
|
}
|
|
|
|
tp := ios.NewTablePrinter()
|
|
tp.AddHeader("ID", "REVIEWER", "PATH", "LINE", "RESOLVED", "BODY")
|
|
for _, c := range all {
|
|
reviewer := ""
|
|
if c.Reviewer != nil {
|
|
reviewer = c.Reviewer.UserName
|
|
}
|
|
resolved := ""
|
|
if c.Resolver != nil {
|
|
resolved = "yes"
|
|
}
|
|
// Collapse multi-line bodies for table view.
|
|
body := strings.ReplaceAll(c.Body, "\n", " ")
|
|
if len(body) > 80 {
|
|
body = body[:77] + "..."
|
|
}
|
|
tp.AddRow(
|
|
fmt.Sprintf("%d", c.ID),
|
|
reviewer,
|
|
c.Path,
|
|
fmt.Sprintf("%d", c.LineNum),
|
|
resolved,
|
|
body,
|
|
)
|
|
}
|
|
return tp.Render()
|
|
}
|
|
|
|
// runPRResolveComment returns a RunE closure that either resolves or unresolves
|
|
// a review comment, depending on the `resolve` flag. The underlying SDK
|
|
// (v0.22.1) doesn't expose these endpoints yet, so we call them raw.
|
|
func runPRResolveComment(resolve bool) func(cmd *cobra.Command, args []string) error {
|
|
return func(cmd *cobra.Command, args []string) error {
|
|
id, err := parseIssueArg(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid comment id: %w", err)
|
|
}
|
|
|
|
client, owner, name, err := newPRCommentClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d", owner, name, id)
|
|
action := "unresolve"
|
|
if resolve {
|
|
action = "resolve"
|
|
}
|
|
endpoint := path + "/" + action
|
|
|
|
if err := client.PostJSON(endpoint, map[string]any{}, nil); err != nil {
|
|
return fmt.Errorf("failed to %s comment %d: %w", action, id, err)
|
|
}
|
|
|
|
cs := ios.ColorScheme()
|
|
verb := "Resolved"
|
|
if !resolve {
|
|
verb = "Unresolved"
|
|
}
|
|
fmt.Fprintf(ios.Out, "%s %s comment %d\n", cs.SuccessIcon(), verb, id)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func newPRCommentClient(cmd *cobra.Command) (*api.Client, string, string, error) {
|
|
repoFlag, _ := cmd.Flags().GetString("repo")
|
|
owner, name, err := parseRepo(repoFlag)
|
|
if err != nil {
|
|
return nil, "", "", err
|
|
}
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return nil, "", "", err
|
|
}
|
|
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
|
|
if err != nil {
|
|
return nil, "", "", err
|
|
}
|
|
return client, owner, name, nil
|
|
}
|