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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ cmd_editor() → resolve_target() → load_editor_adapter() → editor_open()

**`.gtrconfig`**: Team-shared config using gitconfig syntax, parsed via `git config -f`. Keys map differently from git config (e.g., `gtr.copy.include` → `copy.include`, `gtr.hook.postCreate` → `hooks.postCreate`). See the .gtrconfig Key Mapping table in README or `docs/configuration.md`.

**`init` command**: Outputs shell functions for `gtr cd <branch>` navigation. Users add `eval "$(git gtr init bash)"` to their shell rc file.
**`init` command**: Outputs shell functions for `gtr cd <branch>` navigation. Output is cached to `~/.cache/gtr/` and auto-invalidates on version change. Users source the cache file directly in their shell rc for fast startup (see `git gtr help init`).

**`clean --merged`**: Removes worktrees whose PRs/MRs are merged. Auto-detects GitHub (`gh`) or GitLab (`glab`) from the `origin` remote URL. Override with `gtr.provider` config for self-hosted instances.

Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ git gtr run my-feature npm test # Run tests

# Navigate to worktree
gtr cd # Interactive picker (requires fzf + shell integration)
gtr cd my-feature # Requires: eval "$(git gtr init bash)"
gtr cd my-feature # Requires shell integration (see below)
cd "$(git gtr go my-feature)" # Alternative without shell integration

# List all worktrees
Expand Down Expand Up @@ -214,15 +214,29 @@ cd "$(git gtr go 1)" # Navigate to main repo
**Tip:** For easier navigation, use `git gtr init` to enable `gtr cd`:

```bash
# Add to ~/.bashrc or ~/.zshrc (one-time setup)
eval "$(git gtr init bash)"
# Bash (add to ~/.bashrc)
_gtr_init="${XDG_CACHE_HOME:-$HOME/.cache}/gtr/init-gtr.bash"
[[ -f "$_gtr_init" ]] || eval "$(git gtr init bash)" || true
source "$_gtr_init" 2>/dev/null || true; unset _gtr_init

# Zsh (add to ~/.zshrc)
_gtr_init="${XDG_CACHE_HOME:-$HOME/.cache}/gtr/init-gtr.zsh"
[[ -f "$_gtr_init" ]] || eval "$(git gtr init zsh)" || true
source "$_gtr_init" 2>/dev/null || true; unset _gtr_init

# Fish (add to ~/.config/fish/config.fish)
set -l _gtr_init (test -n "$XDG_CACHE_HOME" && echo $XDG_CACHE_HOME || echo $HOME/.cache)/gtr/init-gtr.fish
test -f "$_gtr_init"; or git gtr init fish >/dev/null 2>&1
source "$_gtr_init" 2>/dev/null

# Then navigate with:
gtr cd # Interactive worktree picker (requires fzf)
gtr cd my-feature
gtr cd 1
```

The cache generates on first run and refreshes the next time `git gtr init <shell>` runs. To force-regenerate: `rm -rf ~/.cache/gtr`

With [fzf](https://github.com/junegunn/fzf) installed, `gtr cd` (no arguments) opens a command palette with git log preview and keybindings: `ctrl-e` editor, `ctrl-a` AI, `ctrl-d` delete, `ctrl-y` copy, `ctrl-r` refresh.

> **Note:** If `gtr` conflicts with another command (e.g., GNU `tr` from coreutils), use `--as` to pick a different name:
Expand Down
4 changes: 2 additions & 2 deletions bin/git-gtr
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ main() {
local _shell_name
_shell_name="$(basename "${SHELL:-bash}")"
log_error "'cd' requires shell integration (subprocesses cannot change your shell's directory)"
log_info "Set up with: eval \"\$(git gtr init $_shell_name)\""
log_info "Then use: gtr cd [<branch>]"
log_info "Run 'git gtr help init' for setup instructions"
log_info "Then use: gtr cd [<branch>]"
exit 1
;;
*)
Expand Down
10 changes: 2 additions & 8 deletions lib/commands/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,10 @@ cmd_doctor() {
fish) _rc_file="$HOME/.config/fish/config.fish" ;;
*) _rc_file="" ;;
esac
if [ -n "$_rc_file" ] && [ -f "$_rc_file" ] && grep -q 'git gtr init' "$_rc_file" 2>/dev/null; then
if [ -n "$_rc_file" ] && [ -f "$_rc_file" ] && grep -qE 'git gtr init|gtr/init-' "$_rc_file" 2>/dev/null; then
echo "[OK] Shell integration: loaded (gtr cd available)"
elif [ -n "$_rc_file" ]; then
local _init_hint
if [ "$_shell_name" = "fish" ]; then
_init_hint="git gtr init fish | source"
else
_init_hint="eval \"\$(git gtr init $_shell_name)\""
fi
echo "[i] Shell integration: $_init_hint in ${_rc_file##*/} for gtr cd"
echo "[i] Shell integration: run 'git gtr help init' for setup instructions"
fi

