fj/cmd/pr_clean.go
sid adccd6f6f7 feat: add webhook, repo delete/search, admin, pr clean/resolve/review-comments
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.
2026-04-19 22:01:29 -06:00

98 lines
2.8 KiB
Go

package cmd
import (
"fmt"
"os/exec"
"strings"
"forgejo.zerova.net/public/fgj-sid/internal/api"
"forgejo.zerova.net/public/fgj-sid/internal/config"
"forgejo.zerova.net/public/fgj-sid/internal/git"
"github.com/spf13/cobra"
)
var prCleanCmd = &cobra.Command{
Use: "clean <number>",
Short: "Delete the local branch created by 'pr checkout'",
Long: `Remove the local branch that was checked out for a pull request.
For safety, the PR must be closed (merged or declined). If the branch is
currently checked out, switch away first — this command refuses to delete
your active branch.
Pass --force to delete the local branch even if the PR is still open.`,
Example: ` # Clean up after a merged PR
fgj pr clean 42
# Force-delete local branch for an open PR
fgj pr clean 42 --force`,
Args: cobra.ExactArgs(1),
RunE: runPRClean,
}
func init() {
prCmd.AddCommand(prCleanCmd)
addRepoFlags(prCleanCmd)
prCleanCmd.Flags().Bool("force", false, "Delete the local branch even if the PR is still open")
}
func runPRClean(cmd *cobra.Command, args []string) error {
prNumber, err := parseIssueArg(args[0])
if err != nil {
return fmt.Errorf("invalid pull request number: %w", err)
}
repoFlag, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repoFlag)
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
}
force, _ := cmd.Flags().GetBool("force")
pr, _, err := client.GetPullRequest(owner, name, prNumber)
if err != nil {
return fmt.Errorf("failed to get pull request: %w", err)
}
if !force && string(pr.State) == "open" {
return fmt.Errorf("PR #%d is still open; refuse to delete local branch without --force", prNumber)
}
headBranch := pr.Head.Ref
if headBranch == "" {
return fmt.Errorf("PR #%d has no head branch to clean (it may have been deleted already)", prNumber)
}
// Refuse to delete the current branch.
current, err := git.GetCurrentBranch()
if err == nil && current == headBranch {
return fmt.Errorf("branch %q is currently checked out; switch to another branch first (e.g. 'git switch main')", headBranch)
}
// Check local branch exists.
if out, _ := exec.Command("git", "rev-parse", "--verify", "--quiet", "refs/heads/"+headBranch).Output(); len(strings.TrimSpace(string(out))) == 0 {
fmt.Fprintf(ios.ErrOut, "Local branch %q not found; nothing to clean.\n", headBranch)
return nil
}
delCmd := exec.Command("git", "branch", "-D", headBranch)
delCmd.Stdout = ios.Out
delCmd.Stderr = ios.ErrOut
if err := delCmd.Run(); err != nil {
return fmt.Errorf("failed to delete local branch %q: %w", headBranch, err)
}
cs := ios.ColorScheme()
fmt.Fprintf(ios.Out, "%s Deleted local branch %q\n", cs.SuccessIcon(), headBranch)
return nil
}