diff --git a/README.md b/README.md index 2f5ce8b..22cae0a 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ git gtr ai my-feature # Start claude 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)" cd "$(git gtr go my-feature)" # Alternative without shell integration @@ -217,10 +218,13 @@ cd "$(git gtr go 1)" # Navigate to main repo eval "$(git gtr init bash)" # Then navigate with: +gtr cd # Interactive worktree picker (requires fzf) gtr cd my-feature gtr cd 1 ``` +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: > > ```bash diff --git a/lib/commands/doctor.sh b/lib/commands/doctor.sh index 98aa624..6edf88c 100644 --- a/lib/commands/doctor.sh +++ b/lib/commands/doctor.sh @@ -88,7 +88,7 @@ cmd_doctor() { # Check hosting provider if [ -n "$repo_root" ]; then local provider - provider=$(detect_provider 2>/dev/null) + provider=$(detect_provider 2>/dev/null) || true if [ -n "$provider" ]; then echo "[OK] Provider: $provider" case "$provider" in @@ -112,6 +112,13 @@ cmd_doctor() { fi fi + # Check fzf (optional, for interactive picker) + if command -v fzf >/dev/null 2>&1; then + echo "[OK] fzf: $(fzf --version 2>/dev/null | awk '{print $1}') (interactive picker available)" + else + echo "[i] fzf: not found (install for interactive picker: gtr cd)" + fi + echo "" if [ "$issues" -eq 0 ]; then echo "Everything looks good!" diff --git a/lib/commands/help.sh b/lib/commands/help.sh index b7d020b..e3aef18 100644 --- a/lib/commands/help.sh +++ b/lib/commands/help.sh @@ -367,7 +367,17 @@ Setup: After setup: gtr cd my-feature # cd to worktree gtr cd 1 # cd to main repo + gtr cd # interactive picker (requires fzf) gtr # same as git gtr + +Command palette (gtr cd with no arguments, requires fzf): + enter cd into selected worktree + ctrl-e open in editor + ctrl-a start AI tool + ctrl-d delete worktree (with confirmation) + ctrl-y copy files to worktree + ctrl-r refresh list + esc cancel EOF } @@ -549,6 +559,7 @@ SETUP & MAINTENANCE: Generate shell integration for cd support (bash, zsh, fish) --as : custom function name (default: gtr) Usage: eval "$(git gtr init bash)" + With fzf: 'gtr cd' opens a command palette (preview, editor, AI, delete) version Show version @@ -572,6 +583,7 @@ WORKFLOW EXAMPLES: git gtr run feature/user-auth npm run dev # Start dev server # Navigate to worktree directory + gtr cd # Interactive picker (requires fzf) gtr cd feature/user-auth # With shell integration (git gtr init) cd "$(git gtr go feature/user-auth)" # Without shell integration diff --git a/lib/commands/init.sh b/lib/commands/init.sh index 09b385e..d29f02f 100644 --- a/lib/commands/init.sh +++ b/lib/commands/init.sh @@ -82,7 +82,33 @@ __FUNC__() { if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then shift local dir - dir="$(command git gtr go "$@")" && cd "$dir" && { + if [ "$#" -eq 0 ] && command -v fzf >/dev/null 2>&1; then + local _gtr_selection + _gtr_selection="$(command git gtr list --porcelain | fzf \ + --delimiter=$'\t' \ + --with-nth=2 \ + --ansi \ + --layout=reverse \ + --border \ + --prompt='Worktree> ' \ + --header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \ + --preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \ + --preview-window=right:50% \ + --bind='ctrl-e:execute(git gtr editor {2})' \ + --bind='ctrl-a:execute(git gtr ai {2})' \ + --bind='ctrl-d:execute(git gtr rm {2})+reload(git gtr list --porcelain)' \ + --bind='ctrl-y:execute(git gtr copy {2})' \ + --bind='ctrl-r:reload(git gtr list --porcelain)')" || return 0 + [ -z "$_gtr_selection" ] && return 0 + dir="$(printf '%s' "$_gtr_selection" | cut -f1)" + elif [ "$#" -eq 0 ]; then + echo "Usage: __FUNC__ cd " >&2 + echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2 + return 1 + else + dir="$(command git gtr go "$@")" || return $? + fi + cd "$dir" && { local _gtr_hooks _gtr_hook _gtr_seen _gtr_config_file _gtr_hooks="" _gtr_seen="" @@ -152,10 +178,37 @@ _init_zsh() { # eval "$(git gtr init zsh)" __FUNC__() { + emulate -L zsh if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then shift local dir - dir="$(command git gtr go "$@")" && cd "$dir" && { + if [ "$#" -eq 0 ] && command -v fzf >/dev/null 2>&1; then + local _gtr_selection + _gtr_selection="$(command git gtr list --porcelain | fzf \ + --delimiter=$'\t' \ + --with-nth=2 \ + --ansi \ + --layout=reverse \ + --border \ + --prompt='Worktree> ' \ + --header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \ + --preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \ + --preview-window=right:50% \ + --bind='ctrl-e:execute(git gtr editor {2})' \ + --bind='ctrl-a:execute(git gtr ai {2})' \ + --bind='ctrl-d:execute(git gtr rm {2})+reload(git gtr list --porcelain)' \ + --bind='ctrl-y:execute(git gtr copy {2})' \ + --bind='ctrl-r:reload(git gtr list --porcelain)')" || return 0 + [ -z "$_gtr_selection" ] && return 0 + dir="$(printf '%s' "$_gtr_selection" | cut -f1)" + elif [ "$#" -eq 0 ]; then + echo "Usage: __FUNC__ cd " >&2 + echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2 + return 1 + else + dir="$(command git gtr go "$@")" || return $? + fi + cd "$dir" && { local _gtr_hooks _gtr_hook _gtr_seen _gtr_config_file _gtr_hooks="" _gtr_seen="" @@ -232,8 +285,35 @@ _init_fish() { function __FUNC__ if test (count $argv) -gt 0; and test "$argv[1]" = "cd" - set -l dir (command git gtr go $argv[2..]) - and cd $dir + set -l dir + if test (count $argv) -eq 1; and type -q fzf + set -l _gtr_selection (command git gtr list --porcelain | fzf \ + --delimiter=\t \ + --with-nth=2 \ + --ansi \ + --layout=reverse \ + --border \ + --prompt='Worktree> ' \ + --header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \ + --preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \ + --preview-window=right:50% \ + --bind='ctrl-e:execute(git gtr editor {2})' \ + --bind='ctrl-a:execute(git gtr ai {2})' \ + --bind='ctrl-d:execute(git gtr rm {2})+reload(git gtr list --porcelain)' \ + --bind='ctrl-y:execute(git gtr copy {2})' \ + --bind='ctrl-r:reload(git gtr list --porcelain)') + or return 0 + test -z "$_gtr_selection"; and return 0 + set dir (string split \t -- "$_gtr_selection")[1] + else if test (count $argv) -eq 1 + echo "Usage: __FUNC__ cd " >&2 + echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2 + return 1 + else + set dir (command git gtr go $argv[2..]) + or return $status + end + cd $dir and begin set -l _gtr_hooks set -l _gtr_seen diff --git a/tests/init.bats b/tests/init.bats index 25f8b5f..2c5f844 100644 --- a/tests/init.bats +++ b/tests/init.bats @@ -158,6 +158,53 @@ setup() { [ "$status" -eq 1 ] } +# ── fzf interactive picker ─────────────────────────────────────────────────── + +@test "bash output includes fzf picker for cd with no args" { + run cmd_init bash + [ "$status" -eq 0 ] + [[ "$output" == *"command -v fzf"* ]] + [[ "$output" == *"--prompt='Worktree> '"* ]] + [[ "$output" == *"--with-nth=2"* ]] + [[ "$output" == *"ctrl-e:execute"* ]] +} + +@test "zsh output includes fzf picker for cd with no args" { + run cmd_init zsh + [ "$status" -eq 0 ] + [[ "$output" == *"command -v fzf"* ]] + [[ "$output" == *"--prompt='Worktree> '"* ]] + [[ "$output" == *"--with-nth=2"* ]] + [[ "$output" == *"ctrl-e:execute"* ]] +} + +@test "fish output includes fzf picker for cd with no args" { + run cmd_init fish + [ "$status" -eq 0 ] + [[ "$output" == *"type -q fzf"* ]] + [[ "$output" == *"--prompt='Worktree> '"* ]] + [[ "$output" == *"--with-nth=2"* ]] + [[ "$output" == *"ctrl-e:execute"* ]] +} + +@test "bash output shows fzf install hint when no args and no fzf" { + run cmd_init bash + [ "$status" -eq 0 ] + [[ "$output" == *'Install fzf for an interactive picker'* ]] +} + +@test "fish output shows fzf install hint when no args and no fzf" { + run cmd_init fish + [ "$status" -eq 0 ] + [[ "$output" == *'Install fzf for an interactive picker'* ]] +} + +@test "--as replaces function name in fzf fallback message" { + run cmd_init bash --as gwtr + [ "$status" -eq 0 ] + [[ "$output" == *'Usage: gwtr cd '* ]] +} + # ── git gtr passthrough preserved ──────────────────────────────────────────── @test "bash output passes non-cd commands to git gtr" {