Improve CLI commands (#34973)

Improve help related commands and flags and add tests

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
TheFox0x7 2025-07-10 13:36:55 +02:00 committed by GitHub
parent 091b3e696d
commit 4b174e44a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 116 additions and 168 deletions

View File

@ -32,6 +32,7 @@ var (
CmdHook = &cli.Command{ CmdHook = &cli.Command{
Name: "hook", Name: "hook",
Usage: "(internal) Should only be called by Git", Usage: "(internal) Should only be called by Git",
Hidden: true, // internal commands shouldn't be visible
Description: "Delegate commands to corresponding Git hooks", Description: "Delegate commands to corresponding Git hooks",
Before: PrepareConsoleLoggerLevel(log.FATAL), Before: PrepareConsoleLoggerLevel(log.FATAL),
Commands: []*cli.Command{ Commands: []*cli.Command{

View File

@ -19,6 +19,7 @@ import (
var CmdKeys = &cli.Command{ var CmdKeys = &cli.Command{
Name: "keys", Name: "keys",
Usage: "(internal) Should only be called by SSH server", Usage: "(internal) Should only be called by SSH server",
Hidden: true, // internal commands shouldn't not be visible
Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint", Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint",
Before: PrepareConsoleLoggerLevel(log.FATAL), Before: PrepareConsoleLoggerLevel(log.FATAL),
Action: runKeys, Action: runKeys,

View File

@ -6,6 +6,7 @@ package cmd
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"os" "os"
"strings" "strings"
@ -15,26 +16,28 @@ import (
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
// cmdHelp is our own help subcommand with more information var cliHelpPrinterOld = cli.HelpPrinter
// Keep in mind that the "./gitea help"(subcommand) is different from "./gitea --help"(flag), the flag doesn't parse the config or output "DEFAULT CONFIGURATION:" information
func cmdHelp() *cli.Command { func init() {
c := &cli.Command{ cli.HelpPrinter = cliHelpPrinterNew
Name: "help",
Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]",
Action: func(ctx context.Context, c *cli.Command) (err error) {
lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea
targetCmdIdx := 0
if c.Name == "help" {
targetCmdIdx = 1
} }
if lineage[targetCmdIdx] != lineage[targetCmdIdx].Root() {
err = cli.ShowCommandHelp(ctx, lineage[targetCmdIdx+1] /* parent cmd */, lineage[targetCmdIdx].Name /* sub cmd */) // cliHelpPrinterNew helps to print "DEFAULT CONFIGURATION" for the following cases ( "-c" can apper in any position):
} else { // * ./gitea -c /dev/null -h
err = cli.ShowAppHelp(c) // * ./gitea -c help /dev/null help
// * ./gitea help -c /dev/null
// * ./gitea help -c /dev/null web
// * ./gitea help web -c /dev/null
// * ./gitea web help -c /dev/null
// * ./gitea web -h -c /dev/null
func cliHelpPrinterNew(out io.Writer, templ string, data any) {
cmd, _ := data.(*cli.Command)
if cmd != nil {
prepareWorkPathAndCustomConf(cmd)
} }
_, _ = fmt.Fprintf(c.Root().Writer, ` cliHelpPrinterOld(out, templ, data)
if setting.CustomConf != "" {
_, _ = fmt.Fprintf(out, `
DEFAULT CONFIGURATION: DEFAULT CONFIGURATION:
AppPath: %s AppPath: %s
WorkPath: %s WorkPath: %s
@ -42,75 +45,34 @@ DEFAULT CONFIGURATION:
ConfigFile: %s ConfigFile: %s
`, setting.AppPath, setting.AppWorkPath, setting.CustomPath, setting.CustomConf) `, setting.AppPath, setting.AppWorkPath, setting.CustomPath, setting.CustomConf)
return err
},
}
return c
}
func appGlobalFlags() []cli.Flag {
return []cli.Flag{
// make the builtin flags at the top
cli.HelpFlag,
// shared configuration flags, they are for global and for each sub-command at the same time
// eg: such command is valid: "./gitea --config /tmp/app.ini web --config /tmp/app.ini", while it's discouraged indeed
// keep in mind that the short flags like "-C", "-c" and "-w" are globally polluted, they can't be used for sub-commands anymore.
&cli.StringFlag{
Name: "custom-path",
Aliases: []string{"C"},
Usage: "Set custom path (defaults to '{WorkPath}/custom')",
},
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Value: setting.CustomConf,
Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')",
},
&cli.StringFlag{
Name: "work-path",
Aliases: []string{"w"},
Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)",
},
} }
} }
func prepareSubcommandWithGlobalFlags(command *cli.Command) { func prepareSubcommandWithGlobalFlags(originCmd *cli.Command) {
command.Flags = append(append([]cli.Flag{}, appGlobalFlags()...), command.Flags...) originBefore := originCmd.Before
command.Action = prepareWorkPathAndCustomConf(command.Action) originCmd.Before = func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
command.HideHelp = true prepareWorkPathAndCustomConf(cmd)
if command.Name != "help" { if originBefore != nil {
command.Commands = append(command.Commands, cmdHelp()) return originBefore(ctx, cmd)
} }
for i := range command.Commands { return ctx, nil
prepareSubcommandWithGlobalFlags(command.Commands[i])
} }
} }
// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config // prepareWorkPathAndCustomConf tries to prepare the work path, custom path and custom config from various inputs:
// It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times // command line flags, environment variables, config file
func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(context.Context, *cli.Command) error { func prepareWorkPathAndCustomConf(cmd *cli.Command) {
return func(ctx context.Context, cmd *cli.Command) error {
var args setting.ArgWorkPathAndCustomConf var args setting.ArgWorkPathAndCustomConf
// from children to parent, check the global flags if cmd.IsSet("work-path") {
for _, curCtx := range cmd.Lineage() { args.WorkPath = cmd.String("work-path")
if curCtx.IsSet("work-path") && args.WorkPath == "" {
args.WorkPath = curCtx.String("work-path")
} }
if curCtx.IsSet("custom-path") && args.CustomPath == "" { if cmd.IsSet("custom-path") {
args.CustomPath = curCtx.String("custom-path") args.CustomPath = cmd.String("custom-path")
}
if curCtx.IsSet("config") && args.CustomConf == "" {
args.CustomConf = curCtx.String("config")
} }
if cmd.IsSet("config") {
args.CustomConf = cmd.String("config")
} }
setting.InitWorkPathAndCommonConfig(os.Getenv, args) setting.InitWorkPathAndCommonConfig(os.Getenv, args)
if cmd.Bool("help") || action == nil {
// the default behavior of "urfave/cli": "nil action" means "show help"
return cmdHelp().Action(ctx, cmd)
}
return action(ctx, cmd)
}
} }
type AppVersion struct { type AppVersion struct {
@ -125,10 +87,29 @@ func NewMainApp(appVer AppVersion) *cli.Command {
app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.` app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
app.Version = appVer.Version + appVer.Extra app.Version = appVer.Version + appVer.Extra
app.EnableShellCompletion = true app.EnableShellCompletion = true
app.Flags = []cli.Flag{
// these sub-commands need to use config file &cli.StringFlag{
Name: "work-path",
Aliases: []string{"w"},
TakesFile: true,
Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)",
},
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
TakesFile: true,
Value: setting.CustomConf,
Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')",
},
&cli.StringFlag{
Name: "custom-path",
Aliases: []string{"C"},
TakesFile: true,
Usage: "Set custom path (defaults to '{WorkPath}/custom')",
},
}
// these sub-commands need to use a config file
subCmdWithConfig := []*cli.Command{ subCmdWithConfig := []*cli.Command{
cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config"
CmdWeb, CmdWeb,
CmdServ, CmdServ,
CmdHook, CmdHook,
@ -156,9 +137,6 @@ func NewMainApp(appVer AppVersion) *cli.Command {
// but not sure whether it would break Windows users who used to double-click the EXE to run. // but not sure whether it would break Windows users who used to double-click the EXE to run.
app.DefaultCommand = CmdWeb.Name app.DefaultCommand = CmdWeb.Name
app.Flags = append(app.Flags, cli.VersionFlag)
app.Flags = append(app.Flags, appGlobalFlags()...)
app.HideHelp = true // use our own help action to show helps (with more information like default config)
app.Before = PrepareConsoleLoggerLevel(log.INFO) app.Before = PrepareConsoleLoggerLevel(log.INFO)
for i := range subCmdWithConfig { for i := range subCmdWithConfig {
prepareSubcommandWithGlobalFlags(subCmdWithConfig[i]) prepareSubcommandWithGlobalFlags(subCmdWithConfig[i])

View File

@ -74,12 +74,56 @@ func TestCliCmd(t *testing.T) {
cmd string cmd string
exp string exp string
}{ }{
// main command help // help commands
{
cmd: "./gitea -h",
exp: "DEFAULT CONFIGURATION:",
},
{ {
cmd: "./gitea help", cmd: "./gitea help",
exp: "DEFAULT CONFIGURATION:", exp: "DEFAULT CONFIGURATION:",
}, },
{
cmd: "./gitea -c /dev/null -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea help -c /dev/null",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null test-cmd -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -c /dev/null -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -h -c /dev/null",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null test-cmd help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -c /dev/null help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd help -c /dev/null",
exp: "ConfigFile: /dev/null",
},
// parse paths // parse paths
{ {
cmd: "./gitea test-cmd", cmd: "./gitea test-cmd",

View File

@ -41,6 +41,7 @@ var CmdServ = &cli.Command{
Name: "serv", Name: "serv",
Usage: "(internal) Should only be called by SSH shell", Usage: "(internal) Should only be called by SSH shell",
Description: "Serv provides access auth for repositories", Description: "Serv provides access auth for repositories",
Hidden: true, // Internal commands shouldn't be visible in help
Before: PrepareConsoleLoggerLevel(log.FATAL), Before: PrepareConsoleLoggerLevel(log.FATAL),
Action: runServ, Action: runServ,
Flags: []cli.Flag{ Flags: []cli.Flag{

View File

@ -1,17 +0,0 @@
Bash and Zsh completion
=======================
From within the gitea root run:
```bash
source contrib/autocompletion/bash_autocomplete
```
or for zsh run:
```bash
source contrib/autocompletion/zsh_autocomplete
```
These scripts will check if gitea is on the path and if so add autocompletion for `gitea`. Or if not autocompletion will work for `./gitea`.
If gitea has been installed as a different program pass in the `PROG` environment variable to set the correct program name.

View File

@ -1,30 +0,0 @@
#! /bin/bash
# Heavily inspired by https://github.com/urfave/cli
_cli_bash_autocomplete() {
if [[ "${COMP_WORDS[0]}" != "source" ]]; then
local cur opts base
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == "-"* ]]; then
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
else
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
}
if [ -z "$PROG" ] && [ ! "$(command -v gitea &> /dev/null)" ] ; then
complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete gitea
elif [ -z "$PROG" ]; then
complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete ./gitea
complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete "$PWD/gitea"
else
complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete "$PROG"
unset PROG
fi

View File

@ -1,30 +0,0 @@
#compdef ${PROG:=gitea}
# Heavily inspired by https://github.com/urfave/cli
_cli_zsh_autocomplete() {
local -a opts
local cur
cur=${words[-1]}
if [[ "$cur" == "-"* ]]; then
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
else
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}")
fi
if [[ "${opts[1]}" != "" ]]; then
_describe 'values' opts
else
_files
fi
return
}
if [ -z $PROG ] ; then
compdef _cli_zsh_autocomplete gitea
else
compdef _cli_zsh_autocomplete $(basename $PROG)
fi