From f664d240e74881b350a0e7ec5255af572a7d5e63 Mon Sep 17 00:00:00 2001 From: Raphael Gruber Date: Fri, 27 Mar 2026 21:36:08 +0100 Subject: [PATCH 1/4] feat: improve hook output with colored messages and exit codes Color "Running..." message with PrimaryStyle and show description instead of hook name when available. Remove redundant success line. Include exit code in failure error messages. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/hooks/doc.go | 3 +-- internal/hooks/hooks.go | 23 +++++++++++++++-------- internal/hooks/hooks_test.go | 20 ++++++++------------ 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/internal/hooks/doc.go b/internal/hooks/doc.go index 9ab48fd..f75d15f 100644 --- a/internal/hooks/doc.go +++ b/internal/hooks/doc.go @@ -33,8 +33,7 @@ // - {worktree-dir}: Absolute worktree path // - {repo-dir}: Absolute main repo path // - {branch}: Branch name -// - {repo}: Folder name of git repo -// - {origin}: Repository name from git origin (falls back to {repo}) +// - {repo}: Registered repo name (as shown in wt repo list) // - {trigger}: Command that triggered the hook (checkout, prune, merge, run) // - {action}: Checkout subtype: create, open, pr, or manual (for wt hook) // - {phase}: Hook timing: before or after diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go index 08ce032..337a47e 100644 --- a/internal/hooks/hooks.go +++ b/internal/hooks/hooks.go @@ -2,6 +2,7 @@ package hooks import ( "context" + "errors" "fmt" "io" "os" @@ -13,6 +14,7 @@ import ( "github.com/mattn/go-isatty" "github.com/raphi011/wt/internal/config" "github.com/raphi011/wt/internal/log" + "github.com/raphi011/wt/internal/ui/styles" ) // CommandType identifies which command is triggering the hook @@ -45,7 +47,6 @@ type Context struct { RepoDir string // absolute main repo path Branch string // branch name Repo string // registered repo name (as shown in wt repo list) - Origin string // folder name of the git repo (filepath.Base of repo path) Trigger string // command that triggered the hook (checkout, prune, merge) Action string // checkout subtype: create, open, pr, manual (for wt hook) Phase string // "before" or "after" @@ -179,7 +180,11 @@ func runHook(goCtx context.Context, name string, hook *config.Hook, ctx Context, return nil } - l.Printf("Running hook '%s'...\n", name) + desc := hook.Description + if desc == "" { + desc = name + } + l.Printf("%s\n", styles.PrimaryStyle.Render(fmt.Sprintf("Running %s...", desc))) shell, args := shellCommand(cmd) shellCmd := exec.Command(shell, args...) @@ -189,12 +194,15 @@ func runHook(goCtx context.Context, name string, hook *config.Hook, ctx Context, shellCmd.Stderr = os.Stderr if err := shellCmd.Run(); err != nil { - return fmt.Errorf("command failed: %s", cmd) + exitCode := 1 + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + exitCode = exitErr.ExitCode() + } + return fmt.Errorf("command failed (exit %d): %s", exitCode, cmd) } - if hook.Description != "" { - l.Printf(" ✓ %s\n", hook.Description) - } + l.Debug("hook completed", "name", name) return nil } @@ -284,7 +292,7 @@ var envPlaceholderRegex = regexp.MustCompile(`\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([-+ // SubstitutePlaceholders replaces {placeholder} with values from Context. // -// Static placeholders: {worktree-dir}, {repo-dir}, {branch}, {repo}, {origin}, {trigger} +// Static placeholders: {worktree-dir}, {repo-dir}, {branch}, {repo}, {trigger} // Env placeholders (from Context.Env via --arg key=value or --arg key): // - {key} - value from --arg key=value // - {key:-default} - value with default if key not set @@ -296,7 +304,6 @@ func SubstitutePlaceholders(command string, ctx Context) string { "{repo-dir}": ctx.RepoDir, "{branch}": ctx.Branch, "{repo}": ctx.Repo, - "{origin}": ctx.Origin, "{trigger}": ctx.Trigger, "{action}": ctx.Action, "{phase}": ctx.Phase, diff --git a/internal/hooks/hooks_test.go b/internal/hooks/hooks_test.go index 2baa2c8..cca999f 100644 --- a/internal/hooks/hooks_test.go +++ b/internal/hooks/hooks_test.go @@ -16,7 +16,6 @@ func TestSubstitutePlaceholders(t *testing.T) { RepoDir: "/home/user/repo", Branch: "feature-branch", Repo: "repo", - Origin: "myrepo", Trigger: "checkout", Action: "pr", Phase: "after", @@ -39,8 +38,8 @@ func TestSubstitutePlaceholders(t *testing.T) { }, { name: "all placeholders", - command: "{worktree-dir} {branch} {repo} {origin} {repo-dir} {trigger} {action} {phase}", - expected: "/home/user/worktrees/repo-branch feature-branch repo myrepo /home/user/repo checkout pr after", + command: "{worktree-dir} {branch} {repo} {repo-dir} {trigger} {action} {phase}", + expected: "/home/user/worktrees/repo-branch feature-branch repo /home/user/repo checkout pr after", }, { name: "no placeholders", @@ -906,11 +905,8 @@ func TestRunSingle_Success(t *testing.T) { } out := buf.String() - if !strings.Contains(out, "Running hook 'test-hook'") { - t.Errorf("output = %q, want to contain running message", out) - } - if !strings.Contains(out, "Say hello") { - t.Errorf("output = %q, want to contain description", out) + if !strings.Contains(out, "Running Say hello...") { + t.Errorf("output = %q, want to contain running message with description", out) } } @@ -936,7 +932,7 @@ func TestRunSingle_DryRun(t *testing.T) { if !strings.Contains(out, "[dry-run]") { t.Errorf("output = %q, want to contain [dry-run]", out) } - if strings.Contains(out, "Running hook") { + if strings.Contains(out, "Running") { t.Errorf("output = %q, should not contain running message in dry-run", out) } } @@ -957,8 +953,8 @@ func TestRunSingle_Failure(t *testing.T) { if err == nil { t.Error("RunSingle(failing command) = nil, want error") } - if !strings.Contains(err.Error(), "command failed") { - t.Errorf("error = %q, want to contain 'command failed'", err.Error()) + if !strings.Contains(err.Error(), "command failed (exit 1)") { + t.Errorf("error = %q, want to contain 'command failed (exit 1)'", err.Error()) } } @@ -995,7 +991,7 @@ func TestRunAllNonFatal_WithFailures(t *testing.T) { if !strings.Contains(out, "Warning: hook \"failing\" failed") { t.Errorf("output = %q, want warning for failing hook", out) } - if !strings.Contains(out, "Running hook 'passing'") { + if !strings.Contains(out, "Running OK...") { t.Errorf("output = %q, want running message for passing hook", out) } } From e860d9c680f9a21e62c54c844ec4ecf7abf4d05c Mon Sep 17 00:00:00 2001 From: Raphael Gruber Date: Fri, 27 Mar 2026 21:36:18 +0100 Subject: [PATCH 2/4] refactor: remove redundant {origin} hook placeholder {origin} was identical to {repo} in practice. Remove the field from hooks.Context, all callers, config template, and docs. Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/wt/checkout_cmd.go | 1 - cmd/wt/hook_cmd.go | 2 -- cmd/wt/pr_cmd.go | 2 -- cmd/wt/prune_cmd.go | 2 -- internal/config/config.go | 5 +---- internal/config/config_test.go | 6 +++--- 6 files changed, 4 insertions(+), 14 deletions(-) diff --git a/cmd/wt/checkout_cmd.go b/cmd/wt/checkout_cmd.go index 17e4b41..6e11b3d 100644 --- a/cmd/wt/checkout_cmd.go +++ b/cmd/wt/checkout_cmd.go @@ -421,7 +421,6 @@ func checkoutInRepo(ctx context.Context, repo registry.Repo, branch string, newB RepoDir: repo.Path, Branch: branch, Repo: repo.Name, - Origin: git.GetRepoDisplayName(repo.Path), Trigger: string(hooks.CommandCheckout), Action: action, Env: hookEnv, diff --git a/cmd/wt/hook_cmd.go b/cmd/wt/hook_cmd.go index b2d285e..7523e9c 100644 --- a/cmd/wt/hook_cmd.go +++ b/cmd/wt/hook_cmd.go @@ -128,7 +128,6 @@ func runHookInRepo(ctx context.Context, repo registry.Repo, hookName string, env RepoDir: repo.Path, Branch: branch, Repo: repo.Name, - Origin: repo.Name, Trigger: string(hooks.CommandRun), Action: hooks.ActionManual, Phase: hooks.PhaseAfter, @@ -164,7 +163,6 @@ func runHookInTargets(ctx context.Context, reg *registry.Registry, hookName stri RepoDir: wt.RepoPath, Branch: wt.Branch, Repo: wt.RepoName, - Origin: wt.RepoName, Trigger: string(hooks.CommandRun), Action: hooks.ActionManual, Phase: hooks.PhaseAfter, diff --git a/cmd/wt/pr_cmd.go b/cmd/wt/pr_cmd.go index d87c155..0c1ee8f 100644 --- a/cmd/wt/pr_cmd.go +++ b/cmd/wt/pr_cmd.go @@ -334,7 +334,6 @@ Use --clone-mode to control whether the repo is cloned as bare or regular.`, RepoDir: repoPath, Branch: branch, Repo: repo.Name, - Origin: repo.Name, Trigger: string(hooks.CommandCheckout), Action: hooks.ActionPR, Env: hookEnv, @@ -542,7 +541,6 @@ Merges the PR, removes the worktree (if applicable), and deletes the local branc RepoDir: res.repo.Path, Branch: res.branch, Repo: res.repo.Name, - Origin: res.repo.Name, Trigger: string(hooks.CommandMerge), Env: hookEnv, } diff --git a/cmd/wt/prune_cmd.go b/cmd/wt/prune_cmd.go index 00d1c54..e83d05b 100644 --- a/cmd/wt/prune_cmd.go +++ b/cmd/wt/prune_cmd.go @@ -427,7 +427,6 @@ func pruneWorktrees(ctx context.Context, toRemove []git.Worktree, opts pruneOpts RepoDir: wt.RepoPath, Branch: wt.Branch, Repo: filepath.Base(wt.RepoPath), - Origin: wt.RepoName, Trigger: string(hooks.CommandPrune), Phase: hooks.PhaseBefore, Env: hookEnv, @@ -485,7 +484,6 @@ func pruneWorktrees(ctx context.Context, toRemove []git.Worktree, opts pruneOpts RepoDir: wt.RepoPath, Branch: wt.Branch, Repo: filepath.Base(wt.RepoPath), - Origin: wt.RepoName, Trigger: string(hooks.CommandPrune), Phase: hooks.PhaseAfter, Env: hookEnv, diff --git a/internal/config/config.go b/internal/config/config.go index 0b33881..b1ceb54 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -462,10 +462,8 @@ const defaultConfig = `# wt configuration [checkout] # Worktree folder naming format # Available placeholders: -# {repo} - folder name of git repo +# {repo} - registered repo name (as shown in wt repo list) # {branch} - the branch name as provided -# {origin} - repo name from git origin URL (falls back to {repo}) -# Example: "{origin}_{branch}" creates "origin-name_feature-branch" worktree_format = "{repo}-{branch}" # Base ref mode for new branches (wt checkout -b) @@ -524,7 +522,6 @@ worktree_format = "{repo}-{branch}" # {repo-dir} - absolute main repo path # {branch} - branch name # {repo} - registered repo name -# {origin} - folder name of git repo # {trigger} - command trigger (checkout, prune, merge, run) # {action} - checkout subtype (create, open, pr, manual) # {phase} - hook timing (before, after) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 48b3350..43224cc 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -883,7 +883,7 @@ default_labels = ["team-a", "team-b"] mode = "regular" [checkout] -worktree_format = "{origin}-{branch}" +worktree_format = "{repo}-{branch}" base_ref = "local" auto_fetch = true set_upstream = true @@ -1005,8 +1005,8 @@ on = ["checkout"] if len(cfg.DefaultLabels) != 2 { t.Errorf("len(DefaultLabels) = %d, want 2", len(cfg.DefaultLabels)) } - if cfg.Checkout.WorktreeFormat != "{origin}-{branch}" { - t.Errorf("WorktreeFormat = %q, want %q", cfg.Checkout.WorktreeFormat, "{origin}-{branch}") + if cfg.Checkout.WorktreeFormat != "{repo}-{branch}" { + t.Errorf("WorktreeFormat = %q, want %q", cfg.Checkout.WorktreeFormat, "{repo}-{branch}") } if cfg.Checkout.BaseRef != "local" { t.Errorf("BaseRef = %q, want %q", cfg.Checkout.BaseRef, "local") From 7e6894ad04d099e3a955fc02bde8f8c162935ec8 Mon Sep 17 00:00:00 2001 From: Raphael Gruber Date: Fri, 27 Mar 2026 21:36:28 +0100 Subject: [PATCH 3/4] docs: reorganize README shell integration and clean up hook docs Move shell wrapper and completions into a dedicated Shell Integration section. Link to it from Getting Started step 1. Remove {origin} from placeholder docs. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 69 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 84f1c80..1cd24fd 100644 --- a/README.md +++ b/README.md @@ -46,22 +46,9 @@ Requires `git` in PATH. For GitHub repos: `gh` CLI. For GitLab repos: `glab` CLI ## Getting Started -### 1. Shell Completions +### 1. Shell Integration -Completions are installed automatically when using Homebrew. For manual installs: - -```bash -# Fish -wt completion fish > ~/.config/fish/completions/wt.fish - -# Bash -wt completion bash > ~/.local/share/bash-completion/completions/wt - -# Zsh — ensure ~/.zfunc exists and is on fpath, then generate -mkdir -p ~/.zfunc -echo 'fpath=(~/.zfunc $fpath)' >> ~/.zshrc # add once, before compinit -wt completion zsh > ~/.zfunc/_wt -``` +Set up shell completions and the shell wrapper so that `wt cd` can change your directory. See [Shell Integration](#shell-integration) for instructions. ### 2. Create Config @@ -459,7 +446,6 @@ Hooks without `on` only run when invoked explicitly via `wt hook ` or `--h | `{repo-dir}` | Absolute path to the main repo (bare root or `.git` parent) | | `{branch}` | Branch name | | `{repo}` | Repo name (as registered in `wt repo list`) | -| `{origin}` | Folder name of the git repo (from path) | | `{trigger}` | Command that triggered the hook (`checkout`, `prune`, `merge`, `run`) | | `{action}` | Checkout subtype: `create`, `open`, `pr`, or `manual` (for `wt hook`) | | `{phase}` | Hook timing: `before` or `after` | @@ -644,7 +630,7 @@ command = "code '{worktree-dir}'" > **Note:** Single quotes protect against spaces and most special characters, but not against values containing literal single quotes. This is a limitation of raw text substitution. -The same applies to all placeholders (`{repo-dir}`, `{branch}`, `{repo}`, `{origin}`, `{trigger}`) and custom `--arg` variables: +The same applies to all placeholders (`{repo-dir}`, `{branch}`, `{repo}`, `{trigger}`) and custom `--arg` variables: ```toml [hooks.claude] @@ -709,6 +695,40 @@ Multiple keys can read from the same stdin (all keys receive identical content): cat spec.md | wt hook claude --arg prompt=- --arg context=- ``` +## Shell Integration + +### Shell Wrapper + +`wt cd` prints the worktree path to stdout but can't change your shell's directory on its own. `wt init` outputs a shell wrapper that intercepts `wt cd` and performs the actual `cd`. + +```bash +# Fish - add to ~/.config/fish/config.fish +wt init fish | source + +# Bash - add to ~/.bashrc +eval "$(wt init bash)" + +# Zsh - add to ~/.zshrc +eval "$(wt init zsh)" +``` + +### Shell Completions + +Completions are installed automatically when using Homebrew. For manual installs: + +```bash +# Fish +wt completion fish > ~/.config/fish/completions/wt.fish + +# Bash +wt completion bash > ~/.local/share/bash-completion/completions/wt + +# Zsh — ensure ~/.zfunc exists and is on fpath, then generate +mkdir -p ~/.zfunc +echo 'fpath=(~/.zfunc $fpath)' >> ~/.zshrc # add once, before compinit +wt completion zsh > ~/.zfunc/_wt +``` + ## Integration with gh-dash `wt` works great with [gh-dash](https://github.com/dlvhdr/gh-dash). Add a keybinding to checkout PRs as worktrees: @@ -723,21 +743,6 @@ keybindings: Press `O` to checkout PR → hooks auto-open your editor. -## Shell Integration - -`wt cd` prints the worktree path to stdout but can't change your shell's directory on its own. `wt init` outputs a shell wrapper that intercepts `wt cd` and performs the actual `cd`. - -```bash -# Bash - add to ~/.bashrc -eval "$(wt init bash)" - -# Zsh - add to ~/.zshrc -eval "$(wt init zsh)" - -# Fish - add to ~/.config/fish/config.fish -wt init fish | source -``` - ## Development ```bash From 73fe8efae46ac62c83da6cdded5748b2a9697f08 Mon Sep 17 00:00:00 2001 From: Raphael Gruber Date: Fri, 27 Mar 2026 21:48:35 +0100 Subject: [PATCH 4/4] fix: prevent double hook execution, improve prune error handling - Explicit --hook names now only run in the after phase, preventing double execution (once in before-hooks and again in after-hooks) - Prune before-hook errors now include the actual error message - Prune skips worktree when hook selection fails (prevents unguarded deletion on --hook typos) - Add t.Parallel() to TestValidateHookTriggers subtests - Fix stale {origin} reference in CLAUDE.md - Clarify doc.go: before-hook failure skips individual worktree in prune batches, not the entire operation Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 2 +- cmd/wt/prune_cmd.go | 3 ++- internal/config/config_test.go | 3 +++ internal/hooks/doc.go | 4 +++- internal/hooks/hooks.go | 9 +++++++-- internal/hooks/hooks_test.go | 28 ++++++++++++++++++++++++++++ 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e5421bb..1ce0781 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,7 +82,7 @@ internal/ui/ - Terminal UI components **Worktree naming convention** - Worktrees are created as `-` (e.g., `wt-feature-branch`). The repo name is extracted from git origin URL. -**Path handling** - Worktree placement is controlled by `checkout.worktree_format` (template with `{repo}`, `{branch}`, `{origin}` placeholders). Repos are explicitly registered via `wt repo add` and tracked in the registry (`~/.wt/repos.json`). +**Path handling** - Worktree placement is controlled by `checkout.worktree_format` (template with `{repo}`, `{branch}` placeholders). Repos are explicitly registered via `wt repo add` and tracked in the registry (`~/.wt/repos.json`). **MR/PR status** - Uses `gh pr list` or `glab mr list` to fetch merge request info (auto-detected). States: merged, open, closed. diff --git a/cmd/wt/prune_cmd.go b/cmd/wt/prune_cmd.go index e83d05b..98bad50 100644 --- a/cmd/wt/prune_cmd.go +++ b/cmd/wt/prune_cmd.go @@ -420,6 +420,7 @@ func pruneWorktrees(ctx context.Context, toRemove []git.Worktree, opts pruneOpts beforeMatches, err := hooks.SelectHooks(effCfg.Hooks, opts.HookNames, opts.NoHook, hooks.CommandPrune, "", hooks.PhaseBefore) if err != nil { l.Printf("Warning: failed to select before hooks for %s: %v\n", wt.RepoName, err) + continue } if len(beforeMatches) > 0 { beforeHookCtx := hooks.Context{ @@ -432,7 +433,7 @@ func pruneWorktrees(ctx context.Context, toRemove []git.Worktree, opts pruneOpts Env: hookEnv, } if err := hooks.RunBeforeHooks(ctx, beforeMatches, beforeHookCtx, wt.Path); err != nil { - l.Printf("Skipping %s: before-hook aborted\n", wt.Branch) + l.Printf("Skipping %s: before-hook aborted: %v\n", wt.Branch, err) continue } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 43224cc..366bb5d 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1047,6 +1047,8 @@ on = ["checkout"] } func TestValidateHookTriggers(t *testing.T) { + t.Parallel() + tests := []struct { name string hooks map[string]Hook @@ -1100,6 +1102,7 @@ func TestValidateHookTriggers(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() err := ValidateHookTriggers(tt.hooks) if tt.wantErr { if err == nil { diff --git a/internal/hooks/doc.go b/internal/hooks/doc.go index f75d15f..2c88238 100644 --- a/internal/hooks/doc.go +++ b/internal/hooks/doc.go @@ -53,7 +53,9 @@ // - Main repo path for after:prune hooks (worktree is deleted) // - Repo root for merge hooks (both before and after) // -// Before hooks abort the operation on failure. After hook failures are logged +// Before hooks abort the current item on failure. For checkout and merge, this +// stops the command. For prune batches, the individual worktree is skipped but +// remaining worktrees are still processed. After hook failures are logged // but don't stop batch operations ([RunForEach]). // Use [RunSingle] for individual hook execution where errors are returned to the caller. // diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go index 337a47e..8f7051b 100644 --- a/internal/hooks/hooks.go +++ b/internal/hooks/hooks.go @@ -47,7 +47,7 @@ type Context struct { RepoDir string // absolute main repo path Branch string // branch name Repo string // registered repo name (as shown in wt repo list) - Trigger string // command that triggered the hook (checkout, prune, merge) + Trigger string // command that triggered the hook (checkout, prune, merge, run) Action string // checkout subtype: create, open, pr, manual (for wt hook) Phase string // "before" or "after" Env map[string]string // custom variables from --arg key=value flags @@ -69,8 +69,13 @@ func SelectHooks(cfg config.HooksConfig, hookNames []string, noHook bool, cmdTyp return nil, nil } - // If explicit hooks specified, use them directly (ignores "on" condition) + // If explicit hooks specified, use them directly (ignores "on" condition). + // Only return explicit hooks in the "after" phase to avoid running them twice + // (once in before-hooks and once in after-hooks). if len(hookNames) > 0 { + if phase == PhaseBefore { + return nil, nil + } var matches []HookMatch for _, hookName := range hookNames { hook, exists := cfg.Hooks[hookName] diff --git a/internal/hooks/hooks_test.go b/internal/hooks/hooks_test.go index cca999f..3bcfd4a 100644 --- a/internal/hooks/hooks_test.go +++ b/internal/hooks/hooks_test.go @@ -256,6 +256,34 @@ func TestSelectHooks_NoOnCondition(t *testing.T) { } } +func TestSelectHooks_ExplicitHooksSkipBeforePhase(t *testing.T) { + t.Parallel() + + hooksConfig := config.HooksConfig{ + Hooks: map[string]config.Hook{ + "guard": {Command: "echo guard", On: []string{"before:checkout"}}, + }, + } + + // Explicit hooks should NOT run in before phase (to avoid double execution) + matches, err := SelectHooks(hooksConfig, []string{"guard"}, false, CommandCheckout, "", PhaseBefore) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(matches) != 0 { + t.Errorf("expected no hooks in before phase with explicit --hook, got %d", len(matches)) + } + + // Explicit hooks should run in after phase + matches, err = SelectHooks(hooksConfig, []string{"guard"}, false, CommandCheckout, "", PhaseAfter) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(matches) != 1 { + t.Errorf("expected 1 hook in after phase with explicit --hook, got %d", len(matches)) + } +} + func TestSelectHooks_EmptyConfig(t *testing.T) { hooksConfig := config.HooksConfig{ Hooks: map[string]config.Hook{},