echo ""
Expand Down
25 changes: 19 additions & 6 deletions lib/commands/help.sh
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ Usage: git gtr go <branch>

Prints the absolute path to the specified worktree. Useful for navigation
with cd or for scripting. For direct cd support, use shell integration:
eval "$(git gtr init bash)" # then: gtr cd <branch>
git gtr help init # see setup instructions
gtr cd <branch> # then navigate directly

Special:
Use '1' for the main repo root: git gtr go 1
Expand Down Expand Up @@ -345,21 +346,32 @@ Usage: git gtr init <shell> [--as <name>]
Generates shell functions for enhanced features like 'gtr cd <branch>'
which changes directory to a worktree. Add to your shell configuration.

Output is cached to ~/.cache/gtr/ for fast shell startup (~1ms vs ~60ms).
The cache refreshes the next time 'git gtr init <shell>' runs (checks version).
With the recommended setup below, it regenerates when the cache file is missing.
To force-regenerate: rm -rf ~/.cache/gtr

Supported shells: bash, zsh, fish

Options:
--as <name> Set custom function name (default: gtr)
Useful if 'gtr' conflicts with another command (e.g., GNU tr)

Setup:
Setup (sources cached output directly for fast startup):
# Bash (add to ~/.bashrc)
eval "$(git gtr init bash)"
_gtr_init="${XDG_CACHE_HOME:-$HOME/.cache}/gtr/init-gtr.bash"
[[ -f "$_gtr_init" ]] || eval "$(git gtr init bash)" || true
source "$_gtr_init" 2>/dev/null || true; unset _gtr_init

# Zsh (add to ~/.zshrc)
eval "$(git gtr init zsh)"
_gtr_init="${XDG_CACHE_HOME:-$HOME/.cache}/gtr/init-gtr.zsh"
[[ -f "$_gtr_init" ]] || eval "$(git gtr init zsh)" || true
source "$_gtr_init" 2>/dev/null || true; unset _gtr_init

# Fish (add to ~/.config/fish/config.fish)
git gtr init fish | source
set -l _gtr_init (test -n "$XDG_CACHE_HOME" && echo $XDG_CACHE_HOME || echo $HOME/.cache)/gtr/init-gtr.fish
test -f "$_gtr_init"; or git gtr init fish >/dev/null 2>&1
source "$_gtr_init" 2>/dev/null

# Custom function name (avoids conflict with coreutils gtr)
eval "$(git gtr init zsh --as gwtr)"
Expand Down Expand Up @@ -558,7 +570,8 @@ SETUP & MAINTENANCE:
init <shell> [--as <name>]
Generate shell integration for cd support (bash, zsh, fish)
--as <name>: custom function name (default: gtr)
Usage: eval "$(git gtr init bash)"
Output is cached for fast startup (refreshes when 'git gtr init' runs)
See git gtr help init for recommended setup
With fzf: 'gtr cd' opens a command palette (preview, editor, AI, delete)

version
Expand Down
51 changes: 33 additions & 18 deletions lib/commands/init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,33 +50,49 @@ cmd_init() {
return 1
fi

# Resolve generator function
local generator
case "$shell" in
bash)
_init_bash | sed "s/__FUNC__/$func_name/g"
;;
zsh)
_init_zsh | sed "s/__FUNC__/$func_name/g"
;;
fish)
_init_fish | sed "s/__FUNC__/$func_name/g"
;;
"")
show_command_help
;;
bash) generator="_init_bash" ;;
zsh) generator="_init_zsh" ;;
fish) generator="_init_fish" ;;
"") show_command_help; return 0 ;;
*)
log_error "Unknown shell: $shell"
log_error "Supported shells: bash, zsh, fish"
log_info "Run 'git gtr init --help' for usage"
return 1
;;
esac

