diff --git a/internal/app/app.go b/internal/app/app.go index 767b9a20..c41ffd60 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -26,6 +26,7 @@ import ( "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. @@ -149,9 +150,37 @@ func NewRoot(deps Dependencies) *cobra.Command { root.InitDefaultCompletionCmd() root.SetVersionTemplate("{{.Version}}\n") rejectUnknownSubcommands(root) + addVerbAliases(root) return root } +// 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 { diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 831e51be..1e8054c3 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -192,3 +192,39 @@ func TestRawJSONCommandsKeepJQToThemselves(t *testing.T) { root.SetArgs([]string{"raw", "--json"}) require.ErrorContains(t, root.Execute(), "raw prints text; --json is not supported") } + +func TestVerbAndFlagAliases(t *testing.T) { + deps, _, _ := testDeps(t) + root := NewRoot(deps) + for _, args := range [][]string{{"issue", "ls"}, {"label", "rm"}, {"pr", "review", "ls"}} { + cmd, _, err := root.Find(args) + require.NoError(t, err) + require.Equal(t, map[string]string{"ls": "list", "rm": "delete"}[args[len(args)-1]], cmd.Name(), args) + } + // Aliases never shadow a sibling command. + walkCommands(root, func(cmd *cobra.Command) { + names := map[string]string{} + for _, child := range cmd.Commands() { + for _, name := range append([]string{child.Name()}, child.Aliases...) { + require.Empty(t, names[name], "%s %s is both %s and %s", cmd.CommandPath(), name, names[name], child.Name()) + names[name] = child.Name() + } + } + }) + var labels []string + edit := &cobra.Command{Use: "edit", RunE: func(*cobra.Command, []string) error { return nil }} + edit.Flags().StringSliceVar(&labels, "add-label", nil, "Add labels") + flagAliases(edit, map[string]string{"add-labels": "add-label"}) + edit.SetArgs([]string{"--add-labels", "bug", "--add-label", "docs"}) + require.NoError(t, edit.Execute()) + require.Equal(t, []string{"bug", "docs"}, labels) + require.NotContains(t, edit.UsageString(), "add-labels") +} + +// walkCommands calls visit for cmd and each of its descendants. +func walkCommands(cmd *cobra.Command, visit func(*cobra.Command)) { + visit(cmd) + for _, child := range cmd.Commands() { + walkCommands(child, visit) + } +}