Skip to content

Improve CLI commands #34973

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,13 @@ generate-manpage: ## generate manpage
@gzip -9 man/man1/gitea.1 && echo man/man1/gitea.1.gz created
@#TODO A small script that formats config-cheat-sheet.en-us.md nicely for use as a config man page

.PHONY: generate-completions
generate-completions: ## generate shell completions
@[ -f gitea ] || make backend
@./gitea completion bash > contrib/autocompletion/bash_autocomplete
@./gitea completion zsh > contrib/autocompletion/zsh_autocomplete
@./gitea completion fish > contrib/autocompletion/gitea.fish

.PHONY: docker
docker:
docker build --disable-content-trust=false -t $(DOCKER_REF) .
Expand Down
1 change: 1 addition & 0 deletions cmd/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
CmdHook = &cli.Command{
Name: "hook",
Usage: "(internal) Should only be called by Git",
Hidden: true, // internal commands shouldn't be visible
Description: "Delegate commands to corresponding Git hooks",
Before: PrepareConsoleLoggerLevel(log.FATAL),
Commands: []*cli.Command{
Expand Down
1 change: 1 addition & 0 deletions cmd/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
var CmdKeys = &cli.Command{
Name: "keys",
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",
Before: PrepareConsoleLoggerLevel(log.FATAL),
Action: runKeys,
Expand Down
107 changes: 39 additions & 68 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,92 +24,46 @@ func cmdHelp() *cli.Command {
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 */)
if !c.Args().Present() {
err = cli.ShowAppHelp(c.Root())
} else {
err = cli.ShowAppHelp(c)
err = cli.ShowCommandHelp(ctx, c.Root(), c.Args().First())
}
_, _ = fmt.Fprintf(c.Root().Writer, `
if err == nil {
_, _ = fmt.Fprintf(c.Root().Writer, `
DEFAULT CONFIGURATION:
AppPath: %s
WorkPath: %s
CustomPath: %s
ConfigFile: %s

`, 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) {
command.Flags = append(append([]cli.Flag{}, appGlobalFlags()...), command.Flags...)
command.Action = prepareWorkPathAndCustomConf(command.Action)
command.HideHelp = true
if command.Name != "help" {
command.Commands = append(command.Commands, cmdHelp())
}
for i := range command.Commands {
prepareSubcommandWithGlobalFlags(command.Commands[i])
}
command.Before = prepareWorkPathAndCustomConf()
}

// 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(context.Context, *cli.Command) error {
return func(ctx context.Context, cmd *cli.Command) error {
func prepareWorkPathAndCustomConf() cli.BeforeFunc {
return func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
var args setting.ArgWorkPathAndCustomConf
// from children to parent, check the global flags
for _, curCtx := range cmd.Lineage() {
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")
}
if cmd.IsSet("work-path") {
args.WorkPath = cmd.String("work-path")
}
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)
if cmd.IsSet("custom-path") {
args.CustomPath = cmd.String("custom-path")
}
return action(ctx, cmd)
if cmd.IsSet("config") {
args.CustomConf = cmd.String("config")
}
setting.InitWorkPathAndCommonConfig(os.Getenv, args)
return ctx, nil
}
}

Expand All @@ -125,7 +79,27 @@ 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.Version = appVer.Version + appVer.Extra
app.EnableShellCompletion = true

app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "custom-path",
Aliases: []string{"C"},
TakesFile: true,
Usage: "Set custom path (defaults to '{WorkPath}/custom')",
},
&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: "work-path",
Aliases: []string{"w"},
TakesFile: true,
Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)",
},
}
// these sub-commands need to use config file
subCmdWithConfig := []*cli.Command{
cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config"
Expand Down Expand Up @@ -156,9 +130,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.
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)
for i := range subCmdWithConfig {
prepareSubcommandWithGlobalFlags(subCmdWithConfig[i])
Expand Down
1 change: 1 addition & 0 deletions cmd/serv.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ var CmdServ = &cli.Command{
Name: "serv",
Usage: "(internal) Should only be called by SSH shell",
Description: "Serv provides access auth for repositories",
Hidden: true, // Internal commands shouldn't be visible in help
Before: PrepareConsoleLoggerLevel(log.FATAL),
Action: runServ,
Flags: []cli.Flag{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@ From within the gitea root run:
source contrib/autocompletion/bash_autocomplete
```

or for zsh run:
for zsh run:

```bash
```zsh
source contrib/autocompletion/zsh_autocomplete
```

or for fish:

```fish
source contrib/autocompletion/gitea.fish
```

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.
42 changes: 23 additions & 19 deletions contrib/autocompletion/bash_autocomplete
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
#! /bin/bash
# Heavily inspired by https://github.com/urfave/cli
#!/bin/bash

_cli_bash_autocomplete() {
# This is a shell completion script auto-generated by https://github.com/urfave/cli for bash.

# Macs have bash3 for which the bash-completion package doesn't include
# _init_completion. This is a minimal version of that function.
__gitea_init_completion() {
COMPREPLY=()
_get_comp_words_by_ref "$@" cur prev words cword
}

__gitea_bash_autocomplete() {
if [[ "${COMP_WORDS[0]}" != "source" ]]; then
local cur opts base
local cur opts base words
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
if declare -F _init_completion >/dev/null 2>&1; then
_init_completion -n "=:" || return
else
__gitea_init_completion -n "=:" || return
fi
words=("${words[@]:0:$cword}")
if [[ "$cur" == "-"* ]]; then
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
requestComp="${words[*]} ${cur} --generate-shell-completion"
else
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
requestComp="${words[*]} --generate-shell-completion"
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
opts=$(eval "${requestComp}" 2>/dev/null)
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



complete -o bashdefault -o default -o nospace -F __gitea_bash_autocomplete gitea
Loading