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.
This commit is contained in:
parent
17ca49d0c5
commit
adccd6f6f7
6 changed files with 871 additions and 0 deletions
146
cmd/repo_search.go
Normal file
146
cmd/repo_search.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var repoSearchCmd = &cobra.Command{
|
||||
Use: "search [query]",
|
||||
Aliases: []string{"s"},
|
||||
Short: "Search repositories on the current host",
|
||||
Long: `Search repositories using the host's search index.
|
||||
|
||||
The query is matched against name by default. Pass --topic to match against
|
||||
topics only, or --description to include descriptions. --type limits results
|
||||
to "source" (non-fork, non-mirror), "fork", or "mirror" repositories.`,
|
||||
Example: ` # Search by name substring
|
||||
fgj repo search tea
|
||||
|
||||
# Search by topic
|
||||
fgj repo search ci --topic
|
||||
|
||||
# Find only forks
|
||||
fgj repo search go --type fork
|
||||
|
||||
# List private repos owned by a user (no query)
|
||||
fgj repo search --owner alice --private --limit 50
|
||||
|
||||
# Output as JSON
|
||||
fgj repo search platform --json`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runRepoSearch,
|
||||
}
|
||||
|
||||
func init() {
|
||||
repoCmd.AddCommand(repoSearchCmd)
|
||||
|
||||
repoSearchCmd.Flags().Bool("topic", false, "Match query against topics only")
|
||||
repoSearchCmd.Flags().Bool("description", false, "Include descriptions in the search")
|
||||
repoSearchCmd.Flags().Bool("private", false, "Limit to private repositories")
|
||||
repoSearchCmd.Flags().Bool("archived", false, "Include archived repositories")
|
||||
repoSearchCmd.Flags().Bool("exclude-templates", false, "Exclude template repositories")
|
||||
repoSearchCmd.Flags().String("type", "", "Filter by repo type: source, fork, mirror")
|
||||
repoSearchCmd.Flags().String("owner", "", "Limit to repos owned by this user or org")
|
||||
repoSearchCmd.Flags().String("sort", "", "Sort by: alpha, created, updated, size, id")
|
||||
repoSearchCmd.Flags().String("order", "", "Order: asc or desc")
|
||||
repoSearchCmd.Flags().IntP("limit", "L", 30, "Maximum number of results")
|
||||
|
||||
addJSONFlags(repoSearchCmd, "Output as JSON")
|
||||
}
|
||||
|
||||
func runRepoSearch(cmd *cobra.Command, args []string) error {
|
||||
client, err := loadClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := ""
|
||||
if len(args) == 1 {
|
||||
query = args[0]
|
||||
}
|
||||
|
||||
topic, _ := cmd.Flags().GetBool("topic")
|
||||
desc, _ := cmd.Flags().GetBool("description")
|
||||
private, _ := cmd.Flags().GetBool("private")
|
||||
archived, _ := cmd.Flags().GetBool("archived")
|
||||
excludeTemplates, _ := cmd.Flags().GetBool("exclude-templates")
|
||||
sort, _ := cmd.Flags().GetString("sort")
|
||||
order, _ := cmd.Flags().GetString("order")
|
||||
typeFlag, _ := cmd.Flags().GetString("type")
|
||||
limit, _ := cmd.Flags().GetInt("limit")
|
||||
if limit <= 0 {
|
||||
limit = 30
|
||||
}
|
||||
|
||||
var repoType gitea.RepoType
|
||||
switch typeFlag {
|
||||
case "":
|
||||
repoType = gitea.RepoTypeNone
|
||||
case "source":
|
||||
repoType = gitea.RepoTypeSource
|
||||
case "fork":
|
||||
repoType = gitea.RepoTypeFork
|
||||
case "mirror":
|
||||
repoType = gitea.RepoTypeMirror
|
||||
default:
|
||||
return fmt.Errorf("invalid --type %q (must be source, fork, or mirror)", typeFlag)
|
||||
}
|
||||
|
||||
opt := gitea.SearchRepoOptions{
|
||||
ListOptions: gitea.ListOptions{PageSize: limit},
|
||||
Keyword: query,
|
||||
KeywordIsTopic: topic,
|
||||
KeywordInDescription: desc,
|
||||
IsPrivate: optionalBool(private),
|
||||
IsArchived: optionalBool(archived),
|
||||
ExcludeTemplate: excludeTemplates,
|
||||
Type: repoType,
|
||||
Sort: sort,
|
||||
Order: order,
|
||||
}
|
||||
|
||||
if o, _ := cmd.Flags().GetString("owner"); o != "" {
|
||||
u, _, err := client.GetUserInfo(o)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve owner %q: %w", o, err)
|
||||
}
|
||||
opt.OwnerID = u.ID
|
||||
}
|
||||
|
||||
repos, _, err := client.SearchRepos(opt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("search failed: %w", err)
|
||||
}
|
||||
|
||||
if wantJSON(cmd) {
|
||||
return outputJSON(cmd, repos)
|
||||
}
|
||||
|
||||
if len(repos) == 0 {
|
||||
fmt.Fprintln(ios.Out, "No repositories match.")
|
||||
return nil
|
||||
}
|
||||
|
||||
tp := ios.NewTablePrinter()
|
||||
tp.AddHeader("FULL NAME", "VISIBILITY", "DESCRIPTION", "STARS")
|
||||
for _, r := range repos {
|
||||
visibility := "public"
|
||||
if r.Private {
|
||||
visibility = "private"
|
||||
}
|
||||
tp.AddRow(r.FullName, visibility, r.Description, fmt.Sprintf("%d", r.Stars))
|
||||
}
|
||||
return tp.Render()
|
||||
}
|
||||
|
||||
// optionalBool returns a pointer when the user explicitly wants the positive
|
||||
// filter (IsPrivate/IsArchived); nil means "no filter" to the SDK.
|
||||
func optionalBool(v bool) *bool {
|
||||
if !v {
|
||||
return nil
|
||||
}
|
||||
return &v
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue