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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,9 @@ git gtr config add gtr.copy.include "**/.env.example"

# Run setup after creating worktrees
git gtr config add gtr.hook.postCreate "npm install"

# Re-source environment after gtr cd (runs in current shell)
git gtr config add gtr.hook.postCd "source ./vars.sh"
```

### Team Configuration (.gtrconfig)
Expand Down
98 changes: 96 additions & 2 deletions bin/gtr
Original file line number Diff line number Diff line change
Expand Up @@ -1606,7 +1606,40 @@ gtr() {
if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then
shift
local dir
dir="$(command git gtr go "$@")" && cd "$dir"
dir="$(command git gtr go "$@")" && cd "$dir" && {
local _gtr_hooks _gtr_hook _gtr_seen _gtr_config_file
_gtr_hooks=""
_gtr_seen=""
# Read from git config (local > global > system)
_gtr_hooks="$(git config --get-all gtr.hook.postCd 2>/dev/null)" || true
# Read from .gtrconfig if it exists
_gtr_config_file="$(git rev-parse --show-toplevel 2>/dev/null)/.gtrconfig"
if [ -f "$_gtr_config_file" ]; then
local _gtr_file_hooks
_gtr_file_hooks="$(git config -f "$_gtr_config_file" --get-all hooks.postCd 2>/dev/null)" || true
if [ -n "$_gtr_file_hooks" ]; then
if [ -n "$_gtr_hooks" ]; then
_gtr_hooks="$_gtr_hooks"$'\n'"$_gtr_file_hooks"
else
_gtr_hooks="$_gtr_file_hooks"
fi
fi
fi
if [ -n "$_gtr_hooks" ]; then
# Deduplicate while preserving order
_gtr_seen=""
export WORKTREE_PATH="$dir"
export REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
export BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
while IFS= read -r _gtr_hook; do
[ -z "$_gtr_hook" ] && continue
case "$_gtr_seen" in *"|$_gtr_hook|"*) continue ;; esac
_gtr_seen="$_gtr_seen|$_gtr_hook|"
eval "$_gtr_hook" || echo "gtr: postCd hook failed: $_gtr_hook" >&2
done <<< "$_gtr_hooks"
unset WORKTREE_PATH REPO_ROOT BRANCH
fi
}
else
command git gtr "$@"
fi
Expand All @@ -1628,7 +1661,40 @@ gtr() {
if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then
shift
local dir
dir="$(command git gtr go "$@")" && cd "$dir"
dir="$(command git gtr go "$@")" && cd "$dir" && {
local _gtr_hooks _gtr_hook _gtr_seen _gtr_config_file
_gtr_hooks=""
_gtr_seen=""
# Read from git config (local > global > system)
_gtr_hooks="$(git config --get-all gtr.hook.postCd 2>/dev/null)" || true
# Read from .gtrconfig if it exists
_gtr_config_file="$(git rev-parse --show-toplevel 2>/dev/null)/.gtrconfig"
if [ -f "$_gtr_config_file" ]; then
local _gtr_file_hooks
_gtr_file_hooks="$(git config -f "$_gtr_config_file" --get-all hooks.postCd 2>/dev/null)" || true
if [ -n "$_gtr_file_hooks" ]; then
if [ -n "$_gtr_hooks" ]; then
_gtr_hooks="$_gtr_hooks"$'\n'"$_gtr_file_hooks"
else
_gtr_hooks="$_gtr_file_hooks"
fi
fi
fi
if [ -n "$_gtr_hooks" ]; then
# Deduplicate while preserving order
_gtr_seen=""
export WORKTREE_PATH="$dir"
export REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
export BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
while IFS= read -r _gtr_hook; do
[ -z "$_gtr_hook" ] && continue
case "$_gtr_seen" in *"|$_gtr_hook|"*) continue ;; esac
_gtr_seen="$_gtr_seen|$_gtr_hook|"
eval "$_gtr_hook" || echo "gtr: postCd hook failed: $_gtr_hook" >&2
done <<< "$_gtr_hooks"
unset WORKTREE_PATH REPO_ROOT BRANCH
fi
}
else
command git gtr "$@"
fi
Expand All @@ -1650,6 +1716,33 @@ function gtr
if test (count $argv) -gt 0; and test "$argv[1]" = "cd"
set -l dir (command git gtr go $argv[2..])
and cd $dir
and begin
set -l _gtr_hooks
set -l _gtr_seen
# Read from git config (local > global > system)
set -l _gtr_git_hooks (git config --get-all gtr.hook.postCd 2>/dev/null)
# Read from .gtrconfig if it exists
set -l _gtr_config_file (git rev-parse --show-toplevel 2>/dev/null)"/.gtrconfig"
set -l _gtr_file_hooks
if test -f "$_gtr_config_file"
set _gtr_file_hooks (git config -f "$_gtr_config_file" --get-all hooks.postCd 2>/dev/null)
end
# Merge and deduplicate
set _gtr_hooks $_gtr_git_hooks $_gtr_file_hooks
if test (count $_gtr_hooks) -gt 0
set -lx WORKTREE_PATH "$dir"
set -lx REPO_ROOT (git rev-parse --show-toplevel 2>/dev/null)
set -lx BRANCH (git rev-parse --abbrev-ref HEAD 2>/dev/null)
for _gtr_hook in $_gtr_hooks
if test -n "$_gtr_hook"
if not contains -- "$_gtr_hook" $_gtr_seen
set -a _gtr_seen "$_gtr_hook"
eval "$_gtr_hook"; or echo "gtr: postCd hook failed: $_gtr_hook" >&2
end
end
end
end
end
else
command git gtr $argv
end
Expand Down Expand Up @@ -1991,6 +2084,7 @@ CONFIGURATION OPTIONS:
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)
gtr.hook.postCd Post-cd hooks (multi-valued, shell integration only)

────────────────────────────────────────────────────────────────────────────────

Expand Down
4 changes: 2 additions & 2 deletions completions/_git-gtr
Original file line number Diff line number Diff line change
Expand Up @@ -172,15 +172,15 @@ _git-gtr() {
'--local[Use local git config]' \
'--global[Use global git config]' \
'--system[Use system git config]' \
'*:config key:(gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.provider gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove)'
'*:config key:(gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.provider gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd)'
;;
set|add|unset)
# Write operations only support --local and --global
# (--system may require root or appropriate file permissions)
_arguments \
'--local[Use local git config]' \
'--global[Use global git config]' \
'*:config key:(gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.provider gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove)'
'*:config key:(gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.provider gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd)'
;;
esac
fi
Expand Down
1 change: 1 addition & 0 deletions completions/git-gtr.fish
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ complete -f -c git -n '__fish_git_gtr_using_command config' -a "
gtr.hook.postCreate\t'Post-create hook'
gtr.hook.preRemove\t'Pre-remove hook (abort on failure)'
gtr.hook.postRemove\t'Post-remove hook'
gtr.hook.postCd\t'Post-cd hook (shell integration only)'
"

# Helper function to get branch names and special '1' for main repo
Expand Down
4 changes: 2 additions & 2 deletions completions/gtr.bash
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,15 @@ _git_gtr() {
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "--local --global --system" -- "$cur"))
else
COMPREPLY=($(compgen -W "gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.provider gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove" -- "$cur"))
COMPREPLY=($(compgen -W "gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.provider gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd" -- "$cur"))
fi
;;
set|add|unset)
# Write operations only support --local and --global (--system requires root)
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "--local --global" -- "$cur"))
else
COMPREPLY=($(compgen -W "gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.provider gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove" -- "$cur"))
COMPREPLY=($(compgen -W "gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.provider gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd" -- "$cur"))
fi
;;
esac
Expand Down
16 changes: 11 additions & 5 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,17 +292,23 @@ git gtr config add gtr.hook.preRemove "npm run cleanup"

# Post-remove hooks
git gtr config add gtr.hook.postRemove "echo 'Cleaned up!'"

# Post-cd hooks (run after gtr cd, in current shell)
git gtr config add gtr.hook.postCd "source ./vars.sh"
```

