From ea29130292f2410962b6e256902a59df2b8333af Mon Sep 17 00:00:00 2001 From: Damian Lewis <7067514+damianlewis@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:34:00 +0000 Subject: [PATCH 1/5] feat: add preRemove hooks Add gtr.hook.preRemove configuration for hooks that run before worktree deletion, allowing cleanup tasks that need directory access. Hook failures abort removal unless --force is used. --- bin/gtr | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bin/gtr b/bin/gtr index 93706a9..60eee3a 100755 --- a/bin/gtr +++ b/bin/gtr @@ -351,6 +351,19 @@ cmd_remove() { log_step "Removing worktree: $branch_name" + # Run pre-remove hooks (abort on failure unless --force) + if ! run_hooks_in preRemove "$worktree_path" \ + REPO_ROOT="$repo_root" \ + WORKTREE_PATH="$worktree_path" \ + BRANCH="$branch_name"; then + if [ "$force" -eq 0 ]; then + log_error "Pre-remove hook failed for $branch_name. Use --force to skip hooks." + continue + else + log_warn "Pre-remove hook failed, continuing due to --force" + fi + fi + # Remove the worktree if ! remove_worktree "$worktree_path" "$force"; then continue @@ -1290,6 +1303,7 @@ CONFIGURATION OPTIONS: gtr.copy.excludeDirs Directories to exclude (multi-valued) Supports glob patterns (e.g., "node_modules/.cache", "*/.npm") gtr.hook.postCreate Post-create hooks (multi-valued) + gtr.hook.preRemove Pre-remove hooks (multi-valued, abort on failure) gtr.hook.postRemove Post-remove hooks (multi-valued) ──────────────────────────────────────────────────────────────────────────────── From 14ed3ec75e47cd6955969e31bd9f6be5cf0c7f93 Mon Sep 17 00:00:00 2001 From: Damian Lewis <7067514+damianlewis@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:34:17 +0000 Subject: [PATCH 2/5] feat: add preRemove hook examples to templates Add commented preRemove hook examples to both .gtrconfig.example and gtr.config.example template files. --- templates/.gtrconfig.example | 4 ++++ templates/gtr.config.example | 3 +++ 2 files changed, 7 insertions(+) diff --git a/templates/.gtrconfig.example b/templates/.gtrconfig.example index f0dbeca..c9d76fc 100644 --- a/templates/.gtrconfig.example +++ b/templates/.gtrconfig.example @@ -42,6 +42,10 @@ # postCreate = cp .env.example .env # postCreate = echo "Created worktree at $WORKTREE_PATH" + # Commands to run BEFORE removing a worktree (hook runs in worktree directory) + # If hook fails (non-zero exit), removal is aborted unless --force is used + # preRemove = npm run cleanup + # Commands to run after removing a worktree # postRemove = echo "Removed worktree for branch $BRANCH" diff --git a/templates/gtr.config.example b/templates/gtr.config.example index 20a3af6..888feaf 100644 --- a/templates/gtr.config.example +++ b/templates/gtr.config.example @@ -59,6 +59,9 @@ # gtr.hook.postCreate = npm install # gtr.hook.postCreate = npm run build +# Commands to run before removing a worktree (abort on failure, use --force to skip) +# gtr.hook.preRemove = npm run cleanup + # Commands to run after removing a worktree # gtr.hook.postRemove = echo "Cleaned up worktree" From 57d85550dab906deb34453770f4a29f02406acba Mon Sep 17 00:00:00 2001 From: Damian Lewis <7067514+damianlewis@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:34:26 +0000 Subject: [PATCH 3/5] feat: add preRemove hook to shell completions Add gtr.hook.preRemove to tab completion for bash, zsh, and fish. --- completions/_git-gtr | 2 +- completions/gtr.bash | 2 +- completions/gtr.fish | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/completions/_git-gtr b/completions/_git-gtr index 675f03d..73df9f9 100644 --- a/completions/_git-gtr +++ b/completions/_git-gtr @@ -111,7 +111,7 @@ _git-gtr() { get|set|add|unset) _arguments \ '--global[Use global git config]' \ - '*:config key:(gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.postRemove)' + '*:config key:(gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove)' ;; esac ;; diff --git a/completions/gtr.bash b/completions/gtr.bash index 1d1fad4..41c9185 100644 --- a/completions/gtr.bash +++ b/completions/gtr.bash @@ -68,7 +68,7 @@ _git_gtr() { if [ "$cword" -eq 3 ]; then COMPREPLY=($(compgen -W "get set add unset" -- "$cur")) elif [ "$cword" -eq 4 ]; then - COMPREPLY=($(compgen -W "gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.postRemove" -- "$cur")) + COMPREPLY=($(compgen -W "gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove" -- "$cur")) fi ;; esac diff --git a/completions/gtr.fish b/completions/gtr.fish index eb64352..e0a5842 100644 --- a/completions/gtr.fish +++ b/completions/gtr.fish @@ -80,6 +80,7 @@ complete -f -c git -n '__fish_git_gtr_using_command config' -a " gtr.copy.includeDirs\t'Directories to copy (e.g., node_modules)' gtr.copy.excludeDirs\t'Directories to exclude' gtr.hook.postCreate\t'Post-create hook' + gtr.hook.preRemove\t'Pre-remove hook (abort on failure)' gtr.hook.postRemove\t'Post-remove hook' " From 075a2d68453aba26b173b258a486c8a1f670a58c Mon Sep 17 00:00:00 2001 From: Damian Lewis <7067514+damianlewis@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:34:38 +0000 Subject: [PATCH 4/5] docs: document preRemove hooks in README Add preRemove hook configuration examples and document hook execution order (preRemove runs before deletion, postRemove after). Note that --force bypasses hook failures. --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 07d5e7c..3aeca62 100644 --- a/README.md +++ b/README.md @@ -454,21 +454,34 @@ git gtr config add gtr.copy.excludeDirs "*/.cache" # Exclude .cache ### Hooks -Run custom commands after worktree operations: +Run custom commands during worktree operations: ```bash # Post-create hooks (multi-valued, run in order) git gtr config add gtr.hook.postCreate "npm install" git gtr config add gtr.hook.postCreate "npm run build" +# Pre-remove hooks (run before deletion, abort on failure) +git gtr config add gtr.hook.preRemove "npm run cleanup" + # Post-remove hooks git gtr config add gtr.hook.postRemove "echo 'Cleaned up!'" ``` +**Hook execution order:** + +| Hook | Timing | Use Case | +|------|--------|----------| +| `postCreate` | After worktree creation | Setup, install dependencies | +| `preRemove` | Before worktree deletion | Cleanup requiring directory access | +| `postRemove` | After worktree deletion | Notifications, logging | + +> **Note:** Pre-remove hooks abort removal on failure. Use `--force` to skip failed hooks. + **Environment variables available in hooks:** - `REPO_ROOT` - Repository root path -- `WORKTREE_PATH` - New worktree path +- `WORKTREE_PATH` - Worktree path - `BRANCH` - Branch name **Examples for different build tools:** From c898fcc47ab7fa50df14861a84c407702b15f1f5 Mon Sep 17 00:00:00 2001 From: Damian Lewis <7067514+damianlewis@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:34:47 +0000 Subject: [PATCH 5/5] docs: update CLAUDE.md with preRemove hook details Add preRemove hook to configuration reference, key mapping table, environment variables section, and manual testing checklist. --- CLAUDE.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 986bd8c..beb058e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -173,9 +173,18 @@ rm .env.example git config --add gtr.hook.postCreate "echo 'Created!' > /tmp/gtr-test" ./bin/gtr new test-hooks # Expected: Creates /tmp/gtr-test file +git config --add gtr.hook.preRemove "echo 'Pre-remove!' > /tmp/gtr-pre-removed" git config --add gtr.hook.postRemove "echo 'Removed!' > /tmp/gtr-removed" ./bin/gtr rm test-hooks -# Expected: Creates /tmp/gtr-removed file +# Expected: Creates /tmp/gtr-pre-removed and /tmp/gtr-removed files + +# Test pre-remove hook failure aborts removal +git config gtr.hook.preRemove "exit 1" +./bin/gtr new test-hook-fail +./bin/gtr rm test-hook-fail +# Expected: Removal aborted due to hook failure +./bin/gtr rm test-hook-fail --force +# Expected: Removal proceeds despite hook failure ``` ### Debugging Bash Scripts @@ -440,6 +449,7 @@ All config keys use `gtr.*` prefix and are managed via `git config`. Configurati - `gtr.copy.includeDirs`: Multi-valued directory patterns to copy (e.g., "node_modules", ".venv", "vendor") - `gtr.copy.excludeDirs`: Multi-valued directory patterns to exclude when copying (supports globs like "node_modules/.cache", "\*/.cache") - `gtr.hook.postCreate`: Multi-valued commands to run after creating worktree +- `gtr.hook.preRemove`: Multi-valued commands to run before removing worktree (abort on failure unless --force) - `gtr.hook.postRemove`: Multi-valued commands to run after removing worktree ### File-based Configuration @@ -456,6 +466,7 @@ All config keys use `gtr.*` prefix and are managed via `git config`. Configurati | `gtr.copy.includeDirs` | `copy.includeDirs` | | `gtr.copy.excludeDirs` | `copy.excludeDirs` | | `gtr.hook.postCreate` | `hooks.postCreate` | +| `gtr.hook.preRemove` | `hooks.preRemove` | | `gtr.hook.postRemove` | `hooks.postRemove` | | `gtr.editor.default` | `defaults.editor` | | `gtr.ai.default` | `defaults.ai` | @@ -471,12 +482,14 @@ All config keys use `gtr.*` prefix and are managed via `git config`. Configurati - `GTR_AI_CMD`: Generic AI tool command for custom tools without adapter files - `GTR_AI_CMD_NAME`: First word of `GTR_AI_CMD` used for availability checks -**Hook environment variables** (available in `gtr.hook.postCreate` and `gtr.hook.postRemove` scripts): +**Hook environment variables** (available in `gtr.hook.postCreate`, `gtr.hook.preRemove`, and `gtr.hook.postRemove` scripts): - `REPO_ROOT`: Repository root path -- `WORKTREE_PATH`: New worktree path +- `WORKTREE_PATH`: Worktree path - `BRANCH`: Branch name +**Note:** `preRemove` hooks run with cwd set to the worktree directory (before deletion). If a preRemove hook fails, removal is aborted unless `--force` is used. + ## Important Implementation Details **Worktree Path Resolution**: The `resolve_target()` function in `lib/core.sh` handles both branch names and the special ID '1'. It checks in order: special ID, current branch in main repo, sanitized path match, full directory scan. Returns tab-separated format: `is_main\tpath\tbranch`. @@ -493,7 +506,7 @@ All config keys use `gtr.*` prefix and are managed via `git config`. Configurati **Configuration Precedence**: The `cfg_default()` function in `lib/config.sh` checks local git config first, then `.gtrconfig` file, then global/system git config, then environment variables, then fallback values. Use `cfg_get_all(key, file_key, scope)` for multi-valued configs where `file_key` is the corresponding key in `.gtrconfig` (e.g., `copy.include` for `gtr.copy.include`). -**Multi-Value Configuration Pattern**: Some configs support multiple values (`gtr.copy.include`, `gtr.copy.exclude`, `gtr.copy.includeDirs`, `gtr.copy.excludeDirs`, `gtr.hook.postCreate`, `gtr.hook.postRemove`). The `cfg_get_all()` function merges values from local + global + system + `.gtrconfig` file and deduplicates. Set with: `git config --add gtr.copy.include "pattern"`. +**Multi-Value Configuration Pattern**: Some configs support multiple values (`gtr.copy.include`, `gtr.copy.exclude`, `gtr.copy.includeDirs`, `gtr.copy.excludeDirs`, `gtr.hook.postCreate`, `gtr.hook.preRemove`, `gtr.hook.postRemove`). The `cfg_get_all()` function merges values from local + global + system + `.gtrconfig` file and deduplicates. Set with: `git config --add gtr.copy.include "pattern"`. **Adapter Loading**: Adapters are sourced dynamically via `load_editor_adapter()` and `load_ai_adapter()` in `bin/gtr`. They must exist in `adapters/editor/` or `adapters/ai/` and define the required functions.