From 552972918b1c893788a8e69fef6ae982b0545669 Mon Sep 17 00:00:00 2001 From: karan925 Date: Tue, 24 Feb 2026 01:14:07 -0800 Subject: [PATCH 1/4] feat(init): add interactive fzf worktree picker for `gtr cd` When `gtr cd` is called with no arguments and fzf is installed, an interactive picker launches showing all worktrees with a git log + status preview pane. Gracefully falls back to printing the worktree list with a hint when fzf is not available. Supported in bash, zsh, and fish shells. Co-Authored-By: Claude Opus 4.6 --- README.md | 2 ++ lib/commands/doctor.sh | 7 ++++++ lib/commands/help.sh | 3 +++ lib/commands/init.sh | 54 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f5ce8b..ae6b07e 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,6 +218,7 @@ 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 ``` diff --git a/lib/commands/doctor.sh b/lib/commands/doctor.sh index 98aa624..d44dfee 100644 --- a/lib/commands/doctor.sh +++ b/lib/commands/doctor.sh @@ -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..5f79229 100644 --- a/lib/commands/help.sh +++ b/lib/commands/help.sh @@ -365,6 +365,7 @@ Setup: eval "$(git gtr init zsh --as gwtr)" After setup: + gtr cd # interactive worktree picker (requires fzf) gtr cd my-feature # cd to worktree gtr cd 1 # cd to main repo gtr # same as git gtr @@ -549,6 +550,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 installed, 'gtr cd' (no args) opens an interactive picker version Show version @@ -572,6 +574,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..c90de3b 100644 --- a/lib/commands/init.sh +++ b/lib/commands/init.sh @@ -81,6 +81,23 @@ _init_bash() { __FUNC__() { if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then shift + if [ "$#" -eq 0 ]; then + if command -v fzf >/dev/null 2>&1; then + local _gtr_sel + _gtr_sel="$(command git gtr list --porcelain | fzf \ + --delimiter=$'\t' \ + --with-nth=2 \ + --header='Select worktree (Ctrl-C to cancel)' \ + --preview='git -C {1} log --oneline --graph -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \ + --preview-window=right:50%)" || return 0 + set -- "$(printf '%s' "$_gtr_sel" | cut -f2)" + else + echo "Tip: Install fzf for an interactive worktree picker (https://github.com/junegunn/fzf)" >&2 + echo "" >&2 + command git gtr list + return 0 + fi + fi local dir dir="$(command git gtr go "$@")" && cd "$dir" && { local _gtr_hooks _gtr_hook _gtr_seen _gtr_config_file @@ -154,6 +171,23 @@ _init_zsh() { __FUNC__() { if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then shift + if [ "$#" -eq 0 ]; then + if command -v fzf >/dev/null 2>&1; then + local _gtr_sel + _gtr_sel="$(command git gtr list --porcelain | fzf \ + --delimiter=$'\t' \ + --with-nth=2 \ + --header='Select worktree (Ctrl-C to cancel)' \ + --preview='git -C {1} log --oneline --graph -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \ + --preview-window=right:50%)" || return 0 + set -- "$(printf '%s' "$_gtr_sel" | cut -f2)" + else + echo "Tip: Install fzf for an interactive worktree picker (https://github.com/junegunn/fzf)" >&2 + echo "" >&2 + command git gtr list + return 0 + fi + fi local dir dir="$(command git gtr go "$@")" && cd "$dir" && { local _gtr_hooks _gtr_hook _gtr_seen _gtr_config_file @@ -232,7 +266,25 @@ _init_fish() { function __FUNC__ if test (count $argv) -gt 0; and test "$argv[1]" = "cd" - set -l dir (command git gtr go $argv[2..]) + set -l _gtr_cd_args $argv[2..] + if test (count $_gtr_cd_args) -eq 0 + if command -q fzf + set -l _gtr_sel (command git gtr list --porcelain | fzf \ + --delimiter=\t \ + --with-nth=2 \ + --header='Select worktree (Ctrl-C to cancel)' \ + --preview='git -C {1} log --oneline --graph -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \ + --preview-window=right:50%) + or return 0 + set _gtr_cd_args (printf '%s' "$_gtr_sel" | cut -f2) + else + echo "Tip: Install fzf for an interactive worktree picker (https://github.com/junegunn/fzf)" >&2 + echo "" >&2 + command git gtr list + return 0 + end + end + set -l dir (command git gtr go $_gtr_cd_args) and cd $dir and begin set -l _gtr_hooks From 40005e959c0d06a3cd47094ec77c1d74d9cbaed5 Mon Sep 17 00:00:00 2001 From: karan925 Date: Tue, 24 Feb 2026 08:29:24 -0800 Subject: [PATCH 2/4] add opton to open in editor plus more --- README.md | 3 ++ lib/commands/help.sh | 13 ++++- lib/commands/init.sh | 125 ++++++++++++++++++++++++------------------- 3 files changed, 84 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index ae6b07e..1ffc52f 100644 --- a/README.md +++ b/README.md @@ -221,8 +221,11 @@ eval "$(git gtr init bash)" gtr cd # Interactive worktree picker (requires fzf) gtr cd my-feature gtr cd 1 +gtr cd # interactive picker (requires fzf) ``` +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/help.sh b/lib/commands/help.sh index 5f79229..e3aef18 100644 --- a/lib/commands/help.sh +++ b/lib/commands/help.sh @@ -365,10 +365,19 @@ Setup: eval "$(git gtr init zsh --as gwtr)" After setup: - gtr cd # interactive worktree picker (requires fzf) 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 } @@ -550,7 +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 installed, 'gtr cd' (no args) opens an interactive picker + With fzf: 'gtr cd' opens a command palette (preview, editor, AI, delete) version Show version diff --git a/lib/commands/init.sh b/lib/commands/init.sh index c90de3b..850e92e 100644 --- a/lib/commands/init.sh +++ b/lib/commands/init.sh @@ -81,25 +81,30 @@ _init_bash() { __FUNC__() { if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then shift - if [ "$#" -eq 0 ]; then - if command -v fzf >/dev/null 2>&1; then - local _gtr_sel - _gtr_sel="$(command git gtr list --porcelain | fzf \ - --delimiter=$'\t' \ - --with-nth=2 \ - --header='Select worktree (Ctrl-C to cancel)' \ - --preview='git -C {1} log --oneline --graph -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \ - --preview-window=right:50%)" || return 0 - set -- "$(printf '%s' "$_gtr_sel" | cut -f2)" - else - echo "Tip: Install fzf for an interactive worktree picker (https://github.com/junegunn/fzf)" >&2 - echo "" >&2 - command git gtr list - return 0 - fi - fi 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)" + else + dir="$(command git gtr go "$@")" || return $? + fi + cd "$dir" && { local _gtr_hooks _gtr_hook _gtr_seen _gtr_config_file _gtr_hooks="" _gtr_seen="" @@ -171,25 +176,30 @@ _init_zsh() { __FUNC__() { if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then shift - if [ "$#" -eq 0 ]; then - if command -v fzf >/dev/null 2>&1; then - local _gtr_sel - _gtr_sel="$(command git gtr list --porcelain | fzf \ - --delimiter=$'\t' \ - --with-nth=2 \ - --header='Select worktree (Ctrl-C to cancel)' \ - --preview='git -C {1} log --oneline --graph -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \ - --preview-window=right:50%)" || return 0 - set -- "$(printf '%s' "$_gtr_sel" | cut -f2)" - else - echo "Tip: Install fzf for an interactive worktree picker (https://github.com/junegunn/fzf)" >&2 - echo "" >&2 - command git gtr list - return 0 - fi - fi 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)" + else + dir="$(command git gtr go "$@")" || return $? + fi + cd "$dir" && { local _gtr_hooks _gtr_hook _gtr_seen _gtr_config_file _gtr_hooks="" _gtr_seen="" @@ -266,26 +276,31 @@ _init_fish() { function __FUNC__ if test (count $argv) -gt 0; and test "$argv[1]" = "cd" - set -l _gtr_cd_args $argv[2..] - if test (count $_gtr_cd_args) -eq 0 - if command -q fzf - set -l _gtr_sel (command git gtr list --porcelain | fzf \ - --delimiter=\t \ - --with-nth=2 \ - --header='Select worktree (Ctrl-C to cancel)' \ - --preview='git -C {1} log --oneline --graph -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \ - --preview-window=right:50%) - or return 0 - set _gtr_cd_args (printf '%s' "$_gtr_sel" | cut -f2) - else - echo "Tip: Install fzf for an interactive worktree picker (https://github.com/junegunn/fzf)" >&2 - echo "" >&2 - command git gtr list - return 0 - end + 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 + set dir (command git gtr go $argv[2..]) + or return $status end - set -l dir (command git gtr go $_gtr_cd_args) - and cd $dir + cd $dir and begin set -l _gtr_hooks set -l _gtr_seen From 0c6102e726cd81a7b09f6f1a0a71c46efd89ba49 Mon Sep 17 00:00:00 2001 From: Tom Elizaga Date: Tue, 24 Feb 2026 09:03:00 -0800 Subject: [PATCH 3/4] fix(init): add emulate -L zsh to prevent locale errors with hyphenated branch names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #134 — zsh interprets hyphens in branch names (e.g. fix-cron) as character ranges under non-C locales, causing "range-endpoints in reverse collating sequence order" errors. `emulate -L zsh` resets all zsh options to defaults within the function scope. Also adds fzf install hint when `gtr cd` is called with no args and fzf is not available, and deduplicates a README line. --- README.md | 1 - lib/commands/init.sh | 13 ++++++++++++ tests/init.bats | 47 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ffc52f..22cae0a 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,6 @@ eval "$(git gtr init bash)" gtr cd # Interactive worktree picker (requires fzf) gtr cd my-feature gtr cd 1 -gtr cd # interactive picker (requires fzf) ``` 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. diff --git a/lib/commands/init.sh b/lib/commands/init.sh index 850e92e..d29f02f 100644 --- a/lib/commands/init.sh +++ b/lib/commands/init.sh @@ -101,6 +101,10 @@ __FUNC__() { --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 @@ -174,6 +178,7 @@ _init_zsh() { # eval "$(git gtr init zsh)" __FUNC__() { + emulate -L zsh if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then shift local dir @@ -196,6 +201,10 @@ __FUNC__() { --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 @@ -296,6 +305,10 @@ function __FUNC__ 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 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" { From b8ded84eca9f236c6fe61ed1e8668cbcab63b5d6 Mon Sep 17 00:00:00 2001 From: Tom Elizaga Date: Tue, 24 Feb 2026 09:09:55 -0800 Subject: [PATCH 4/4] fix(doctor): guard detect_provider against set -e early exit detect_provider returns non-zero in repos without remotes, causing set -e to abort cmd_doctor before reaching the fzf and summary checks. --- lib/commands/doctor.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/doctor.sh b/lib/commands/doctor.sh index d44dfee..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