diff --git a/CLAUDE.md b/CLAUDE.md index 949a9bb..aabea71 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 ` navigation. Users add `eval "$(git gtr init bash)"` to their shell rc file. +**`init` command**: Outputs shell functions for `gtr cd ` 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. diff --git a/README.md b/README.md index 22cae0a..e3b0ec6 100644 --- a/README.md +++ b/README.md @@ -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 @@ -214,8 +214,20 @@ 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) @@ -223,6 +235,8 @@ gtr cd my-feature gtr cd 1 ``` +The cache generates on first run and refreshes the next time `git gtr init ` 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: diff --git a/bin/git-gtr b/bin/git-gtr index 2929911..9770fb7 100755 --- a/bin/git-gtr +++ b/bin/git-gtr @@ -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 []" + log_info "Run 'git gtr help init' for setup instructions" + log_info "Then use: gtr cd []" exit 1 ;; *) diff --git a/lib/commands/doctor.sh b/lib/commands/doctor.sh index df749bc..da49668 100644 --- a/lib/commands/doctor.sh +++ b/lib/commands/doctor.sh @@ -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 "" diff --git a/lib/commands/help.sh b/lib/commands/help.sh index e3aef18..db2f340 100644 --- a/lib/commands/help.sh +++ b/lib/commands/help.sh @@ -102,7 +102,8 @@ Usage: git gtr go 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 + git gtr help init # see setup instructions + gtr cd # then navigate directly Special: Use '1' for the main repo root: git gtr go 1 @@ -345,21 +346,32 @@ Usage: git gtr init [--as ] Generates shell functions for enhanced features like 'gtr cd ' 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 ' 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 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)" @@ -558,7 +570,8 @@ SETUP & MAINTENANCE: init [--as ] Generate shell integration for cd support (bash, zsh, fish) --as : 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 diff --git a/lib/commands/init.sh b/lib/commands/init.sh index bd776a7..e35af99 100644 --- a/lib/commands/init.sh +++ b/lib/commands/init.sh @@ -50,19 +50,13 @@ 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" @@ -70,13 +64,35 @@ cmd_init() { 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 @@ -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 diff --git a/tests/init.bats b/tests/init.bats index 9aeaf45..e34fc8f 100644 --- a/tests/init.bats +++ b/tests/init.bats @@ -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 ──────────────────────────────────────────────────── @@ -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 +}