Skip to content

Commit 35d8119

Browse files
authored
feat: add preRemove hooks (#48)
1 parent c7279bc commit 35d8119

File tree

8 files changed

+56
-8
lines changed

8 files changed

+56
-8
lines changed

CLAUDE.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,18 @@ rm .env.example
173173
git config --add gtr.hook.postCreate "echo 'Created!' > /tmp/gtr-test"
174174
./bin/gtr new test-hooks
175175
# Expected: Creates /tmp/gtr-test file
176+
git config --add gtr.hook.preRemove "echo 'Pre-remove!' > /tmp/gtr-pre-removed"
176177
git config --add gtr.hook.postRemove "echo 'Removed!' > /tmp/gtr-removed"
177178
./bin/gtr rm test-hooks
178-
# Expected: Creates /tmp/gtr-removed file
179+
# Expected: Creates /tmp/gtr-pre-removed and /tmp/gtr-removed files
180+
181+
# Test pre-remove hook failure aborts removal
182+
git config gtr.hook.preRemove "exit 1"
183+
./bin/gtr new test-hook-fail
184+
./bin/gtr rm test-hook-fail
185+
# Expected: Removal aborted due to hook failure
186+
./bin/gtr rm test-hook-fail --force
187+
# Expected: Removal proceeds despite hook failure
179188
```
180189

181190
### Debugging Bash Scripts
@@ -440,6 +449,7 @@ All config keys use `gtr.*` prefix and are managed via `git config`. Configurati
440449
- `gtr.copy.includeDirs`: Multi-valued directory patterns to copy (e.g., "node_modules", ".venv", "vendor")
441450
- `gtr.copy.excludeDirs`: Multi-valued directory patterns to exclude when copying (supports globs like "node_modules/.cache", "\*/.cache")
442451
- `gtr.hook.postCreate`: Multi-valued commands to run after creating worktree
452+
- `gtr.hook.preRemove`: Multi-valued commands to run before removing worktree (abort on failure unless --force)
443453
- `gtr.hook.postRemove`: Multi-valued commands to run after removing worktree
444454

445455
### File-based Configuration
@@ -456,6 +466,7 @@ All config keys use `gtr.*` prefix and are managed via `git config`. Configurati
456466
| `gtr.copy.includeDirs` | `copy.includeDirs` |
457467
| `gtr.copy.excludeDirs` | `copy.excludeDirs` |
458468
| `gtr.hook.postCreate` | `hooks.postCreate` |
469+
| `gtr.hook.preRemove` | `hooks.preRemove` |
459470
| `gtr.hook.postRemove` | `hooks.postRemove` |
460471
| `gtr.editor.default` | `defaults.editor` |
461472
| `gtr.ai.default` | `defaults.ai` |
@@ -471,12 +482,14 @@ All config keys use `gtr.*` prefix and are managed via `git config`. Configurati
471482
- `GTR_AI_CMD`: Generic AI tool command for custom tools without adapter files
472483
- `GTR_AI_CMD_NAME`: First word of `GTR_AI_CMD` used for availability checks
473484

474-
**Hook environment variables** (available in `gtr.hook.postCreate` and `gtr.hook.postRemove` scripts):
485+
**Hook environment variables** (available in `gtr.hook.postCreate`, `gtr.hook.preRemove`, and `gtr.hook.postRemove` scripts):
475486

476487
- `REPO_ROOT`: Repository root path
477-
- `WORKTREE_PATH`: New worktree path
488+
- `WORKTREE_PATH`: Worktree path
478489
- `BRANCH`: Branch name
479490

491+
**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.
492+
480493
## Important Implementation Details
481494

482495
**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
493506

494507
**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`).
495508

496-
**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"`.
509+
**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"`.
497510

498511
**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.
499512

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,21 +454,34 @@ git gtr config add gtr.copy.excludeDirs "*/.cache" # Exclude .cache
454454

455455
### Hooks
456456

457-
Run custom commands after worktree operations:
457+
Run custom commands during worktree operations:
458458

459459
```bash
460460
# Post-create hooks (multi-valued, run in order)
461461
git gtr config add gtr.hook.postCreate "npm install"
462462
git gtr config add gtr.hook.postCreate "npm run build"
463463

464+
# Pre-remove hooks (run before deletion, abort on failure)
465+
git gtr config add gtr.hook.preRemove "npm run cleanup"
466+
464467
# Post-remove hooks
465468
git gtr config add gtr.hook.postRemove "echo 'Cleaned up!'"
466469
```
467470