# Generate output (cached to ~/.cache/gtr/, auto-invalidates on version change)
local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/gtr"
local cache_file="$cache_dir/init-${func_name}.${shell}"
local cache_stamp="# gtr-cache: version=${GTR_VERSION:-unknown} func=$func_name shell=$shell"

# Return cached output if version matches
if [ -f "$cache_file" ]; then
local first_line
first_line="$(head -1 "$cache_file")"
if [ "$first_line" = "$cache_stamp" ]; then
tail -n +2 "$cache_file"
return 0
fi
fi

# Generate, output, and cache (output first so set -e cache failures don't swallow it)
local output
output="$("$generator" | sed "s/__FUNC__/$func_name/g")"
printf '%s\n' "$output"
if mkdir -p "$cache_dir" 2>/dev/null; then
printf '%s\n%s\n' "$cache_stamp" "$output" > "$cache_file" 2>/dev/null || true
fi
}

_init_bash() {
cat <<'BASH'
# git-gtr shell integration
# Add to ~/.bashrc:
# eval "$(git gtr init bash)"
# git-gtr shell integration (cached to ~/.cache/gtr/)
# Setup: see git gtr help init

__FUNC__() {
if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then
Expand Down Expand Up @@ -196,9 +212,8 @@ BASH

_init_zsh() {
cat <<'ZSH'
# git-gtr shell integration
# Add to ~/.zshrc:
# eval "$(git gtr init zsh)"
# git-gtr shell integration (cached to ~/.cache/gtr/)
# Setup: see git gtr help init

__FUNC__() {
emulate -L zsh
Expand Down
57 changes: 57 additions & 0 deletions tests/init.bats
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ load test_helper

setup() {
source "$PROJECT_ROOT/lib/commands/init.sh"
# Isolate cache to temp dir so tests don't pollute ~/.cache or each other
export XDG_CACHE_HOME="$BATS_TMPDIR/gtr-init-cache-$$"
export GTR_VERSION="test"
}

teardown() {
rm -rf "$BATS_TMPDIR/gtr-init-cache-$$"
}

# ── Default function name ────────────────────────────────────────────────────
Expand Down Expand Up @@ -496,3 +503,53 @@ setup() {
[[ "$output" == *"command git gtr"* ]]
[[ "$output" == *"git gtr list --porcelain"* ]]
}

# ── caching (default behavior) ──────────────────────────────────────────────

@test "init creates cache file and returns output" {
GTR_VERSION="9.9.9" run cmd_init zsh
[ "$status" -eq 0 ]
[[ "$output" == *"gtr()"* ]]
[ -f "$XDG_CACHE_HOME/gtr/init-gtr.zsh" ]
}

@test "init returns cached output on second call" {
# First call: generates and caches
GTR_VERSION="9.9.9" run cmd_init bash
[ "$status" -eq 0 ]
local first_output="$output"
# Second call: reads from cache
GTR_VERSION="9.9.9" run cmd_init bash
[ "$status" -eq 0 ]
[ "$output" = "$first_output" ]
}

@test "cache invalidates when version changes" {
# Generate with version 1.0.0
GTR_VERSION="1.0.0" run cmd_init zsh
[ "$status" -eq 0 ]
# Check cache stamp
local stamp
stamp="$(head -1 "$XDG_CACHE_HOME/gtr/init-gtr.zsh")"
[[ "$stamp" == *"version=1.0.0"* ]]
# Regenerate with version 2.0.0
GTR_VERSION="2.0.0" run cmd_init zsh
[ "$status" -eq 0 ]
stamp="$(head -1 "$XDG_CACHE_HOME/gtr/init-gtr.zsh")"
[[ "$stamp" == *"version=2.0.0"* ]]
}

@test "cache uses --as func name in cache key" {
GTR_VERSION="9.9.9" run cmd_init bash --as myfn
[ "$status" -eq 0 ]
[[ "$output" == *"myfn()"* ]]
[ -f "$XDG_CACHE_HOME/gtr/init-myfn.bash" ]
}

@test "cache works for all shells" {
for sh in bash zsh fish; do
GTR_VERSION="9.9.9" run cmd_init "$sh"
[ "$status" -eq 0 ]
[ -f "$XDG_CACHE_HOME/gtr/init-gtr.${sh}" ]
done
}