mirror of https://github.com/go-gitea/gitea
Refactor to use urfave/cli/v2 (#25959)
Replace #10912 And there are many new tests to cover the CLI behavior There were some concerns about the "option order in hook scripts" (https://github.com/go-gitea/gitea/pull/10912#issuecomment-1137543314), it's not a problem now. Because the hook script uses `/gitea hook --config=/app.ini pre-receive` format. The "config" is a global option, it can appear anywhere. ---- ## ⚠️ BREAKING ⚠️ This PR does it best to avoid breaking anything. The major changes are: * `gitea` itself won't accept web's options: `--install-port` / `--pid` / `--port` / `--quiet` / `--verbose` .... They are `web` sub-command's options. * Use `./gitea web --pid ....` instead * `./gitea` can still run the `web` sub-command as shorthand, with default options * The sub-command's options must follow the sub-command * Before: `./gitea --sub-opt subcmd` might equal to `./gitea subcmd --sub-opt` (well, might not ...) * After: only `./gitea subcmd --sub-opt` could be used * The global options like `--config` are not affectedpull/26036/head^2
parent
840830b655
commit
d0dbe52e76
File diff suppressed because one or more lines are too long
@ -0,0 +1,196 @@ |
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"reflect" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/modules/log" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
|
||||
"github.com/urfave/cli/v2" |
||||
) |
||||
|
||||
// cmdHelp is our own help subcommand with more information
|
||||
func cmdHelp() *cli.Command { |
||||
c := &cli.Command{ |
||||
Name: "help", |
||||
Aliases: []string{"h"}, |
||||
Usage: "Shows a list of commands or help for one command", |
||||
ArgsUsage: "[command]", |
||||
Action: func(c *cli.Context) (err error) { |
||||
args := c.Args() |
||||
if args.Present() { |
||||
err = cli.ShowCommandHelp(c, args.First()) |
||||
} else { |
||||
err = cli.ShowAppHelp(c) |
||||
} |
||||
_, _ = fmt.Fprintf(c.App.Writer, ` |
||||
DEFAULT CONFIGURATION: |
||||
AppPath: %s |
||||
WorkPath: %s |
||||
CustomPath: %s |
||||
ConfigFile: %s |
||||
|
||||
`, setting.AppPath, setting.AppWorkPath, setting.CustomPath, setting.CustomConf) |
||||
return err |
||||
}, |
||||
} |
||||
return c |
||||
} |
||||
|
||||
var helpFlag = cli.HelpFlag |
||||
|
||||
func init() { |
||||
// cli.HelpFlag = nil TODO: after https://github.com/urfave/cli/issues/1794 we can use this
|
||||
} |
||||
|
||||
func appGlobalFlags() []cli.Flag { |
||||
return []cli.Flag{ |
||||
// make the builtin flags at the top
|
||||
helpFlag, |
||||
cli.VersionFlag, |
||||
|
||||
// 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 prepareSubcommandWithConfig(command *cli.Command, globalFlags []cli.Flag) { |
||||
command.Flags = append(append([]cli.Flag{}, globalFlags...), command.Flags...) |
||||
command.Action = prepareWorkPathAndCustomConf(command.Action) |
||||
command.HideHelp = true |
||||
if command.Name != "help" { |
||||
command.Subcommands = append(command.Subcommands, cmdHelp()) |
||||
} |
||||
for i := range command.Subcommands { |
||||
prepareSubcommandWithConfig(command.Subcommands[i], globalFlags) |
||||
} |
||||
} |
||||
|
||||
// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config
|
||||
// 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
|
||||
func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context) error { |
||||
return func(ctx *cli.Context) error { |
||||
var args setting.ArgWorkPathAndCustomConf |
||||
ctxLineage := ctx.Lineage() |
||||
for i := len(ctxLineage) - 1; i >= 0; i-- { |
||||
curCtx := ctxLineage[i] |
||||
if curCtx.IsSet("work-path") && args.WorkPath == "" { |
||||
args.WorkPath = curCtx.String("work-path") |
||||
} |
||||
if curCtx.IsSet("custom-path") && args.CustomPath == "" { |
||||
args.CustomPath = curCtx.String("custom-path") |
||||
} |
||||
if curCtx.IsSet("config") && args.CustomConf == "" { |
||||
args.CustomConf = curCtx.String("config") |
||||
} |
||||
} |
||||
setting.InitWorkPathAndCommonConfig(os.Getenv, args) |
||||
if ctx.Bool("help") || action == nil { |
||||
// the default behavior of "urfave/cli": "nil action" means "show help"
|
||||
return cmdHelp().Action(ctx) |
||||
} |
||||
return action(ctx) |
||||
} |
||||
} |
||||
|
||||
func reflectGet(v any, fieldName string) any { |
||||
e := reflect.ValueOf(v).Elem() |
||||
return e.FieldByName(fieldName).Interface() |
||||
} |
||||
|
||||
// https://cli.urfave.org/migrate-v1-to-v2/#flag-aliases-are-done-differently
|
||||
// Sadly v2 doesn't warn you if a comma is in the name. (https://github.com/urfave/cli/issues/1103)
|
||||
func checkCommandFlags(c any) bool { |
||||
var cmds []*cli.Command |
||||
if app, ok := c.(*cli.App); ok { |
||||
cmds = app.Commands |
||||
} else { |
||||
cmds = c.(*cli.Command).Subcommands |
||||
} |
||||
ok := true |
||||
for _, cmd := range cmds { |
||||
for _, flag := range cmd.Flags { |
||||
flagName := reflectGet(flag, "Name").(string) |
||||
if strings.Contains(flagName, ",") { |
||||
ok = false |
||||
log.Error("cli.Flag can't have comma in its Name: %q, use Aliases instead", flagName) |
||||
} |
||||
} |
||||
if !checkCommandFlags(cmd) { |
||||
ok = false |
||||
} |
||||
} |
||||
return ok |
||||
} |
||||
|
||||
func NewMainApp() *cli.App { |
||||
app := cli.NewApp() |
||||
app.EnableBashCompletion = true |
||||
|
||||
// these sub-commands need to use config file
|
||||
subCmdWithConfig := []*cli.Command{ |
||||
CmdWeb, |
||||
CmdServ, |
||||
CmdHook, |
||||
CmdDump, |
||||
CmdAdmin, |
||||
CmdMigrate, |
||||
CmdKeys, |
||||
CmdConvert, |
||||
CmdDoctor, |
||||
CmdManager, |
||||
CmdEmbedded, |
||||
CmdMigrateStorage, |
||||
CmdDumpRepository, |
||||
CmdRestoreRepository, |
||||
CmdActions, |
||||
cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config"
|
||||
} |
||||
|
||||
// these sub-commands do not need the config file, and they do not depend on any path or environment variable.
|
||||
subCmdStandalone := []*cli.Command{ |
||||
CmdCert, |
||||
CmdGenerate, |
||||
CmdDocs, |
||||
} |
||||
|
||||
app.DefaultCommand = CmdWeb.Name |
||||
|
||||
globalFlags := appGlobalFlags() |
||||
app.Flags = append(app.Flags, globalFlags...) |
||||
app.HideHelp = true // use our own help action to show helps (with more information like default config)
|
||||
app.Before = PrepareConsoleLoggerLevel(log.INFO) |
||||
for i := range subCmdWithConfig { |
||||
prepareSubcommandWithConfig(subCmdWithConfig[i], globalFlags) |
||||
} |
||||
app.Commands = append(app.Commands, subCmdWithConfig...) |
||||
app.Commands = append(app.Commands, subCmdStandalone...) |
||||
|
||||
if !checkCommandFlags(app) { |
||||
panic("some flags are incorrect") // this is a runtime check to help developers
|
||||
} |
||||
return app |
||||
} |
Loading…
Reference in new issue