471+
**Hook execution order:**
472+
473+
| Hook | Timing | Use Case |
474+
|------|--------|----------|
475+
| `postCreate` | After worktree creation | Setup, install dependencies |
476+
| `preRemove` | Before worktree deletion | Cleanup requiring directory access |
477+
| `postRemove` | After worktree deletion | Notifications, logging |
478+
479+
> **Note:** Pre-remove hooks abort removal on failure. Use `--force` to skip failed hooks.
480+
468481
**Environment variables available in hooks:**
469482

470483
- `REPO_ROOT` - Repository root path
471-
- `WORKTREE_PATH` - New worktree path
484+
- `WORKTREE_PATH` - Worktree path
472485
- `BRANCH` - Branch name
473486

474487
**Examples for different build tools:**

bin/gtr

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,19 @@ cmd_remove() {
351351

352352
log_step "Removing worktree: $branch_name"
353353

354+
# Run pre-remove hooks (abort on failure unless --force)
355+
if ! run_hooks_in preRemove "$worktree_path" \
356+
REPO_ROOT="$repo_root" \
357+
WORKTREE_PATH="$worktree_path" \
358+
BRANCH="$branch_name"; then
359+
if [ "$force" -eq 0 ]; then
360+
log_error "Pre-remove hook failed for $branch_name. Use --force to skip hooks."
361+
continue
362+
else
363+
log_warn "Pre-remove hook failed, continuing due to --force"
364+
fi
365+
fi
366+
354367
# Remove the worktree
355368
if ! remove_worktree "$worktree_path" "$force"; then
356369
continue
@@ -1290,6 +1303,7 @@ CONFIGURATION OPTIONS:
12901303
gtr.copy.excludeDirs Directories to exclude (multi-valued)
12911304
Supports glob patterns (e.g., "node_modules/.cache", "*/.npm")
12921305
gtr.hook.postCreate Post-create hooks (multi-valued)
1306+
gtr.hook.preRemove Pre-remove hooks (multi-valued, abort on failure)
12931307
gtr.hook.postRemove Post-remove hooks (multi-valued)
12941308
12951309
────────────────────────────────────────────────────────────────────────────────

completions/_git-gtr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ _git-gtr() {
111111
get|set|add|unset)
112112
_arguments \
113113
'--global[Use global git config]' \
114-
'*: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)'
114+
'*: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)'
115115
;;
116116
esac
117117
;;

completions/gtr.bash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ _git_gtr() {
6868
if [ "$cword" -eq 3 ]; then
6969
COMPREPLY=($(compgen -W "get set add unset" -- "$cur"))
7070
elif [ "$cword" -eq 4 ]; then
71-
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"))
71+
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"))
7272
fi
7373
;;
7474
esac

completions/gtr.fish

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ complete -f -c git -n '__fish_git_gtr_using_command config' -a "
8080
gtr.copy.includeDirs\t'Directories to copy (e.g., node_modules)'
8181
gtr.copy.excludeDirs\t'Directories to exclude'
8282
gtr.hook.postCreate\t'Post-create hook'
83+
gtr.hook.preRemove\t'Pre-remove hook (abort on failure)'
8384
gtr.hook.postRemove\t'Post-remove hook'
8485
"
8586

templates/.gtrconfig.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
# postCreate = cp .env.example .env
4343
# postCreate = echo "Created worktree at $WORKTREE_PATH"
4444

45+
# Commands to run BEFORE removing a worktree (hook runs in worktree directory)
46+
# If hook fails (non-zero exit), removal is aborted unless --force is used
47+
# preRemove = npm run cleanup
48+
4549
# Commands to run after removing a worktree
4650
# postRemove = echo "Removed worktree for branch $BRANCH"
4751

templates/gtr.config.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@
5959
# gtr.hook.postCreate = npm install
6060
# gtr.hook.postCreate = npm run build
6161

62+
# Commands to run before removing a worktree (abort on failure, use --force to skip)
63+
# gtr.hook.preRemove = npm run cleanup
64+
6265
# Commands to run after removing a worktree
6366
# gtr.hook.postRemove = echo "Cleaned up worktree"
6467

0 commit comments

Comments
 (0)