Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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` |
Expand All @@ -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`.
Expand All @@ -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.

Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down
14 changes: 14 additions & 0 deletions bin/gtr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

────────────────────────────────────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion completions/_git-gtr
Original file line number Diff line number Diff line change
Expand Up @@ -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
;;
Expand Down
2 changes: 1 addition & 1 deletion completions/gtr.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions completions/gtr.fish
Original file line number Diff line number Diff line change
Expand Up @@ -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'
"

Expand Down
4 changes: 4 additions & 0 deletions templates/.gtrconfig.example
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
3 changes: 3 additions & 0 deletions templates/gtr.config.example
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down