From 56a1962bbf0ebc7f840cc40296c1fe959b8475e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 14 Nov 2023 22:46:22 +0200 Subject: [PATCH] feat: shell completion improvements --- cmd/add.go | 21 +++++++++++---- cmd/dump.go | 7 ++--- cmd/install.go | 5 ++-- cmd/run.go | 30 ++++++++++++++++++--- cmd/uninstall.go | 5 ++-- cmd/version.go | 5 ++-- internal/lefthook/run.go | 50 +++++++++++++++++++++++++++++++++++ internal/lefthook/run_test.go | 49 +++++++++++++++++++++++++++------- 8 files changed, 144 insertions(+), 28 deletions(-) diff --git a/cmd/add.go b/cmd/add.go index b86f1dbe..a5367a15 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/evilmartians/lefthook/internal/config" "github.com/evilmartians/lefthook/internal/lefthook" ) @@ -14,12 +15,22 @@ var addDoc string func newAddCmd(opts *lefthook.Options) *cobra.Command { args := lefthook.AddArgs{} + addHookCompletions := func(cmd *cobra.Command, args []string, toComplete string) (ret []string, compDir cobra.ShellCompDirective) { + compDir = cobra.ShellCompDirectiveNoFileComp + if len(args) != 0 { + return + } + ret = config.AvailableHooks[:] + return + } + addCmd := cobra.Command{ - Use: "add hook-name", - Short: "This command adds a hook directory to a repository", - Long: addDoc, - Example: "lefthook add pre-commit", - Args: cobra.MinimumNArgs(1), + Use: "add hook-name", + Short: "This command adds a hook directory to a repository", + Long: addDoc, + Example: "lefthook add pre-commit", + ValidArgsFunction: addHookCompletions, + Args: cobra.MinimumNArgs(1), RunE: func(_cmd *cobra.Command, hooks []string) error { args.Hook = hooks[0] return lefthook.Add(opts, &args) diff --git a/cmd/dump.go b/cmd/dump.go index 1dfbc02e..a8a49f25 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -9,9 +9,10 @@ import ( func newDumpCmd(opts *lefthook.Options) *cobra.Command { dumpArgs := lefthook.DumpArgs{} dumpCmd := cobra.Command{ - Use: "dump", - Short: "Prints config merged from all extensions (in YAML format by default)", - Example: "lefthook dump", + Use: "dump", + Short: "Prints config merged from all extensions (in YAML format by default)", + Example: "lefthook dump", + ValidArgsFunction: cobra.NoFileCompletions, Run: func(cmd *cobra.Command, args []string) { lefthook.Dump(opts, dumpArgs) }, diff --git a/cmd/install.go b/cmd/install.go index 8fca459b..eb365e22 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -11,8 +11,9 @@ func newInstallCmd(opts *lefthook.Options) *cobra.Command { var a, force bool installCmd := cobra.Command{ - Use: "install", - Short: "Write basic configuration file in your project repository. Or initialize existed config", + Use: "install", + Short: "Write basic configuration file in your project repository. Or initialize existed config", + ValidArgsFunction: cobra.NoFileCompletions, RunE: func(cmd *cobra.Command, _args []string) error { return lefthook.Install(opts, force) }, diff --git a/cmd/run.go b/cmd/run.go index 4f2999b3..d8f45f91 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -3,17 +3,38 @@ package cmd import ( "github.com/spf13/cobra" + "github.com/evilmartians/lefthook/internal/config" "github.com/evilmartians/lefthook/internal/lefthook" ) func newRunCmd(opts *lefthook.Options) *cobra.Command { runArgs := lefthook.RunArgs{} + runHookCompletions := func(cmd *cobra.Command, args []string, toComplete string) (ret []string, compDir cobra.ShellCompDirective) { + compDir = cobra.ShellCompDirectiveNoFileComp + if len(args) != 0 { + return + } + ret = lefthook.ConfigHookCompletions(opts) + ret = append(ret, config.AvailableHooks[:]...) + return + } + + runHookCommandCompletions := func(cmd *cobra.Command, args []string, toComplete string) (ret []string, compDir cobra.ShellCompDirective) { + compDir = cobra.ShellCompDirectiveNoFileComp + if len(args) == 0 { + return + } + ret = lefthook.ConfigHookCommandCompletions(opts, args[0]) + return + } + runCmd := cobra.Command{ - Use: "run hook-name [git args...]", - Short: "Execute group of hooks", - Example: "lefthook run pre-commit", - Args: cobra.MinimumNArgs(1), + Use: "run hook-name [git args...]", + Short: "Execute group of hooks", + Example: "lefthook run pre-commit", + ValidArgsFunction: runHookCompletions, + Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { // args[0] - hook name // args[1:] - git hook arguments, number and value depends on the hook @@ -39,6 +60,7 @@ func newRunCmd(opts *lefthook.Options) *cobra.Command { runCmd.Flags().StringSliceVar(&runArgs.Files, "files", nil, "run on specified files. takes precedence over --all-files") runCmd.Flags().StringSliceVar(&runArgs.RunOnlyCommands, "commands", nil, "run only specified commands") + _ = runCmd.RegisterFlagCompletionFunc("commands", runHookCommandCompletions) return &runCmd } diff --git a/cmd/uninstall.go b/cmd/uninstall.go index d49c5a6c..901ca575 100644 --- a/cmd/uninstall.go +++ b/cmd/uninstall.go @@ -10,8 +10,9 @@ func newUninstallCmd(opts *lefthook.Options) *cobra.Command { args := lefthook.UninstallArgs{} uninstallCmd := cobra.Command{ - Use: "uninstall", - Short: "Revert install command", + Use: "uninstall", + Short: "Revert install command", + ValidArgsFunction: cobra.NoFileCompletions, RunE: func(cmd *cobra.Command, _args []string) error { return lefthook.Uninstall(opts, &args) }, diff --git a/cmd/version.go b/cmd/version.go index dfd8b34a..858498f0 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -12,8 +12,9 @@ func newVersionCmd(_opts *lefthook.Options) *cobra.Command { var verbose bool versionCmd := cobra.Command{ - Use: "version", - Short: "Show lefthook version", + Use: "version", + Short: "Show lefthook version", + ValidArgsFunction: cobra.NoFileCompletions, Run: func(cmd *cobra.Command, args []string) { log.Println(version.Version(verbose)) }, diff --git a/internal/lefthook/run.go b/internal/lefthook/run.go index 0fec2287..c49a1871 100644 --- a/internal/lefthook/run.go +++ b/internal/lefthook/run.go @@ -230,3 +230,53 @@ func printSummary( } } } + +func ConfigHookCompletions(opts *Options) []string { + lefthook, err := initialize(opts) + if err != nil { + return nil + } + return lefthook.configHookCompletions() +} + +func (l *Lefthook) configHookCompletions() []string { + cfg, err := config.Load(l.Fs, l.repo) + if err != nil { + return nil + } + if err = cfg.Validate(); err != nil { + return nil + } + hooks := make([]string, 0, len(cfg.Hooks)) + for hook := range cfg.Hooks { + hooks = append(hooks, hook) + } + return hooks +} + +func ConfigHookCommandCompletions(opts *Options, hookName string) []string { + lefthook, err := initialize(opts) + if err != nil { + return nil + } + return lefthook.configHookCommandCompletions(hookName) +} + +func (l *Lefthook) configHookCommandCompletions(hookName string) []string { + cfg, err := config.Load(l.Fs, l.repo) + if err != nil { + return nil + } + if err = cfg.Validate(); err != nil { + return nil + } + if hook, found := cfg.Hooks[hookName]; !found { + return nil + } else { + commands := make([]string, 0, len(hook.Commands)) + for command := range hook.Commands { + commands = append(commands, command) + } + return commands + } +} diff --git a/internal/lefthook/run_test.go b/internal/lefthook/run_test.go index b984e1f5..6924b656 100644 --- a/internal/lefthook/run_test.go +++ b/internal/lefthook/run_test.go @@ -3,6 +3,7 @@ package lefthook import ( "fmt" "path/filepath" + "slices" "testing" "github.com/spf13/afero" @@ -41,11 +42,13 @@ func TestRun(t *testing.T) { gitPath := filepath.Join(root, ".git") for i, tt := range [...]struct { - name, hook, config string - gitArgs []string - envs map[string]string - existingDirs []string - error bool + name, hook, config string + gitArgs []string + envs map[string]string + existingDirs []string + hookNameCompletions []string + hookCommandCompletions []string + error bool }{ { name: "Skip case", @@ -87,7 +90,8 @@ pre-commit: parallel: true piped: true `, - error: true, + hookNameCompletions: []string{"pre-commit"}, + error: true, }, { name: "Valid hook", @@ -97,7 +101,8 @@ pre-commit: parallel: false piped: true `, - error: false, + hookNameCompletions: []string{"pre-commit"}, + error: false, }, { name: "When in git rebase-merge flow", @@ -116,7 +121,9 @@ pre-commit: existingDirs: []string{ filepath.Join(gitPath, "rebase-merge"), }, - error: false, + hookNameCompletions: []string{"pre-commit"}, + hookCommandCompletions: []string{"echo"}, + error: false, }, { name: "When in git rebase-apply flow", @@ -135,7 +142,9 @@ pre-commit: existingDirs: []string{ filepath.Join(gitPath, "rebase-apply"), }, - error: false, + hookNameCompletions: []string{"pre-commit"}, + hookCommandCompletions: []string{"echo"}, + error: false, }, { name: "When not in rebase flow", @@ -151,7 +160,9 @@ post-commit: - merge run: echo 'SHOULD RUN' `, - error: true, + hookNameCompletions: []string{"post-commit"}, + hookCommandCompletions: []string{"echo"}, + error: true, }, } { t.Run(fmt.Sprintf("%d: %s", i, tt.name), func(t *testing.T) { @@ -193,6 +204,24 @@ post-commit: t.Errorf("expected an error") } } + + hookNameCompletions := lefthook.configHookCompletions() + if tt.hookNameCompletions != nil { + if !slices.Equal(tt.hookNameCompletions, hookNameCompletions) { + t.Errorf("expected hook name completions %v, got %v", tt.hookNameCompletions, hookNameCompletions) + } + } else if len(hookNameCompletions) != 0 { + t.Errorf("expected no hook name completions, got %v", lefthook.configHookCompletions()) + } + + hookCommandCompletions := lefthook.configHookCommandCompletions(tt.hook) + if tt.hookCommandCompletions != nil { + if !slices.Equal(tt.hookCommandCompletions, hookCommandCompletions) { + t.Errorf("expected hook command completions %v, got %v", tt.hookCommandCompletions, hookCommandCompletions) + } + } else if len(hookCommandCompletions) != 0 { + t.Errorf("expected no hook command completions, got %v", hookCommandCompletions) + } }) } }