refactor(completion): name path arguments explicitly and share firstArg

The six commands that take a file or directory argument set their
completer directly instead of a Use-line parser wrapping every
command, the first-argument-only wrappers become one firstArg helper,
and only the bash completion help keeps a custom text.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-12 10:50:37 +02:00
1 parent 1f4b5f868f
commit 9ac5b79e9e
22 files changed
+75 -212

No files matched your search

+1 -1
View File
@@ -38,7 +38,7 @@ $ tea run jobs 34 --json id,conclusion --jq '.[] | select(.conclusion == "failur
}, },
} }
pageFlags(cmd, &limit, &page) pageFlags(cmd, &limit, &page)
cmd.ValidArgsFunction = a.completeRunArg() cmd.ValidArgsFunction = firstArg(a.completeRuns())
return cmd return cmd
} }
+1 -1
View File
@@ -107,7 +107,7 @@ $ tea run logs 34 --output logs.zip`,
cmd.Flags().BoolVarP(&follow, "follow", "f", false, "Wait for jobs to start and print new lines until they complete") cmd.Flags().BoolVarP(&follow, "follow", "f", false, "Wait for jobs to start and print new lines until they complete")
cmd.Flags().DurationVarP(&interval, "interval", "i", 5*time.Second, "Polling interval with --follow") cmd.Flags().DurationVarP(&interval, "interval", "i", 5*time.Second, "Polling interval with --follow")
cmd.Flags().DurationVar(&timeout, "timeout", time.Hour, "Maximum time to follow") cmd.Flags().DurationVar(&timeout, "timeout", time.Hour, "Maximum time to follow")
cmd.ValidArgsFunction = a.completeRunArg() cmd.ValidArgsFunction = firstArg(a.completeRuns())
_ = cmd.RegisterFlagCompletionFunc("job", a.completeJobs()) _ = cmd.RegisterFlagCompletionFunc("job", a.completeJobs())
return cmd return cmd
} }
+5 -16
View File
@@ -228,7 +228,7 @@ $ tea run view 34 --json jobs --jq '.jobs[] | {name, conclusion}'`,
cmd.Flags().BoolVar(&noJobs, "no-jobs", false, "Leave out the jobs") cmd.Flags().BoolVar(&noJobs, "no-jobs", false, "Leave out the jobs")
cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the run in the browser") cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the run in the browser")
cmd.Flags().BoolVar(&log, "log", false, "Print the logs of every job instead, like tea run logs") cmd.Flags().BoolVar(&log, "log", false, "Print the logs of every job instead, like tea run logs")
cmd.ValidArgsFunction = a.completeRunArg() cmd.ValidArgsFunction = firstArg(a.completeRuns())
return cmd return cmd
} }
@@ -459,17 +459,6 @@ func (a *application) runPicker() (terminal.Chooser, bool) {
return chooser, err == nil && a.policy(s).CanPrompt(a.deps.Streams) return chooser, err == nil && a.policy(s).CanPrompt(a.deps.Streams)
} }
// completeRunArg completes the run argument of a run command with recent runs.
func (a *application) completeRunArg() cobra.CompletionFunc {
runs := a.completeRuns()
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return runs(cmd, args, toComplete)
}
}
// accountNote reminds people who pass the global --user, which selects the account tea acts as, // accountNote reminds people who pass the global --user, which selects the account tea acts as,
// of the flag they may have meant, as gh's --user means something else. // of the flag they may have meant, as gh's --user means something else.
func (a *application) accountNote(cmd *cobra.Command, t target, hint string) { func (a *application) accountNote(cmd *cobra.Command, t target, hint string) {
@@ -507,7 +496,7 @@ $ tea run watch 34 --timeout 10m && echo deployed`,
} }
cmd.Flags().DurationVarP(&interval, "interval", "i", 3*time.Second, "Refresh interval") cmd.Flags().DurationVarP(&interval, "interval", "i", 3*time.Second, "Refresh interval")
cmd.Flags().DurationVar(&timeout, "timeout", time.Hour, "Maximum time to wait") cmd.Flags().DurationVar(&timeout, "timeout", time.Hour, "Maximum time to wait")
cmd.ValidArgsFunction = a.completeRunArg() cmd.ValidArgsFunction = firstArg(a.completeRuns())
return cmd return cmd
} }
@@ -622,7 +611,7 @@ $ tea run cancel 34 --force`,
}, },
} }
cmd.Flags().BoolVar(&force, "force", false, "Cancel without waiting for the runner to acknowledge") cmd.Flags().BoolVar(&force, "force", false, "Cancel without waiting for the runner to acknowledge")
cmd.ValidArgsFunction = a.completeRunArg() cmd.ValidArgsFunction = firstArg(a.completeRuns())
return cmd return cmd
} }
@@ -681,7 +670,7 @@ $ tea run rerun 34 --job build`,
} }
cmd.Flags().BoolVar(&failed, "failed", false, "Rerun only the failed jobs") cmd.Flags().BoolVar(&failed, "failed", false, "Rerun only the failed jobs")
cmd.Flags().StringVarP(&job, "job", "j", "", "Rerun only this job, by `name or ID`") cmd.Flags().StringVarP(&job, "job", "j", "", "Rerun only this job, by `name or ID`")
cmd.ValidArgsFunction = a.completeRunArg() cmd.ValidArgsFunction = firstArg(a.completeRuns())
_ = cmd.RegisterFlagCompletionFunc("job", a.completeJobs()) _ = cmd.RegisterFlagCompletionFunc("job", a.completeJobs())
return cmd return cmd
} }
@@ -736,7 +725,7 @@ $ tea run delete 34 --yes`,
return nil return nil
}, },
} }
cmd.ValidArgsFunction = a.completeRunArg() cmd.ValidArgsFunction = firstArg(a.completeRuns())
return cmd return cmd
} }
+1 -6
View File
@@ -433,10 +433,5 @@ func (a *application) completeActionValues(secret bool, scope *actionScopeFlags)
if secret { if secret {
names = a.completeSecrets(scope.scope) names = a.completeSecrets(scope.scope)
} }
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { return firstArg(names)
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return names(cmd, args, toComplete)
}
} }
+3 -14
View File
@@ -128,7 +128,7 @@ $ tea workflow view ci --web`,
cmd.Flags().BoolVar(&showYAML, "yaml", false, "Print the workflow file") cmd.Flags().BoolVar(&showYAML, "yaml", false, "Print the workflow file")
cmd.Flags().StringVarP(&ref, "ref", "r", "", "Branch, tag or commit of the workflow file that --yaml prints (default: the default branch)") cmd.Flags().StringVarP(&ref, "ref", "r", "", "Branch, tag or commit of the workflow file that --yaml prints (default: the default branch)")
cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the workflow file in the browser") cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the workflow file in the browser")
cmd.ValidArgsFunction = a.completeWorkflowArg() cmd.ValidArgsFunction = firstArg(a.completeWorkflows())
_ = cmd.RegisterFlagCompletionFunc("ref", a.completeBranches()) _ = cmd.RegisterFlagCompletionFunc("ref", a.completeBranches())
return cmd return cmd
} }
@@ -269,7 +269,7 @@ $ tea workflow run nightly --follow`,
cmd.Flags().BoolVar(&follow, "follow", false, "Follow the logs of the run's jobs after dispatching it") cmd.Flags().BoolVar(&follow, "follow", false, "Follow the logs of the run's jobs after dispatching it")
cmd.Flags().DurationVar(&interval, "interval", 2*time.Second, "Polling interval with --follow") cmd.Flags().DurationVar(&interval, "interval", 2*time.Second, "Polling interval with --follow")
cmd.Flags().DurationVar(&timeout, "timeout", 30*time.Minute, "Maximum time to follow") cmd.Flags().DurationVar(&timeout, "timeout", 30*time.Minute, "Maximum time to follow")
cmd.ValidArgsFunction = a.completeWorkflowArg() cmd.ValidArgsFunction = firstArg(a.completeWorkflows())
_ = cmd.RegisterFlagCompletionFunc("ref", a.completeBranches()) _ = cmd.RegisterFlagCompletionFunc("ref", a.completeBranches())
_ = cmd.RegisterFlagCompletionFunc("field", a.completeWorkflowInputs()) _ = cmd.RegisterFlagCompletionFunc("field", a.completeWorkflowInputs())
_ = cmd.RegisterFlagCompletionFunc("input", a.completeWorkflowInputs()) _ = cmd.RegisterFlagCompletionFunc("input", a.completeWorkflowInputs())
@@ -445,7 +445,7 @@ func (a *application) workflowStateCommand(enable bool) *cobra.Command {
$ tea workflow %s Nightly`, verb, verb), $ tea workflow %s Nightly`, verb, verb),
Args: argsExact(1), Args: argsExact(1),
Annotations: map[string]string{"tea.arguments": workflowArguments}, Annotations: map[string]string{"tea.arguments": workflowArguments},
ValidArgsFunction: a.completeWorkflowArg(), ValidArgsFunction: firstArg(a.completeWorkflows()),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
t, err := a.resolve(cmd.Context(), true) t, err := a.resolve(cmd.Context(), true)
if err != nil { if err != nil {
@@ -472,17 +472,6 @@ $ tea workflow %s Nightly`, verb, verb),
} }
} }
// completeWorkflowArg completes the workflow argument with workflow files.
func (a *application) completeWorkflowArg() cobra.CompletionFunc {
workflows := a.completeWorkflows()
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return workflows(cmd, args, toComplete)
}
}
// completeWorkflowInputs completes the inputs of the workflow given as the first argument as // completeWorkflowInputs completes the inputs of the workflow given as the first argument as
// name=, then the values of a choice or boolean input. // name=, then the values of a choice or boolean input.
func (a *application) completeWorkflowInputs() cobra.CompletionFunc { func (a *application) completeWorkflowInputs() cobra.CompletionFunc {
+6 -28
View File
@@ -124,6 +124,8 @@ func TestCompletionOffersPathsOnlyWherePathsAreExpected(t *testing.T) {
"release asset upload v1 a.txt ": cobra.ShellCompDirectiveDefault, "release asset upload v1 a.txt ": cobra.ShellCompDirectiveDefault,
"repo clone alice/project ": cobra.ShellCompDirectiveFilterDirs, "repo clone alice/project ": cobra.ShellCompDirectiveFilterDirs,
"repo clone alice/project dir ": cobra.ShellCompDirectiveNoFileComp, "repo clone alice/project dir ": cobra.ShellCompDirectiveNoFileComp,
"issue attachment upload 5 ": cobra.ShellCompDirectiveDefault,
"release create v1 ": cobra.ShellCompDirectiveDefault,
"--cwd ": cobra.ShellCompDirectiveFilterDirs, "--cwd ": cobra.ShellCompDirectiveFilterDirs,
"--jq ": cobra.ShellCompDirectiveNoFileComp, "--jq ": cobra.ShellCompDirectiveNoFileComp,
} { } {
@@ -136,9 +138,9 @@ func TestCompletionOffersPathsOnlyWherePathsAreExpected(t *testing.T) {
return return
} }
path := strings.Fields(cmd.CommandPath())[1:] path := strings.Fields(cmd.CommandPath())[1:]
slots := argSlots(cmd.Use)
_, directive := complete(t, deps, out, append(path, "")...) _, directive := complete(t, deps, out, append(path, "")...)
require.Equal(t, len(slots) > 0 && slots[0].kind == "file", directive == cobra.ShellCompDirectiveDefault, "%s <TAB>: %d", cmd.CommandPath(), directive) takesFile := cmd.CommandPath() == "tea label import" || cmd.CommandPath() == "tea ssh-key create"
require.Equal(t, takesFile, directive == cobra.ShellCompDirectiveDefault, "%s <TAB>: %d", cmd.CommandPath(), directive)
for _, flags := range []*pflag.FlagSet{cmd.LocalNonPersistentFlags(), cmd.PersistentFlags()} { for _, flags := range []*pflag.FlagSet{cmd.LocalNonPersistentFlags(), cmd.PersistentFlags()} {
flags.VisitAll(func(flag *pflag.Flag) { flags.VisitAll(func(flag *pflag.Flag) {
if flag.NoOptDefVal != "" || flag.Hidden { if flag.NoOptDefVal != "" || flag.Hidden {
@@ -152,21 +154,6 @@ func TestCompletionOffersPathsOnlyWherePathsAreExpected(t *testing.T) {
}) })
} }
func TestArgSlots(t *testing.T) {
for use, want := range map[string][]argSlot{
"list": {},
"view <number>": {{}},
"upload <tag> <file>...": {{}, {kind: "file", variadic: true}},
"clone <repository> [directory]": {{}, {kind: "directory"}},
"create <public-key-file>": {{kind: "file"}},
"review [<number> | <url> | <branch>]": {{}},
"man [command...]": {{variadic: true}},
"set <key> <value>": {{}, {}},
} {
require.Equal(t, want, append([]argSlot{}, argSlots(use)...), use)
}
}
func TestOfflineCompletions(t *testing.T) { func TestOfflineCompletions(t *testing.T) {
deps, out, _ := testDeps(t) deps, out, _ := testDeps(t)
deps.Git = &fakeGit{results: map[string]string{ deps.Git = &fakeGit{results: map[string]string{
@@ -338,15 +325,6 @@ func TestInstanceCompletions(t *testing.T) {
func TestCompletionInstallHelp(t *testing.T) { func TestCompletionInstallHelp(t *testing.T) {
deps, out, _ := testDeps(t) deps, out, _ := testDeps(t)
for shell, want := range map[string]string{ require.NoError(t, Execute(context.Background(), deps, []string{"completion", "bash", "--help"}))
"bash": "$ eval \"$(tea completion bash)\"\n", require.Contains(t, out.String(), "$ eval \"$(tea completion bash)\"\n")
"zsh": "$ autoload -U compinit && compinit\n",
"fish": "$ tea completion fish > ~/.config/fish/completions/tea.fish\n",
"powershell": "$ tea completion powershell | Out-String | Invoke-Expression\n",
} {
out.Reset()
require.NoError(t, Execute(context.Background(), deps, []string{"completion", shell, "--help"}))
require.Contains(t, out.String(), want, shell)
require.NotContains(t, out.String(), "source <(tea completion bash)\n", shell)
}
} }
+2 -2
View File
@@ -50,7 +50,7 @@ $ tea run artifact list 34`,
}, },
} }
pageFlags(list, &limit, &page) pageFlags(list, &limit, &page)
list.ValidArgsFunction = a.completeRunArg() list.ValidArgsFunction = firstArg(a.completeRuns())
group.AddCommand(list) group.AddCommand(list)
return group return group
@@ -148,7 +148,7 @@ $ tea run download 34 --zip`,
destination.register(cmd) destination.register(cmd)
cmd.Flags().StringSliceVarP(&names, "name", "n", nil, "Only download the artifacts with these names") cmd.Flags().StringSliceVarP(&names, "name", "n", nil, "Only download the artifacts with these names")
cmd.Flags().BoolVar(&asZip, "zip", false, "Save each artifact as a ZIP archive instead of extracting it") cmd.Flags().BoolVar(&asZip, "zip", false, "Save each artifact as a ZIP archive instead of extracting it")
cmd.ValidArgsFunction = a.completeRunArg() cmd.ValidArgsFunction = firstArg(a.completeRuns())
_ = cmd.RegisterFlagCompletionFunc("name", commaSeparated(a.completeArtifactNames())) _ = cmd.RegisterFlagCompletionFunc("name", commaSeparated(a.completeArtifactNames()))
return cmd return cmd
} }
+2 -8
View File
@@ -426,13 +426,7 @@ func article(noun string) string {
// completeDiscussions completes the open issues or pull requests that a command of kind takes. // completeDiscussions completes the open issues or pull requests that a command of kind takes.
func (a *application) completeDiscussions(kind refKind) cobra.CompletionFunc { func (a *application) completeDiscussions(kind refKind) cobra.CompletionFunc {
if kind == pullRef { if kind == pullRef {
complete := a.completePulls("open") return firstArg(a.completePulls("open"))
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return complete(cmd, args, toComplete)
}
} }
return a.completeIssueArgs("open", false) return firstArg(a.completeIssues("open"))
} }
+36 -89
View File
@@ -519,9 +519,9 @@ func actionScope(scope func() services.ActionScope, t target) services.ActionSco
// pathFlags take local file paths, like every flag whose name ends in -file. // pathFlags take local file paths, like every flag whose name ends in -file.
var pathFlags = []string{"asset", "attachment", "file", "input", "ssh-key"} var pathFlags = []string{"asset", "attachment", "file", "input", "ssh-key"}
// completeDefaults completes paths only where a command takes them: path flags, flags marked with // completeDefaults completes paths only where a command takes them: path flags and flags marked
// MarkFlagFilename, and positional arguments named file or directory in Use. Everything else without a // with MarkFlagFilename. Everything else without a completer completes nothing, by the root's
// completer completes nothing, by the root's default directive, rather than file names. // default directive, rather than file names.
func completeDefaults(cmd *cobra.Command) { func completeDefaults(cmd *cobra.Command) {
for _, flags := range []*pflag.FlagSet{cmd.Flags(), cmd.PersistentFlags()} { for _, flags := range []*pflag.FlagSet{cmd.Flags(), cmd.PersistentFlags()} {
flags.VisitAll(func(flag *pflag.Flag) { flags.VisitAll(func(flag *pflag.Flag) {
@@ -536,23 +536,6 @@ func completeDefaults(cmd *cobra.Command) {
} }
}) })
} }
if slots := argSlots(cmd.Use); len(slots) > 0 {
complete := cmd.ValidArgsFunction
cmd.ValidArgsFunction = func(c *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
slot := slots[min(len(args), len(slots)-1)]
switch {
case len(args) >= len(slots) && !slot.variadic:
return nil, cobra.ShellCompDirectiveNoFileComp
case slot.kind == "file":
return completeFiles(c, args, toComplete)
case slot.kind == "directory":
return completeDirectories(c, args, toComplete)
case complete != nil:
return complete(c, args, toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
}
for _, child := range cmd.Commands() { for _, child := range cmd.Commands() {
completeDefaults(child) completeDefaults(child)
} }
@@ -568,89 +551,53 @@ func completeDirectories(*cobra.Command, []string, string) ([]cobra.Completion,
return nil, cobra.ShellCompDirectiveFilterDirs return nil, cobra.ShellCompDirectiveFilterDirs
} }
// argSlot is one positional argument that a Use line declares, such as <number>, [directory] or // firstArg completes only the first argument with complete.
// <file>...; kind is "file" or "directory" for path arguments. func firstArg(complete cobra.CompletionFunc) cobra.CompletionFunc {
type argSlot struct { return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
kind string if len(args) > 0 {
variadic bool return nil, cobra.ShellCompDirectiveNoFileComp
}
// argSlots parses the positional arguments of a Use line.
func argSlots(use string) []argSlot {
_, rest, _ := strings.Cut(use, " ")
var slots []argSlot
depth, start := 0, 0
for i, r := range rest {
switch r {
case '<', '[', '{':
if depth == 0 {
start = i
}
depth++
case '>', ']', '}':
if depth--; depth != 0 {
continue
}
slot := argSlot{variadic: strings.HasPrefix(rest[i+1:], "...") || strings.HasSuffix(rest[start:i], "...")}
for name := range strings.FieldsFuncSeq(rest[start:i+1], func(r rune) bool { return strings.ContainsRune("<>[]|. ", r) }) {
switch {
case name == "file" || strings.HasSuffix(name, "-file"):
slot.kind = "file"
case name == "directory" || name == "dir":
slot.kind = "directory"
}
}
slots = append(slots, slot)
} }
return complete(cmd, args, toComplete)
} }
return slots
} }
// completionInstallHelp explains how to load the completion scripts, including with macOS's bash 3.2 // pathArgs completes the arguments before position n with complete, and paths with path from
// and in zsh without compinit. // position n on: every later argument when variadic, else only the one at n.
func pathArgs(n int, variadic bool, path, complete cobra.CompletionFunc) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
switch {
case len(args) < n:
return complete(cmd, args, toComplete)
case len(args) == n || variadic:
return path(cmd, args, toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
}
// completionInstallHelp explains how to load the bash script with macOS's bash 3.2, which cannot
// source <(tea completion bash); cobra's help covers the other shells.
func completionInstallHelp(root *cobra.Command) { func completionInstallHelp(root *cobra.Command) {
completion, _, err := root.Find([]string{"completion"}) completion, _, err := root.Find([]string{"completion"})
if err != nil || completion == root { if err != nil || completion == root {
return return
} }
completion.Short = "Generate shell completion scripts" completion.Short = "Generate shell completion scripts"
completion.Long = `Generate shell completion scripts for tea. examples := map[string]string{
"bash": `$ eval "$(tea completion bash)"`,
Completion offers commands, flags, flag values such as --state or --json fields, and values "zsh": `$ source <(tea completion zsh)`,
from the selected instance, such as repositories, issue numbers and labels. See "fish": `$ tea completion fish > ~/.config/fish/completions/tea.fish`,
tea completion <shell> --help for how to load the script in your shell.` "powershell": `$ tea completion powershell | Out-String | Invoke-Expression`,
help := map[string][2]string{
"bash": {`Generate the completion script for bash.
The script needs the bash-completion package. On macOS, install bash-completion with
Homebrew for the system bash 3.2, or bash-completion@2 for a Homebrew bash, and load it
as its install notes say.
To load completions in every new shell, add the eval line below to ~/.bashrc (on macOS,
~/.bash_profile). Use eval: bash 3.2, the default on macOS, cannot source <(tea completion bash).`, `# Load completions in this shell, or in every shell from ~/.bashrc
$ eval "$(tea completion bash)"
# Or install the script for every user (Linux, then macOS)
$ tea completion bash > /etc/bash_completion.d/tea
$ tea completion bash > "$(brew --prefix)/etc/bash_completion.d/tea"`},
"zsh": {`Generate the completion script for zsh.
Completion must be initialized before the script loads: add these lines to ~/.zshrc,
keeping an existing compinit call instead of the first line. Without compinit, zsh
reports "command not found: compdef".`, `$ autoload -U compinit && compinit
$ source <(tea completion zsh)
# Or install the script into a directory of your fpath once
$ tea completion zsh > "${fpath[1]}/_tea"`},
"fish": {`Generate the completion script for fish.
To load completions in every new shell, install the script once.`, `$ tea completion fish > ~/.config/fish/completions/tea.fish`},
"powershell": {`Generate the completion script for PowerShell.
To load completions in every new shell, add this line to your PowerShell profile.`, `$ tea completion powershell | Out-String | Invoke-Expression`},
} }
for _, shell := range completion.Commands() { for _, shell := range completion.Commands() {
shell.Long, shell.Example = help[shell.Name()][0], help[shell.Name()][1] shell.Example = examples[shell.Name()]
if flag := shell.Flags().Lookup("no-descriptions"); flag != nil { if flag := shell.Flags().Lookup("no-descriptions"); flag != nil {
flag.Usage = "Disable completion descriptions" flag.Usage = "Disable completion descriptions"
} }
} }
bash, _, _ := completion.Find([]string{"bash"})
bash.Long = `Generate the completion script for bash. It needs the bash-completion package.
To load completions in every new shell, add the eval line below to ~/.bashrc (on macOS,
~/.bash_profile); bash 3.2, the default on macOS, cannot source <(tea completion bash).`
} }
+1 -1
View File
@@ -90,7 +90,7 @@ are addressed by the %s number and the attachment ID that attachment list shows.
var uploadName string var uploadName string
upload := &cobra.Command{Use: "upload {<number> | <url>} <file>", Short: "Attach a file", Example: fmt.Sprintf(`$ tea %[1]s attachment upload %[2]s crash.log upload := &cobra.Command{Use: "upload {<number> | <url>} <file>", Short: "Attach a file", Example: fmt.Sprintf(`$ tea %[1]s attachment upload %[2]s crash.log
$ tea %[1]s attachment upload %[2]s screenshot.png --name before.png`, words.group, words.example), Annotations: annotations, Args: argsExact(2), ValidArgsFunction: completeFirst, RunE: func(cmd *cobra.Command, args []string) error { $ tea %[1]s attachment upload %[2]s screenshot.png --name before.png`, words.group, words.example), Annotations: annotations, Args: argsExact(2), ValidArgsFunction: pathArgs(1, false, completeFiles, completeFirst), RunE: func(cmd *cobra.Command, args []string) error {
name := uploadName name := uploadName
if cmd.Flags().Changed("name") && strings.TrimSpace(name) == "" { if cmd.Flags().Changed("name") && strings.TrimSpace(name) == "" {
return clierr.Wrap(clierr.Usage, errors.New("--name cannot be empty")) return clierr.Wrap(clierr.Usage, errors.New("--name cannot be empty"))
+1 -1
View File
@@ -124,7 +124,7 @@ $ tea issue edit 12 --body-file notes.md`, Annotations: map[string]string{"tea.a
a.completeMetadataFlags(cmd) a.completeMetadataFlags(cmd)
_ = cmd.RegisterFlagCompletionFunc("milestone", a.completeMilestones()) _ = cmd.RegisterFlagCompletionFunc("milestone", a.completeMilestones())
_ = cmd.RegisterFlagCompletionFunc("ref", a.completeBranches()) _ = cmd.RegisterFlagCompletionFunc("ref", a.completeBranches())
cmd.ValidArgsFunction = a.completeIssueArgs("open", true) cmd.ValidArgsFunction = a.completeIssues("open")
return cmd return cmd
} }
+2 -14
View File
@@ -187,22 +187,10 @@ $ tea issue view https://gitea.com/OWNER/REPO/issues/12 --web`, Annotations: map
}} }}
cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the issue in the browser") cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the issue in the browser")
cmd.Flags().BoolVarP(&comments, "comments", "c", false, "View issue comments") cmd.Flags().BoolVarP(&comments, "comments", "c", false, "View issue comments")
cmd.ValidArgsFunction = a.completeIssueArgs("open", false) cmd.ValidArgsFunction = firstArg(a.completeIssues("open"))
return cmd return cmd
} }
// completeIssueArgs completes issue numbers in state for the first argument, or for every
// argument when variadic.
func (a *application) completeIssueArgs(state string, variadic bool) cobra.CompletionFunc {
complete := a.completeIssues(state)
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) > 0 && !variadic {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return complete(cmd, args, toComplete)
}
}
func (a *application) issueCreateCommand() *cobra.Command { func (a *application) issueCreateCommand() *cobra.Command {
var title, body, bodyFile, deadline, templateName, ref, milestone string var title, body, bodyFile, deadline, templateName, ref, milestone string
var useEditor, web bool var useEditor, web bool
@@ -446,7 +434,7 @@ $ tea issue %[1]s 12 13 --comment "Done in #14"`, verb), Annotations: map[string
return errors.Join(append(failures, writeBatchJSON(a, cmd, len(numbers), results))...) return errors.Join(append(failures, writeBatchJSON(a, cmd, len(numbers), results))...)
}} }}
cmd.Flags().StringVarP(&comment, "comment", "c", "", "Leave a comment before the issue is "+state) cmd.Flags().StringVarP(&comment, "comment", "c", "", "Leave a comment before the issue is "+state)
cmd.ValidArgsFunction = a.completeIssueArgs(completeState, true) cmd.ValidArgsFunction = a.completeIssues(completeState)
return cmd return cmd
} }
+1 -1
View File
@@ -302,7 +302,7 @@ func (a *application) labelImportCommand(scope labelScope) *cobra.Command {
" ;description" and " ;exclusive", " ;archived" or both. tea label export writes this " ;description" and " ;exclusive", " ;archived" or both. tea label export writes this
format, and Gitea's label templates use it. Existing labels are refused unless --force format, and Gitea's label templates use it. Existing labels are refused unless --force
updates them.`, Example: `$ tea label import labels.txt updates them.`, Example: `$ tea label import labels.txt
$ tea label export --repo alice/old | tea label import - --repo alice/new`, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error { $ tea label export --repo alice/old | tea label import - --repo alice/new`, Args: argsExact(1), ValidArgsFunction: completeFiles, RunE: func(cmd *cobra.Command, args []string) error {
return a.importLabels(cmd, scope, args[0], force) return a.importLabels(cmd, scope, args[0], force)
}} }}
cmd.Flags().BoolVarP(&force, "force", "f", false, "Update labels that exist already") cmd.Flags().BoolVarP(&force, "force", "f", false, "Update labels that exist already")
+3 -15
View File
@@ -161,7 +161,7 @@ $ tea milestone view 3 --web`, Annotations: map[string]string{"tea.arguments": m
}} }}
cmd.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID") cmd.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID")
cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the milestone in the browser") cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the milestone in the browser")
cmd.ValidArgsFunction = a.completeMilestoneArgs(false) cmd.ValidArgsFunction = firstArg(a.completeMilestones())
return cmd return cmd
} }
@@ -340,7 +340,7 @@ $ tea milestone edit 3 --editor`, Annotations: map[string]string{"tea.arguments"
}} }}
fields.register(cmd) fields.register(cmd)
cmd.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID") cmd.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID")
cmd.ValidArgsFunction = a.completeMilestoneArgs(false) cmd.ValidArgsFunction = firstArg(a.completeMilestones())
return cmd return cmd
} }
@@ -367,7 +367,7 @@ $ tea milestone delete 7 --yes`, Annotations: map[string]string{"tea.confirm": "
return a.jsonResult(cmd, item) return a.jsonResult(cmd, item)
}} }}
cmd.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID") cmd.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID")
cmd.ValidArgsFunction = a.completeMilestoneArgs(false) cmd.ValidArgsFunction = firstArg(a.completeMilestones())
return cmd return cmd
} }
@@ -378,15 +378,3 @@ func findMilestone(cmd *cobra.Command, s services.Milestones, raw string, byName
} }
return s.FindOne(cmd.Context(), raw, byName) return s.FindOne(cmd.Context(), raw, byName)
} }
// completeMilestoneArgs completes milestone titles for the first argument, or for every argument
// when variadic.
func (a *application) completeMilestoneArgs(variadic bool) cobra.CompletionFunc {
complete := a.completeMilestones()
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) > 0 && !variadic {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return complete(cmd, args, toComplete)
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ $ tea milestone issue list v1.0 --state all --kind issues`, Annotations: map[str
enumFlag(list, &opts.State, "state", "s", "open", "Filter by state", "open", "closed", "all") enumFlag(list, &opts.State, "state", "s", "open", "Filter by state", "open", "closed", "all")
enumFlag(list, &opts.Kind, "kind", "", "all", "List issues, pull requests or both", "issues", "pulls", "all") enumFlag(list, &opts.Kind, "kind", "", "all", "List issues, pull requests or both", "issues", "pulls", "all")
list.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID") list.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID")
list.ValidArgsFunction = a.completeMilestoneArgs(false) list.ValidArgsFunction = firstArg(a.completeMilestones())
group.AddCommand(list) group.AddCommand(list)
for _, verb := range []string{"add", "remove"} { for _, verb := range []string{"add", "remove"} {
var byName bool var byName bool
+1 -1
View File
@@ -54,6 +54,6 @@ $ tea milestone %[1]s v1.0 v1.1`, verb), Annotations: map[string]string{"tea.arg
return errors.Join(append(failures, writeBatchJSON(a, cmd, len(selected), results))...) return errors.Join(append(failures, writeBatchJSON(a, cmd, len(selected), results))...)
}} }}
cmd.Flags().BoolVar(&byName, "by-name", false, "Match numeric arguments as milestone titles rather than IDs") cmd.Flags().BoolVar(&byName, "by-name", false, "Match numeric arguments as milestone titles rather than IDs")
cmd.ValidArgsFunction = a.completeMilestoneArgs(true) cmd.ValidArgsFunction = a.completeMilestones()
return cmd return cmd
} }
+1 -1
View File
@@ -154,7 +154,7 @@ $ tea release upload v1.0.0 dist/tea-linux --clobber`,
} }
command.Flags().StringVar(&name, "name", "", "Asset name (default: the file's name)") command.Flags().StringVar(&name, "name", "", "Asset name (default: the file's name)")
command.Flags().BoolVar(&clobber, "clobber", false, "Replace assets with the same name") command.Flags().BoolVar(&clobber, "clobber", false, "Replace assets with the same name")
command.ValidArgsFunction = a.completeReleases() command.ValidArgsFunction = pathArgs(1, true, completeFiles, a.completeReleases())
return command return command
} }
+1 -1
View File
@@ -307,7 +307,7 @@ $ tea release create v1.1.0-rc1 --target release/1.1 --draft --prerelease --titl
command.Flags().StringVar(&fields.tag, "tag", "", "Release tag") command.Flags().StringVar(&fields.tag, "tag", "", "Release tag")
_ = command.Flags().MarkHidden("tag") _ = command.Flags().MarkHidden("tag")
command.Flags().StringSliceVar(&assets, "asset", nil, "File to upload as an asset (repeatable)") command.Flags().StringSliceVar(&assets, "asset", nil, "File to upload as an asset (repeatable)")
command.ValidArgsFunction = a.completeTags() command.ValidArgsFunction = pathArgs(1, true, completeFiles, a.completeTags())
return command return command
} }
+1 -1
View File
@@ -284,7 +284,7 @@ $ tea repo clone alice/project -- --branch develop --recurse-submodules`,
command.Flags().IntVarP(&opts.depth, "depth", "d", 0, "Number of commits to fetch (0: full history)") command.Flags().IntVarP(&opts.depth, "depth", "d", 0, "Number of commits to fetch (0: full history)")
command.Flags().StringVarP(&opts.upstream, "upstream-remote-name", "u", "upstream", "Remote name for the parent of a fork") command.Flags().StringVarP(&opts.upstream, "upstream-remote-name", "u", "upstream", "Remote name for the parent of a fork")
command.Flags().BoolVar(&noUpstream, "no-upstream", false, "Do not add a remote for the parent of a fork") command.Flags().BoolVar(&noUpstream, "no-upstream", false, "Do not add a remote for the parent of a fork")
command.ValidArgsFunction = a.completeRepos() command.ValidArgsFunction = pathArgs(1, false, completeDirectories, a.completeRepos())
return command return command
} }
+1 -1
View File
@@ -135,7 +135,7 @@ func sshKeySource(source string) string {
func (a *application) sshKeyCreateCommand() *cobra.Command { func (a *application) sshKeyCreateCommand() *cobra.Command {
var title string var title string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "create [<file>]", Aliases: []string{"add"}, Short: "Add an SSH public key from a file or stdin", Args: usageArgs(cobra.MaximumNArgs(1)), Use: "create [<file>]", Aliases: []string{"add"}, Short: "Add an SSH public key from a file or stdin", Args: usageArgs(cobra.MaximumNArgs(1)), ValidArgsFunction: completeFiles,
Long: `Add an SSH public key to your account. Without a file, or with -, the key is read from stdin. Long: `Add an SSH public key to your account. Without a file, or with -, the key is read from stdin.
The title defaults to the key's comment, such as alice@laptop, or else to this computer's name.`, The title defaults to the key's comment, such as alice@laptop, or else to this computer's name.`,
+1 -6
View File
@@ -339,12 +339,7 @@ to set single units instead, or to override some of them.`,
$ tea team create acme/reviewers --unit repo.code=read,repo.pulls=write $ tea team create acme/reviewers --unit repo.code=read,repo.pulls=write
# give the team access to every repository of the organization # give the team access to every repository of the organization
$ tea team create acme ops --permission admin --all-repositories`, $ tea team create acme ops --permission admin --all-repositories`,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { ValidArgsFunction: firstArg(a.completeOrgs()),
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return a.completeOrgs()(cmd, args, toComplete)
},
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
org, name, scoped := strings.Cut(args[0], "/") org, name, scoped := strings.Cut(args[0], "/")
names := 0 names := 0
+3 -3
View File
@@ -161,7 +161,7 @@ func (a *application) timeCreateCommand() *cobra.Command {
return &cobra.Command{Use: "create {<number> | <url>} <duration>", Aliases: []string{"add", "log"}, Short: "Log time spent on an issue", Long: `Log time spent on an issue or pull request. The duration uses h, m and s units, such as return &cobra.Command{Use: "create {<number> | <url>} <duration>", Aliases: []string{"add", "log"}, Short: "Log time spent on an issue", Long: `Log time spent on an issue or pull request. The duration uses h, m and s units, such as
1h25m or "1h 25m".`, Example: `$ tea time create 12 1h25m 1h25m or "1h 25m".`, Example: `$ tea time create 12 1h25m
$ tea time add 12 "1h 30m" $ tea time add 12 "1h 30m"
$ tea time log https://gitea.com/OWNER/REPO/issues/12 45m`, Annotations: map[string]string{"tea.arguments": issueArguments}, Args: argsExact(2), ValidArgsFunction: a.completeIssueArgs("open", false), RunE: func(cmd *cobra.Command, args []string) error { $ tea time log https://gitea.com/OWNER/REPO/issues/12 45m`, Annotations: map[string]string{"tea.arguments": issueArguments}, Args: argsExact(2), ValidArgsFunction: firstArg(a.completeIssues("open")), RunE: func(cmd *cobra.Command, args []string) error {
seconds, err := parseSpent(args[1]) seconds, err := parseSpent(args[1])
if err != nil { if err != nil {
return err return err
@@ -181,7 +181,7 @@ $ tea time log https://gitea.com/OWNER/REPO/issues/12 45m`, Annotations: map[str
func (a *application) timeDeleteCommand() *cobra.Command { func (a *application) timeDeleteCommand() *cobra.Command {
return &cobra.Command{Use: "delete {<number> | <url>} <time-id>", Short: "Delete a time record", Example: `# Find the record with tea time list --issue 12 return &cobra.Command{Use: "delete {<number> | <url>} <time-id>", Short: "Delete a time record", Example: `# Find the record with tea time list --issue 12
$ tea time delete 12 8`, Annotations: map[string]string{"tea.text": "time delete does not return a resource", "tea.confirm": "true", "tea.arguments": issueArguments}, Args: argsExact(2), ValidArgsFunction: a.completeIssueArgs("all", false), RunE: func(cmd *cobra.Command, args []string) error { $ tea time delete 12 8`, Annotations: map[string]string{"tea.text": "time delete does not return a resource", "tea.confirm": "true", "tea.arguments": issueArguments}, Args: argsExact(2), ValidArgsFunction: firstArg(a.completeIssues("all")), RunE: func(cmd *cobra.Command, args []string) error {
id, err := number(args[1]) id, err := number(args[1])
if err != nil { if err != nil {
return err return err
@@ -201,7 +201,7 @@ $ tea time delete 12 8`, Annotations: map[string]string{"tea.text": "time delete
} }
func (a *application) timeResetCommand() *cobra.Command { func (a *application) timeResetCommand() *cobra.Command {
return &cobra.Command{Use: "reset {<number> | <url>}", Short: "Delete all time logged on an issue", Example: "$ tea time reset 12 --yes", Annotations: map[string]string{"tea.text": "time reset does not return a resource", "tea.confirm": "true", "tea.arguments": issueArguments}, Args: argsExact(1), ValidArgsFunction: a.completeIssueArgs("all", false), RunE: func(cmd *cobra.Command, args []string) error { return &cobra.Command{Use: "reset {<number> | <url>}", Short: "Delete all time logged on an issue", Example: "$ tea time reset 12 --yes", Annotations: map[string]string{"tea.text": "time reset does not return a resource", "tea.confirm": "true", "tea.arguments": issueArguments}, Args: argsExact(1), ValidArgsFunction: firstArg(a.completeIssues("all")), RunE: func(cmd *cobra.Command, args []string) error {
target, n, err := a.resolveRef(cmd, args[0], issueRef) target, n, err := a.resolveRef(cmd, args[0], issueRef)
if err != nil { if err != nil {
return err return err