diff --git a/internal/docs/man.go b/internal/docs/man.go index 6a259a0bd..633e149a1 100644 --- a/internal/docs/man.go +++ b/internal/docs/man.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/cli/cli/v2/pkg/cmd/root" "github.com/cpuguy83/go-md2man/v2/md2man" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -175,11 +176,8 @@ func genMan(cmd *cobra.Command, header *GenManHeader) []byte { buf := new(bytes.Buffer) manPreamble(buf, header, cmd, dashCommandName) - for _, g := range subcommandGroups(cmd) { - if len(g.Commands) == 0 { - continue - } - fmt.Fprintf(buf, "# %s\n", strings.ToUpper(g.Name)) + for _, g := range root.GroupedCommands(cmd) { + fmt.Fprintf(buf, "# %s\n", strings.ToUpper(g.Title)) for _, subcmd := range g.Commands { fmt.Fprintf(buf, "`%s`\n: %s\n\n", manLink(subcmd), subcmd.Short) } diff --git a/internal/docs/markdown.go b/internal/docs/markdown.go index e33df5f45..4312459c5 100644 --- a/internal/docs/markdown.go +++ b/internal/docs/markdown.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" + "github.com/cli/cli/v2/pkg/cmd/root" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -99,11 +100,8 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) fmt.Fprintf(w, "%s\n\n", cmd.Long) } - for _, g := range subcommandGroups(cmd) { - if len(g.Commands) == 0 { - continue - } - fmt.Fprintf(w, "### %s\n\n", g.Name) + for _, g := range root.GroupedCommands(cmd) { + fmt.Fprintf(w, "### %s\n\n", g.Title) for _, subcmd := range g.Commands { fmt.Fprintf(w, "* [%s](%s)\n", subcmd.CommandPath(), linkHandler(cmdManualPath(subcmd))) } @@ -130,56 +128,6 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) return nil } -type commandGroup struct { - Name string - Commands []*cobra.Command -} - -// subcommandGroups lists child commands of a Cobra command split into groups. -// TODO: have rootHelpFunc use this instead of repeating the same logic. -func subcommandGroups(c *cobra.Command) []commandGroup { - var rest []*cobra.Command - var core []*cobra.Command - var actions []*cobra.Command - - for _, subcmd := range c.Commands() { - if !subcmd.IsAvailableCommand() { - continue - } - if _, ok := subcmd.Annotations["IsCore"]; ok { - core = append(core, subcmd) - } else if _, ok := subcmd.Annotations["IsActions"]; ok { - actions = append(actions, subcmd) - } else { - rest = append(rest, subcmd) - } - } - - if len(core) > 0 { - return []commandGroup{ - { - Name: "Core commands", - Commands: core, - }, - { - Name: "Actions commands", - Commands: actions, - }, - { - Name: "Additional commands", - Commands: rest, - }, - } - } - - return []commandGroup{ - { - Name: "Commands", - Commands: rest, - }, - } -} - // GenMarkdownTree will generate a markdown page for this command and all // descendants in the directory given. The header may be nil. // This function may not work correctly if your command names have `-` in them. diff --git a/pkg/cmd/auth/auth.go b/pkg/cmd/auth/auth.go index 9b344a636..c003049eb 100644 --- a/pkg/cmd/auth/auth.go +++ b/pkg/cmd/auth/auth.go @@ -14,11 +14,9 @@ import ( func NewCmdAuth(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ - Use: "auth ", - Short: "Authenticate gh and git with GitHub", - Annotations: map[string]string{ - "IsCore": "true", - }, + Use: "auth ", + Short: "Authenticate gh and git with GitHub", + GroupID: "core", } cmdutil.DisableAuthCheck(cmd) diff --git a/pkg/cmd/browse/browse.go b/pkg/cmd/browse/browse.go index 85572fdac..de18ad416 100644 --- a/pkg/cmd/browse/browse.go +++ b/pkg/cmd/browse/browse.go @@ -76,7 +76,6 @@ func NewCmdBrowse(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co #=> Open main.go in the main branch `), Annotations: map[string]string{ - "IsCore": "true", "help:arguments": heredoc.Doc(` A browser location can be specified using arguments in the following format: - by number for issue or pull request, e.g. "123"; or @@ -86,6 +85,7 @@ func NewCmdBrowse(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co To configure a web browser other than the default, use the BROWSER environment variable. `), }, + GroupID: "core", RunE: func(cmd *cobra.Command, args []string) error { opts.BaseRepo = f.BaseRepo diff --git a/pkg/cmd/gist/gist.go b/pkg/cmd/gist/gist.go index 3b0d5711c..f637a0ffb 100644 --- a/pkg/cmd/gist/gist.go +++ b/pkg/cmd/gist/gist.go @@ -18,13 +18,13 @@ func NewCmdGist(f *cmdutil.Factory) *cobra.Command { Short: "Manage gists", Long: `Work with GitHub gists.`, Annotations: map[string]string{ - "IsCore": "true", "help:arguments": heredoc.Doc(` A gist can be supplied as argument in either of the following formats: - by ID, e.g. 5b0e0062eb8e9654adad7bb1d81cc75f - by URL, e.g. "https://gist.github.com/OWNER/5b0e0062eb8e9654adad7bb1d81cc75f" `), }, + GroupID: "core", } cmd.AddCommand(gistCloneCmd.NewCmdClone(f, nil)) diff --git a/pkg/cmd/issue/issue.go b/pkg/cmd/issue/issue.go index adff7dac3..8847bf3bf 100644 --- a/pkg/cmd/issue/issue.go +++ b/pkg/cmd/issue/issue.go @@ -30,13 +30,13 @@ func NewCmdIssue(f *cmdutil.Factory) *cobra.Command { $ gh issue view 123 --web `), Annotations: map[string]string{ - "IsCore": "true", "help:arguments": heredoc.Doc(` An issue can be supplied as argument in any of the following formats: - by number, e.g. "123"; or - by URL, e.g. "https://github.com/OWNER/REPO/issues/123". `), }, + GroupID: "core", } cmdutil.EnableRepoOverride(cmd, f) diff --git a/pkg/cmd/pr/pr.go b/pkg/cmd/pr/pr.go index 43090b690..b46c71e9e 100644 --- a/pkg/cmd/pr/pr.go +++ b/pkg/cmd/pr/pr.go @@ -31,7 +31,6 @@ func NewCmdPR(f *cmdutil.Factory) *cobra.Command { $ gh pr view --web `), Annotations: map[string]string{ - "IsCore": "true", "help:arguments": heredoc.Doc(` A pull request can be supplied as argument in any of the following formats: - by number, e.g. "123"; @@ -39,6 +38,7 @@ func NewCmdPR(f *cmdutil.Factory) *cobra.Command { - by the name of its head branch, e.g. "patch-1" or "OWNER:patch-1". `), }, + GroupID: "core", } cmdutil.EnableRepoOverride(cmd, f) diff --git a/pkg/cmd/release/release.go b/pkg/cmd/release/release.go index 861cc2d7b..19916fcce 100644 --- a/pkg/cmd/release/release.go +++ b/pkg/cmd/release/release.go @@ -15,11 +15,9 @@ import ( func NewCmdRelease(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ - Use: "release ", - Short: "Manage releases", - Annotations: map[string]string{ - "IsCore": "true", - }, + Use: "release ", + Short: "Manage releases", + GroupID: "core", } cmdutil.EnableRepoOverride(cmd, f) diff --git a/pkg/cmd/repo/repo.go b/pkg/cmd/repo/repo.go index e8696d7d9..51fc5863e 100644 --- a/pkg/cmd/repo/repo.go +++ b/pkg/cmd/repo/repo.go @@ -30,13 +30,13 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command { $ gh repo view --web `), Annotations: map[string]string{ - "IsCore": "true", "help:arguments": heredoc.Doc(` A repository can be supplied as an argument in any of the following formats: - "OWNER/REPO" - by URL, e.g. "https://github.com/OWNER/REPO" `), }, + GroupID: "core", } cmd.AddCommand(repoViewCmd.NewCmdView(f, nil)) diff --git a/pkg/cmd/root/help.go b/pkg/cmd/root/help.go index 40fbec01d..bec40c95a 100644 --- a/pkg/cmd/root/help.go +++ b/pkg/cmd/root/help.go @@ -102,32 +102,6 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) { } namePadding := 12 - coreCommands := []string{} - actionsCommands := []string{} - additionalCommands := []string{} - for _, c := range command.Commands() { - if c.Short == "" { - continue - } - if c.Hidden { - continue - } - - s := rpad(c.Name()+":", namePadding) + c.Short - if _, ok := c.Annotations["IsCore"]; ok { - coreCommands = append(coreCommands, s) - } else if _, ok := c.Annotations["IsActions"]; ok { - actionsCommands = append(actionsCommands, s) - } else { - additionalCommands = append(additionalCommands, s) - } - } - - // If there are no core commands, assume everything is a core command - if len(coreCommands) == 0 { - coreCommands = additionalCommands - additionalCommands = []string{} - } type helpEntry struct { Title string @@ -148,14 +122,16 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) { helpEntries = append(helpEntries, helpEntry{"", longText}) } helpEntries = append(helpEntries, helpEntry{"USAGE", command.UseLine()}) - if len(coreCommands) > 0 { - helpEntries = append(helpEntries, helpEntry{"CORE COMMANDS", strings.Join(coreCommands, "\n")}) - } - if len(actionsCommands) > 0 { - helpEntries = append(helpEntries, helpEntry{"ACTIONS COMMANDS", strings.Join(actionsCommands, "\n")}) - } - if len(additionalCommands) > 0 { - helpEntries = append(helpEntries, helpEntry{"ADDITIONAL COMMANDS", strings.Join(additionalCommands, "\n")}) + + for _, g := range GroupedCommands(command) { + var names []string + for _, c := range g.Commands { + names = append(names, rpad(c.Name()+":", namePadding)+c.Short) + } + helpEntries = append(helpEntries, helpEntry{ + Title: strings.ToUpper(g.Title), + Body: strings.Join(names, "\n"), + }) } if isRootCmd(command) { @@ -225,6 +201,49 @@ func findCommand(cmd *cobra.Command, name string) *cobra.Command { return nil } +type CommandGroup struct { + Title string + Commands []*cobra.Command +} + +func GroupedCommands(cmd *cobra.Command) []CommandGroup { + var res []CommandGroup + + for _, g := range cmd.Groups() { + var cmds []*cobra.Command + for _, c := range cmd.Commands() { + if c.GroupID == g.ID && c.IsAvailableCommand() { + cmds = append(cmds, c) + } + } + if len(cmds) > 0 { + res = append(res, CommandGroup{ + Title: g.Title, + Commands: cmds, + }) + } + } + + var cmds []*cobra.Command + for _, c := range cmd.Commands() { + if c.GroupID == "" && c.IsAvailableCommand() { + cmds = append(cmds, c) + } + } + if len(cmds) > 0 { + defaultGroupTitle := "Additional commands" + if len(cmd.Groups()) == 0 { + defaultGroupTitle = "Available commands" + } + res = append(res, CommandGroup{ + Title: defaultGroupTitle, + Commands: cmds, + }) + } + + return res +} + // rpad adds padding to the right of a string. func rpad(s string, padding int) string { template := fmt.Sprintf("%%-%ds ", padding) diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 68f60eaa2..3474d66c2 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -43,8 +43,6 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { Short: "GitHub CLI", Long: `Work seamlessly with GitHub from the command line.`, - SilenceErrors: true, - SilenceUsage: true, Example: heredoc.Doc(` $ gh issue create $ gh repo clone cli/cli @@ -61,15 +59,33 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { // cmd.SetOut(f.IOStreams.Out) // can't use due to https://github.com/spf13/cobra/issues/1708 // cmd.SetErr(f.IOStreams.ErrOut) // just let it default to os.Stderr instead - cmd.Flags().Bool("version", false, "Show gh version") cmd.PersistentFlags().Bool("help", false, "Show help for command") - cmd.SetHelpFunc(func(c *cobra.Command, args []string) { - rootHelpFunc(f, c, args) + + // override Cobra's default behaviors unless an opt-out has been set + if os.Getenv("GH_COBRA") == "" { + cmd.SilenceErrors = true + cmd.SilenceUsage = true + + // this --version flag is checked in rootHelpFunc + cmd.Flags().Bool("version", false, "Show gh version") + + cmd.SetHelpFunc(func(c *cobra.Command, args []string) { + rootHelpFunc(f, c, args) + }) + cmd.SetUsageFunc(func(c *cobra.Command) error { + return rootUsageFunc(f.IOStreams.ErrOut, c) + }) + cmd.SetFlagErrorFunc(rootFlagErrorFunc) + } + + cmd.AddGroup(&cobra.Group{ + ID: "core", + Title: "Core commands", }) - cmd.SetUsageFunc(func(c *cobra.Command) error { - return rootUsageFunc(f.IOStreams.ErrOut, c) + cmd.AddGroup(&cobra.Group{ + ID: "actions", + Title: "GitHub Actions commands", }) - cmd.SetFlagErrorFunc(rootFlagErrorFunc) // Child commands cmd.AddCommand(versionCmd.NewCmdVersion(f, version, buildDate)) @@ -158,7 +174,7 @@ func newCodespaceCmd(f *cmdutil.Factory) *cobra.Command { cmd := codespaceCmd.NewRootCmd(app) cmd.Use = "codespace" cmd.Aliases = []string{"cs"} - cmd.Annotations = map[string]string{"IsCore": "true"} + cmd.GroupID = "core" return cmd } diff --git a/pkg/cmd/run/run.go b/pkg/cmd/run/run.go index 649d70f0d..f65400595 100644 --- a/pkg/cmd/run/run.go +++ b/pkg/cmd/run/run.go @@ -13,12 +13,10 @@ import ( func NewCmdRun(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ - Use: "run ", - Short: "View details about workflow runs", - Long: "List, view, and watch recent workflow runs from GitHub Actions.", - Annotations: map[string]string{ - "IsActions": "true", - }, + Use: "run ", + Short: "View details about workflow runs", + Long: "List, view, and watch recent workflow runs from GitHub Actions.", + GroupID: "actions", } cmdutil.EnableRepoOverride(cmd, f) diff --git a/pkg/cmd/workflow/workflow.go b/pkg/cmd/workflow/workflow.go index 964034840..ccdb7d34a 100644 --- a/pkg/cmd/workflow/workflow.go +++ b/pkg/cmd/workflow/workflow.go @@ -12,12 +12,10 @@ import ( func NewCmdWorkflow(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ - Use: "workflow ", - Short: "View details about GitHub Actions workflows", - Long: "List, view, and run workflows in GitHub Actions.", - Annotations: map[string]string{ - "IsActions": "true", - }, + Use: "workflow ", + Short: "View details about GitHub Actions workflows", + Long: "List, view, and run workflows in GitHub Actions.", + GroupID: "actions", } cmdutil.EnableRepoOverride(cmd, f)