mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
The root help lists commands in gh-style groups (core, Actions, additional and alias commands) followed by help topics, examples and pointers. Every help page uses USAGE, ALIASES, FLAGS, INHERITED FLAGS, ARGUMENTS, JSON FIELDS, EXAMPLES and LEARN MORE sections. Inherited flags show --repo, --remote and --cwd only where a repository is selected, --json and --jq where JSON is printed, --yes where the command confirms, and one line for the remaining global flags. tea help environment, exit-codes and formatting document global flags, environment variables, exit codes and JSON output. tea help with an unknown topic is a usage error with suggestions instead of printing the root help. A test keeps Shorts, examples and Long texts complete, with allowlists for the commands that still lack them. Signed-off-by: bircni <bircni@icloud.com>
573 lines
21 KiB
Go
573 lines
21 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Package app builds the tea command tree and its injected dependencies.
|
|
package app
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"gitea.dev/tea/internal/aliases"
|
|
"gitea.dev/tea/internal/auth"
|
|
"gitea.dev/tea/internal/clierr"
|
|
"gitea.dev/tea/internal/git"
|
|
"gitea.dev/tea/internal/services"
|
|
"gitea.dev/tea/internal/settings"
|
|
"gitea.dev/tea/internal/terminal"
|
|
"gitea.dev/tea/internal/transport"
|
|
"gitea.dev/tea/internal/version"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
)
|
|
|
|
// Dependencies supplies external boundaries for a fresh CLI invocation.
|
|
type Dependencies struct {
|
|
Browser terminal.Browser
|
|
Editor terminal.Editor
|
|
Pager terminal.Pager
|
|
Executable func() (string, error)
|
|
Git git.Runner
|
|
Now func() time.Time
|
|
Settings *settings.Store
|
|
Credentials *auth.Store
|
|
Streams terminal.Streams
|
|
Prompter terminal.Prompter
|
|
IssueForm terminal.IssueForm
|
|
IssueEditForm terminal.IssueEditForm
|
|
PullForm terminal.PullForm
|
|
ReviewForm terminal.ReviewForm
|
|
MergeForm terminal.MergeForm
|
|
MilestoneForm terminal.MilestoneForm
|
|
Getenv func(string) string
|
|
Client func(string, string, bool) *transport.Client
|
|
}
|
|
|
|
// System connects the CLI to the current environment and terminal.
|
|
func System() Dependencies {
|
|
streams := terminal.System()
|
|
store := settings.New(os.Getenv("TEA_CONFIG_DIR"))
|
|
prompter := &terminal.FormPrompter{Streams: streams}
|
|
return Dependencies{Browser: terminal.SystemBrowser{ErrOut: streams.ErrOut}, Editor: terminal.SystemEditor{Streams: streams}, Pager: terminal.SystemPager{}, Executable: os.Executable, Git: git.System{In: streams.In, ErrOut: streams.ErrOut}, Now: time.Now, Settings: store, Credentials: &auth.Store{Dir: store.Dir, Keyring: auth.SystemKeyring{}}, Streams: streams, Prompter: prompter, IssueForm: prompter, IssueEditForm: prompter, PullForm: prompter, ReviewForm: prompter, MergeForm: prompter, MilestoneForm: prompter, Getenv: os.Getenv, Client: transport.New}
|
|
}
|
|
|
|
type options struct {
|
|
host, user, repo, cwd, remote string
|
|
sudo string
|
|
json, jq string
|
|
noInput, interactive, yes bool
|
|
debug bool
|
|
}
|
|
|
|
type application struct {
|
|
deps Dependencies
|
|
options options
|
|
}
|
|
|
|
// NewRoot builds fresh commands. Construction performs no disk, network, or Git IO.
|
|
func NewRoot(deps Dependencies) *cobra.Command {
|
|
if deps.Executable == nil {
|
|
deps.Executable = os.Executable
|
|
}
|
|
if deps.Now == nil {
|
|
deps.Now = time.Now
|
|
}
|
|
if deps.Git == nil {
|
|
deps.Git = git.System{In: deps.Streams.In, ErrOut: deps.Streams.ErrOut}
|
|
}
|
|
if deps.Browser == nil {
|
|
deps.Browser = terminal.SystemBrowser{ErrOut: deps.Streams.ErrOut}
|
|
}
|
|
if deps.Editor == nil {
|
|
deps.Editor = terminal.SystemEditor{Streams: deps.Streams}
|
|
}
|
|
a := &application{deps: deps}
|
|
if client := deps.Client; client != nil {
|
|
a.deps.Client = func(instance, token string, insecure bool) *transport.Client {
|
|
if a.options.debug {
|
|
return client(instance, token, insecure).Debug(deps.Streams.ErrOut)
|
|
}
|
|
return client(instance, token, insecure)
|
|
}
|
|
}
|
|
root := &cobra.Command{Use: "tea", Short: "Work with Gitea from your terminal", Long: "Work seamlessly with Gitea from the command line.", Version: version.Format(), SilenceErrors: true, SilenceUsage: true}
|
|
root.SetIn(deps.Streams.In)
|
|
root.SetOut(deps.Streams.Out)
|
|
root.SetErr(deps.Streams.ErrOut)
|
|
root.SetFlagErrorFunc(func(_ *cobra.Command, err error) error { return clierr.Wrap(clierr.Usage, err) })
|
|
flags := root.PersistentFlags()
|
|
flags.StringVar(&a.options.host, "host", "", "Gitea instance `URL`, including any base path")
|
|
flags.StringVar(&a.options.user, "user", "", "Account `name` on the selected instance")
|
|
flags.StringVar(&a.options.sudo, "sudo", "", "Perform API requests as this `user` (requires an administrator account)")
|
|
flags.StringVarP(&a.options.repo, "repo", "R", "", "Select a repository as `OWNER/REPO` or URL")
|
|
flags.StringVar(&a.options.cwd, "cwd", "", "Working `directory` for repository discovery")
|
|
flags.StringVar(&a.options.remote, "remote", "", "Git `remote` used for repository discovery")
|
|
flags.StringVar(&a.options.json, "json", "", "Output JSON with the specified comma-separated `fields`, or all fields")
|
|
flags.Lookup("json").NoOptDefVal = "*"
|
|
flags.StringVarP(&a.options.jq, "jq", "q", "", "Filter JSON output using a jq `expression`; implies --json")
|
|
flags.BoolVar(&a.options.noInput, "no-input", false, "Disable prompts and implicit editor/browser interactions")
|
|
flags.BoolVar(&a.options.interactive, "interactive", false, "Allow prompts with machine output")
|
|
flags.BoolVarP(&a.options.yes, "yes", "y", false, "Accept confirmations without prompting")
|
|
flags.BoolVar(&a.options.debug, "debug", false, "Log the method, URL, status, and duration of each HTTP request to stderr")
|
|
root.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
|
|
if err := a.checkOutputFlags(cmd); err != nil {
|
|
return err
|
|
}
|
|
if cmd.Flags().Changed("sudo") {
|
|
if a.options.sudo == "" || strings.IndexFunc(a.options.sudo, func(r rune) bool { return unicode.IsSpace(r) || unicode.IsControl(r) }) >= 0 {
|
|
return clierr.Wrap(clierr.Usage, errors.New("--sudo requires a username"))
|
|
}
|
|
for c := cmd; c.HasParent(); c = c.Parent() {
|
|
if c.Name() == "auth" && !c.Parent().HasParent() {
|
|
return clierr.Wrap(clierr.Usage, errors.New("--sudo cannot be used with auth commands"))
|
|
}
|
|
}
|
|
}
|
|
if offline(cmd) {
|
|
return nil
|
|
}
|
|
snapshot, err := a.deps.Settings.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return a.policy(snapshot).Validate(a.deps.Streams)
|
|
}
|
|
root.AddCommand(a.configCommand(), a.aliasCommand(), a.authCommand(), a.apiCommand(), a.repoCommand(), a.issueCommand(), a.prCommand(), a.doctorCommand(), a.workflowCommand(), a.runCommand(), a.labelCommand(), a.milestoneCommand(), a.sshKeyCommand(), a.timeCommand(), a.wikiCommand(), a.actionValueCommand(true), a.actionValueCommand(false), a.releaseCommand(), a.organizationCommand(), a.notificationCommand(), a.teamCommand(), a.branchCommand(), a.webhookCommand(), a.adminCommand(), a.searchCommand(), a.statusCommand(false))
|
|
root.AddCommand(a.manCommand())
|
|
root.AddCommand(&cobra.Command{Use: "version", Short: "Print tea's version", Example: "$ tea version", Annotations: textOutput("version prints text"), RunE: func(cmd *cobra.Command, _ []string) error {
|
|
_, err := fmt.Fprintln(cmd.OutOrStdout(), version.Format())
|
|
return err
|
|
}})
|
|
root.AddCommand(helpTopics(flags)...)
|
|
root.SetHelpCommand(a.helpCommand())
|
|
root.InitDefaultHelpCmd()
|
|
root.InitDefaultCompletionCmd()
|
|
root.SetVersionTemplate("{{.Version}}\n")
|
|
root.SetHelpFunc(a.writeHelp)
|
|
root.AddGroup(
|
|
&cobra.Group{ID: "core", Title: "Core commands"},
|
|
&cobra.Group{ID: "actions", Title: "Actions commands"},
|
|
&cobra.Group{ID: "additional", Title: "Additional commands"},
|
|
&cobra.Group{ID: "alias", Title: "Alias commands"},
|
|
)
|
|
for _, cmd := range root.Commands() {
|
|
if cmd.Annotations[topicAnnotation] == "" {
|
|
cmd.GroupID = cmp.Or(rootGroups[cmd.Name()], "additional")
|
|
}
|
|
}
|
|
rejectUnknownSubcommands(root)
|
|
addVerbAliases(root)
|
|
return root
|
|
}
|
|
|
|
// rootGroups places the core and Actions commands in their root help groups (D26); every other
|
|
// command is additional.
|
|
var rootGroups = map[string]string{
|
|
"auth": "core", "issue": "core", "org": "core", "pr": "core", "release": "core", "repo": "core",
|
|
"run": "actions", "secret": "actions", "variable": "actions", "workflow": "actions",
|
|
}
|
|
|
|
// verbAliases are the gh-style short names that every command with a common verb accepts (D6).
|
|
// Commands add other gh verbs, such as new, co, add and get, to their own Aliases.
|
|
var verbAliases = map[string]string{"list": "ls", "delete": "rm"}
|
|
|
|
// addVerbAliases gives every list and delete command its short name.
|
|
func addVerbAliases(cmd *cobra.Command) {
|
|
if alias, ok := verbAliases[cmd.Name()]; ok && cmd.HasParent() && !slices.Contains(cmd.Aliases, alias) {
|
|
cmd.Aliases = append(cmd.Aliases, alias)
|
|
}
|
|
for _, child := range cmd.Commands() {
|
|
addVerbAliases(child)
|
|
}
|
|
}
|
|
|
|
// flagAliases makes the flags of cmd accept each key of aliases as another name of the flag it maps
|
|
// to, without showing it in help. Renamed flags keep their old names working this way, such as the
|
|
// plural --add-labels after the singular gh name --add-label became primary (D8).
|
|
func flagAliases(cmd *cobra.Command, aliases map[string]string) {
|
|
normalize := cmd.Flags().GetNormalizeFunc()
|
|
cmd.Flags().SetNormalizeFunc(func(flags *pflag.FlagSet, name string) pflag.NormalizedName {
|
|
if to, ok := aliases[name]; ok {
|
|
name = to
|
|
}
|
|
return normalize(flags, name)
|
|
})
|
|
}
|
|
|
|
// checkOutputFlags validates --json and --jq before a command runs. --jq alone selects every field,
|
|
// except on commands that filter their raw JSON themselves (annotation tea.rawjson).
|
|
func (a *application) checkOutputFlags(cmd *cobra.Command) error {
|
|
selected := ""
|
|
switch {
|
|
case cmd.Flags().Changed("json"):
|
|
if a.options.json == "" {
|
|
return clierr.Wrap(clierr.Usage, errors.New("--json= needs field names; use --json alone for every field"))
|
|
}
|
|
selected = "--json"
|
|
case a.options.jq != "" && cmd.Annotations["tea.rawjson"] == "":
|
|
selected, a.options.json = "--jq", "*"
|
|
}
|
|
if reason := cmd.Annotations["tea.text"]; reason != "" && selected != "" {
|
|
return clierr.Wrap(clierr.Usage, fmt.Errorf("%s; %s is not supported", reason, selected))
|
|
}
|
|
if flag := cmd.Flags().Lookup("web"); flag != nil && flag.Value.String() == "true" && (a.options.json != "" || a.options.jq != "") {
|
|
return clierr.Wrap(clierr.Usage, errors.New("--web cannot be combined with --json or --jq"))
|
|
}
|
|
return clierr.Wrap(clierr.Usage, terminal.ValidateOutput(a.options.json, a.options.jq, commandOutputFields(cmd)))
|
|
}
|
|
|
|
// offline reports commands that must work without valid settings.
|
|
func offline(cmd *cobra.Command) bool {
|
|
if cmd.Annotations["tea.group"] == "true" {
|
|
return true
|
|
}
|
|
switch cmd.Name() {
|
|
case "help", "version", "man", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
|
|
return true
|
|
}
|
|
return cmd.HasParent() && cmd.Parent().Name() == "completion"
|
|
}
|
|
|
|
// rejectUnknownSubcommands makes `tea bogus` and `tea issue bogus` usage errors instead of printing help.
|
|
func rejectUnknownSubcommands(cmd *cobra.Command) {
|
|
if cmd.HasSubCommands() && !cmd.Runnable() {
|
|
if cmd.Annotations == nil {
|
|
cmd.Annotations = map[string]string{}
|
|
}
|
|
cmd.Annotations["tea.group"] = "true"
|
|
cmd.Args, cmd.SuggestionsMinimumDistance = cobra.ArbitraryArgs, 2
|
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
|
if len(args) == 0 {
|
|
return cmd.Help()
|
|
}
|
|
err := fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath())
|
|
if suggestions := cmd.SuggestionsFor(args[0]); len(suggestions) > 0 {
|
|
err = fmt.Errorf("%w; did you mean %s?", err, strings.Join(suggestions, " or "))
|
|
}
|
|
return clierr.Wrap(clierr.Usage, err)
|
|
}
|
|
}
|
|
for _, child := range cmd.Commands() {
|
|
rejectUnknownSubcommands(child)
|
|
}
|
|
}
|
|
|
|
func builtins(root *cobra.Command) map[string]bool {
|
|
names := map[string]bool{"help": true, "completion": true}
|
|
for _, cmd := range root.Commands() {
|
|
if cmd.Annotations["tea.alias"] != "true" {
|
|
names[cmd.Name()] = true
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
// Execute expands aliases and executes the fresh root without printing errors.
|
|
func Execute(ctx context.Context, deps Dependencies, args []string) error {
|
|
root := NewRoot(deps)
|
|
if at := commandIndex(root, args); at == len(args) || (args[at] != cobra.ShellCompRequestCmd && args[at] != cobra.ShellCompNoDescRequestCmd) {
|
|
args = joinJSONFields(root, args)
|
|
}
|
|
// Broken settings or aliases must not prevent help or recovery commands.
|
|
snapshot, loadErr := deps.Settings.Load()
|
|
names := builtins(root)
|
|
if commandAt := commandIndex(root, args); commandAt < len(args) {
|
|
name := args[commandAt]
|
|
completion := name == cobra.ShellCompRequestCmd || name == cobra.ShellCompNoDescRequestCmd
|
|
switch {
|
|
case loadErr != nil:
|
|
// An alias cannot be expanded without settings; report why instead of "unknown command".
|
|
if !names[name] && !completion {
|
|
return loadErr
|
|
}
|
|
case completion:
|
|
completionArgs := args[commandAt+1:]
|
|
if aliasAt := commandIndex(root, completionArgs); aliasAt < len(completionArgs) {
|
|
if expanded, err := aliases.ExpandCompletion(completionArgs[aliasAt:], snapshot.Preferences.Aliases, names); err == nil {
|
|
args = slices.Concat(args[:commandAt+1], completionArgs[:aliasAt], expanded)
|
|
}
|
|
}
|
|
default:
|
|
expanded, err := aliases.Expand(args[commandAt:], snapshot.Preferences.Aliases, names)
|
|
if err != nil {
|
|
return clierr.Wrap(clierr.Usage, err)
|
|
}
|
|
args = joinJSONFields(root, slices.Concat(args[:commandAt], expanded))
|
|
}
|
|
}
|
|
if loadErr == nil {
|
|
for name, expansion := range snapshot.Preferences.Aliases {
|
|
if aliases.Check(name, snapshot.Preferences.Aliases, names) == nil {
|
|
root.AddCommand(&cobra.Command{Use: name, Short: fmt.Sprintf("Alias for %q", expansion), GroupID: "alias", Annotations: map[string]string{"tea.alias": "true"}, RunE: func(*cobra.Command, []string) error {
|
|
return clierr.Wrap(clierr.Usage, errors.New("alias must be expanded before execution"))
|
|
}})
|
|
}
|
|
}
|
|
}
|
|
root.SetArgs(args)
|
|
err := root.ExecuteContext(ctx)
|
|
if apiErr, ok := errors.AsType[*transport.APIError](err); ok && apiErr.Status == 401 {
|
|
return clierr.Wrap(clierr.Authentication, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
var jsonFieldList = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*(,[A-Za-z][A-Za-z0-9_]*)*$`)
|
|
|
|
// joinJSONFields accepts `--json number,title` like `--json=number,title`. A field-shaped word after a
|
|
// bare --json stays a positional argument only when the selected command does not output those fields
|
|
// and accepts the word as an argument, so a misspelled field reports the available fields rather than
|
|
// an argument count. A bare --json still selects every field. Unknown commands, such as aliases before
|
|
// expansion, are joined.
|
|
func joinJSONFields(root *cobra.Command, args []string) []string {
|
|
for i := 0; i < len(args); i++ {
|
|
if args[i] == "--" {
|
|
break
|
|
}
|
|
if args[i] != "--json" || i+1 >= len(args) || !jsonFieldList.MatchString(args[i+1]) {
|
|
continue
|
|
}
|
|
cmd, _, err := root.Find(slices.Concat(args[:i+1], args[i+2:]))
|
|
if err == nil && terminal.ValidateOutput(args[i+1], "", commandOutputFields(cmd)) != nil && acceptsArgs(args) {
|
|
continue
|
|
}
|
|
args = slices.Concat(args[:i], []string{"--json=" + args[i+1]}, args[i+2:])
|
|
}
|
|
return args
|
|
}
|
|
|
|
// acceptsArgs reports whether the command that args select accepts their positional arguments. It
|
|
// parses a separate command tree, because parsing sets the flags of the one that runs.
|
|
func acceptsArgs(args []string) bool {
|
|
cmd, rest, err := NewRoot(Dependencies{}).Find(args)
|
|
return err == nil && cmd.ParseFlags(rest) == nil && cmd.ValidateArgs(cmd.Flags().Args()) == nil
|
|
}
|
|
|
|
// confirm asks for confirmation under the prompt policy unless --yes was supplied.
|
|
func (a *application) confirm(cmd *cobra.Command, description string) error {
|
|
s, err := a.deps.Settings.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return terminal.Confirm(cmd.Context(), description, a.options.yes, a.deps.Streams, a.policy(s), a.deps.Prompter)
|
|
}
|
|
|
|
// confirmTyped asks the user to type answer before an irreversible action unless --yes was supplied.
|
|
func (a *application) confirmTyped(cmd *cobra.Command, description, answer string) error {
|
|
if a.options.yes {
|
|
return nil
|
|
}
|
|
s, err := a.deps.Settings.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !a.policy(s).CanPrompt(a.deps.Streams) {
|
|
return clierr.Wrap(clierr.Usage, errors.New("confirmation required: supply --yes"))
|
|
}
|
|
typed, err := a.deps.Prompter.Input(cmd.Context(), description+" Type "+answer+" to confirm:")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !strings.EqualFold(strings.TrimSpace(typed), answer) {
|
|
return clierr.Wrap(clierr.Canceled, errors.New("operation canceled"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// require prompts for a missing required value under the prompt policy.
|
|
func (a *application) require(cmd *cobra.Command, value *string, flag string) (bool, error) {
|
|
s, err := a.deps.Settings.Load()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return terminal.Require(cmd.Context(), value, flag, a.deps.Streams, a.policy(s), a.deps.Prompter)
|
|
}
|
|
|
|
func (a *application) policy(snapshot settings.Snapshot) terminal.Policy {
|
|
return terminal.Policy{Enabled: snapshot.Preferences.Prompt == "enabled", NoInput: a.options.noInput, Machine: a.options.json != "", Interactive: a.options.interactive}
|
|
}
|
|
|
|
// titleCase capitalizes an ASCII command word for help and status text.
|
|
func titleCase(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
return strings.ToUpper(s[:1]) + s[1:]
|
|
}
|
|
|
|
// textOutput marks a command whose output is not a JSON resource, so --json is rejected up front.
|
|
func textOutput(reason string) map[string]string {
|
|
return map[string]string{"tea.text": reason}
|
|
}
|
|
|
|
// commandIndex skips persistent flags without parsing or mutating command state.
|
|
func commandIndex(root *cobra.Command, args []string) int {
|
|
for i := 0; i < len(args); i++ {
|
|
arg := args[i]
|
|
if arg == "--" {
|
|
return i + 1
|
|
}
|
|
if !strings.HasPrefix(arg, "-") || arg == "-" {
|
|
return i
|
|
}
|
|
name := strings.TrimLeft(arg, "-")
|
|
if strings.Contains(name, "=") {
|
|
continue
|
|
}
|
|
flag := root.PersistentFlags().Lookup(name)
|
|
if flag == nil && len(name) > 0 && !strings.HasPrefix(arg, "--") {
|
|
flag = root.PersistentFlags().ShorthandLookup(name[:1])
|
|
if len(name) > 1 {
|
|
continue
|
|
}
|
|
}
|
|
if flag != nil && flag.NoOptDefVal == "" {
|
|
i++
|
|
}
|
|
}
|
|
return len(args)
|
|
}
|
|
|
|
// configKeys are the preferences, instance settings and account settings that config get reads.
|
|
var configKeys = []string{
|
|
"browser", "color", "default_host", "editor", "git_protocol", "insecure", "oauth_client_id", "pager", "prompt",
|
|
"remote", "ssh_agent", "ssh_certificate_principal", "ssh_host", "ssh_key", "ssh_key_fingerprint", "version",
|
|
}
|
|
|
|
func commandOutputFields(cmd *cobra.Command) []string {
|
|
if cmd.Name() == "status" && cmd.Parent() != nil {
|
|
if cmd.Parent().Name() == "pr" {
|
|
return jsonFields[services.PullStatus]()
|
|
}
|
|
if cmd.Parent().Parent() == nil {
|
|
return jsonFields[services.AccountStatus]()
|
|
}
|
|
}
|
|
if cmd.Parent() == nil {
|
|
return nil
|
|
}
|
|
if cmd.Parent().Name() == "search" {
|
|
if cmd.Name() == "repo" {
|
|
return jsonFields[services.Repository]()
|
|
}
|
|
return jsonFields[services.SearchItem]()
|
|
}
|
|
switch cmd.Parent().Name() {
|
|
case "auth":
|
|
if cmd.Name() == "status" {
|
|
return jsonFields[authStatusRow]()
|
|
}
|
|
case "config":
|
|
if cmd.Name() == "get" || cmd.Name() == "list" {
|
|
return configKeys
|
|
}
|
|
case "tea":
|
|
if cmd.Name() == "doctor" {
|
|
return jsonFields[target]()
|
|
}
|
|
case "attachment":
|
|
return jsonFields[services.IssueAttachment]()
|
|
case "artifact":
|
|
return jsonFields[services.Artifact]()
|
|
case "user":
|
|
if cmd.Parent().Parent() != nil && cmd.Parent().Parent().Name() == "admin" {
|
|
return jsonFields[services.AdminUser]()
|
|
}
|
|
case "webhook":
|
|
return jsonFields[services.Webhook]()
|
|
case "branch":
|
|
return jsonFields[services.Branch]()
|
|
case "team":
|
|
return jsonFields[services.Team]()
|
|
case "notification":
|
|
return jsonFields[services.Notification]()
|
|
case "org":
|
|
return jsonFields[services.Organization]()
|
|
case "member":
|
|
return jsonFields[services.OrganizationMember]()
|
|
case "asset":
|
|
return jsonFields[services.ReleaseAsset]()
|
|
case "release":
|
|
return jsonFields[services.Release]()
|
|
case "secret":
|
|
return jsonFields[services.ActionSecret]()
|
|
case "variable":
|
|
return jsonFields[services.ActionVariable]()
|
|
case "wiki":
|
|
if cmd.Name() == "revisions" {
|
|
return jsonFields[services.WikiRevision]()
|
|
}
|
|
return jsonFields[services.WikiPage]()
|
|
case "time":
|
|
return jsonFields[services.TrackedTime]()
|
|
case "ssh-key":
|
|
return jsonFields[services.SSHKey]()
|
|
case "comment":
|
|
return jsonFields[services.Comment]()
|
|
case "issue":
|
|
return jsonFields[services.Issue]()
|
|
case "review":
|
|
if cmd.Name() == "request" || cmd.Name() == "cancel-request" {
|
|
return jsonFields[services.Pull]()
|
|
}
|
|
if cmd.Name() == "comments" || cmd.Name() == "reply" {
|
|
return jsonFields[services.ReviewComment]()
|
|
}
|
|
return jsonFields[services.Review]()
|
|
case "pr":
|
|
switch cmd.Name() {
|
|
case "checkout":
|
|
return jsonFields[git.CheckoutResult]()
|
|
case "checks":
|
|
return jsonFields[services.Check]()
|
|
case "review":
|
|
return jsonFields[services.Review]()
|
|
case "clean":
|
|
return jsonFields[git.CleanResult]()
|
|
}
|
|
return jsonFields[services.Pull]()
|
|
case "repo":
|
|
if cmd.Name() == "sync" {
|
|
return jsonFields[git.SyncResult]()
|
|
}
|
|
return jsonFields[services.Repository]()
|
|
case "label":
|
|
return jsonFields[services.Label]()
|
|
case "milestone":
|
|
return jsonFields[services.Milestone]()
|
|
case "workflow":
|
|
if cmd.Name() == "run" {
|
|
return jsonFields[services.DispatchResult]()
|
|
}
|
|
return jsonFields[services.Workflow]()
|
|
case "run":
|
|
if cmd.Name() == "jobs" {
|
|
return jsonFields[services.Job]()
|
|
}
|
|
return jsonFields[services.Run]()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// jsonFields lists the JSON names of T's fields, which --json can select, in alphabetical order.
|
|
func jsonFields[T any]() []string {
|
|
t := reflect.TypeFor[T]()
|
|
fields := make([]string, 0, t.NumField())
|
|
for field := range t.Fields() {
|
|
if name, _, _ := strings.Cut(field.Tag.Get("json"), ","); name != "" && name != "-" {
|
|
fields = append(fields, name)
|
|
}
|
|
}
|
|
slices.Sort(fields)
|
|
return fields
|
|
}
|