**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 |
| 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 |
| `postCd` | After `gtr cd` changes directory | Re-source environment, update shell context |

> **Note:** Pre-remove hooks abort removal on failure. Use `--force` to skip failed hooks.
>
> **Note:** `postCd` hooks run in the **current shell** (not a subshell) so they can modify environment variables. They only run via `gtr cd` (shell integration), not `git gtr go`. Failures warn but don't undo the `cd`.

**Environment variables available in hooks:**

Expand Down
2 changes: 2 additions & 0 deletions lib/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ cfg_map_to_file_key() {
gtr.hook.postCreate) echo "hooks.postCreate" ;;
gtr.hook.preRemove) echo "hooks.preRemove" ;;
gtr.hook.postRemove) echo "hooks.postRemove" ;;
gtr.hook.postCd) echo "hooks.postCd" ;;
gtr.editor.default) echo "defaults.editor" ;;
gtr.editor.workspace) echo "editor.workspace" ;;
gtr.ai.default) echo "defaults.ai" ;;
Expand Down Expand Up @@ -307,6 +308,7 @@ cfg_list() {
hooks.postCreate) mapped_key="gtr.hook.postCreate" ;;
hooks.preRemove) mapped_key="gtr.hook.preRemove" ;;
hooks.postRemove) mapped_key="gtr.hook.postRemove" ;;
hooks.postCd) mapped_key="gtr.hook.postCd" ;;
defaults.editor) mapped_key="gtr.editor.default" ;;
editor.workspace) mapped_key="gtr.editor.workspace" ;;
defaults.ai) mapped_key="gtr.ai.default" ;;
Expand Down
4 changes: 4 additions & 0 deletions templates/.gtrconfig.example
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
# Commands to run after removing a worktree
# postRemove = echo "Removed worktree for branch $BRANCH"

# Commands to run after 'gtr cd' changes directory (shell integration only)
# Runs in the current shell (not a subshell) — can source files and set env vars
# postCd = source ./vars.sh

[defaults]
# Default editor for 'git gtr editor' command
# Available: cursor, vscode, zed, windsurf, or custom via GTR_EDITOR_CMD
Expand Down