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
72 changes: 55 additions & 17 deletions lib/commands/init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ __FUNC__() {
echo "No worktrees to pick from. Create one with: git gtr new <branch>" >&2
return 0
fi
local _gtr_selection
local _gtr_selection _gtr_key _gtr_line
_gtr_selection="$(printf '%s\n' "$_gtr_porcelain" | fzf \
--delimiter=$'\t' \
--with-nth=2 \
Expand All @@ -100,13 +100,23 @@ __FUNC__() {
--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})' \
--expect=ctrl-a,ctrl-e \
--bind='ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)' \
--bind='ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)' \
--bind='ctrl-r:reload(git gtr list --porcelain)')" || return 0
[ -z "$_gtr_selection" ] && return 0
dir="$(printf '%s' "$_gtr_selection" | cut -f1)"
_gtr_key="$(head -1 <<< "$_gtr_selection")"
_gtr_line="$(sed -n '2p' <<< "$_gtr_selection")"
[ -z "$_gtr_line" ] && return 0
# ctrl-a/ctrl-e: run after fzf exits (needs full terminal for TUI apps)
if [ "$_gtr_key" = "ctrl-a" ]; then
command git gtr ai "$(printf '%s' "$_gtr_line" | cut -f2)"
return $?
elif [ "$_gtr_key" = "ctrl-e" ]; then
command git gtr editor "$(printf '%s' "$_gtr_line" | cut -f2)"
return $?
fi
dir="$(printf '%s' "$_gtr_line" | cut -f1)"
elif [ "$#" -eq 0 ]; then
echo "Usage: __FUNC__ cd <branch>" >&2
echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2
Expand Down Expand Up @@ -195,7 +205,7 @@ __FUNC__() {
echo "No worktrees to pick from. Create one with: git gtr new <branch>" >&2
return 0
fi
local _gtr_selection
local _gtr_selection _gtr_key _gtr_line
_gtr_selection="$(printf '%s\n' "$_gtr_porcelain" | fzf \
--delimiter=$'\t' \
--with-nth=2 \
Expand All @@ -206,13 +216,23 @@ __FUNC__() {
--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})' \
--expect=ctrl-a,ctrl-e \
--bind='ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)' \
--bind='ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)' \
--bind='ctrl-r:reload(git gtr list --porcelain)')" || return 0
[ -z "$_gtr_selection" ] && return 0
dir="$(printf '%s' "$_gtr_selection" | cut -f1)"
_gtr_key="$(head -1 <<< "$_gtr_selection")"
_gtr_line="$(sed -n '2p' <<< "$_gtr_selection")"
[ -z "$_gtr_line" ] && return 0
# ctrl-a/ctrl-e: run after fzf exits (needs full terminal for TUI apps)
if [ "$_gtr_key" = "ctrl-a" ]; then
command git gtr ai "$(printf '%s' "$_gtr_line" | cut -f2)"
return $?
elif [ "$_gtr_key" = "ctrl-e" ]; then
command git gtr editor "$(printf '%s' "$_gtr_line" | cut -f2)"
return $?
fi
dir="$(printf '%s' "$_gtr_line" | cut -f1)"
elif [ "$#" -eq 0 ]; then
echo "Usage: __FUNC__ cd <branch>" >&2
echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2
Expand Down Expand Up @@ -314,14 +334,32 @@ function __FUNC__
--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})' \
--expect=ctrl-a,ctrl-e \
--bind='ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)' \
--bind='ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)' \
--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]
# --expect gives two lines: key (index 1) and selection (index 2)
# Fish collapses empty lines in command substitution, so when Enter
# is pressed the empty key line disappears and count drops to 1.
if test (count $_gtr_selection) -eq 1
set -l _gtr_key ""
set -l _gtr_line "$_gtr_selection[1]"
else
set -l _gtr_key "$_gtr_selection[1]"
set -l _gtr_line "$_gtr_selection[2]"
end
test -z "$_gtr_line"; and return 0
# ctrl-a/ctrl-e: run after fzf exits (needs full terminal for TUI apps)
if test "$_gtr_key" = "ctrl-a"
command git gtr ai (string split \t -- "$_gtr_line")[2]
return $status
else if test "$_gtr_key" = "ctrl-e"
command git gtr editor (string split \t -- "$_gtr_line")[2]
return $status
end
set dir (string split \t -- "$_gtr_line")[1]
else if test (count $argv) -eq 1
echo "Usage: __FUNC__ cd <branch>" >&2
echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2
Expand Down
215 changes: 209 additions & 6 deletions tests/init.bats
Original file line number Diff line number Diff line change
Expand Up @@ -160,39 +160,242 @@ setup() {

# ── fzf interactive picker ───────────────────────────────────────────────────

@test "bash output includes fzf picker for cd with no args" {
# ── fzf: general setup ──────────────────────────────────────────────────────

@test "bash output includes fzf detection 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" {
@test "zsh output includes fzf detection 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" {
@test "fish output includes fzf detection 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"* ]]
}

# ── fzf: header shows all keybindings ───────────────────────────────────────

@test "bash fzf header lists all keybindings" {
run cmd_init bash
[ "$status" -eq 0 ]
[[ "$output" == *"enter:cd"* ]]
[[ "$output" == *"ctrl-e:editor"* ]]
[[ "$output" == *"ctrl-a:ai"* ]]
[[ "$output" == *"ctrl-d:delete"* ]]
[[ "$output" == *"ctrl-y:copy"* ]]
[[ "$output" == *"ctrl-r:refresh"* ]]
}

@test "zsh fzf header lists all keybindings" {
run cmd_init zsh
[ "$status" -eq 0 ]
[[ "$output" == *"enter:cd"* ]]
[[ "$output" == *"ctrl-e:editor"* ]]
[[ "$output" == *"ctrl-a:ai"* ]]
[[ "$output" == *"ctrl-d:delete"* ]]
[[ "$output" == *"ctrl-y:copy"* ]]
[[ "$output" == *"ctrl-r:refresh"* ]]
}

@test "fish fzf header lists all keybindings" {
run cmd_init fish
[ "$status" -eq 0 ]
[[ "$output" == *"enter:cd"* ]]
[[ "$output" == *"ctrl-e:editor"* ]]
[[ "$output" == *"ctrl-a:ai"* ]]
[[ "$output" == *"ctrl-d:delete"* ]]
[[ "$output" == *"ctrl-y:copy"* ]]
[[ "$output" == *"ctrl-r:refresh"* ]]
}

# ── fzf: enter (cd) ─────────────────────────────────────────────────────────

@test "bash fzf enter extracts path from selection field 1 and cd" {
run cmd_init bash
[ "$status" -eq 0 ]
# Selection is parsed with cut -f1 to get path, then cd
[[ "$output" == *'cut -f1'* ]]
[[ "$output" == *'cd "$dir"'* ]]
}

@test "zsh fzf enter extracts path from selection field 1 and cd" {
run cmd_init zsh
[ "$status" -eq 0 ]
[[ "$output" == *'cut -f1'* ]]
[[ "$output" == *'cd "$dir"'* ]]
}

@test "fish fzf enter extracts path from selection and cd" {
run cmd_init fish
[ "$status" -eq 0 ]
# Fish uses string split to extract path, then cd
[[ "$output" == *'string split'* ]]
[[ "$output" == *'set dir'* ]]
[[ "$output" == *'cd $dir'* ]]
}

# ── fzf: ctrl-e (editor) — via --expect ──────────────────────────────────────

@test "bash fzf ctrl-e handled via --expect for full terminal access" {
run cmd_init bash
[ "$status" -eq 0 ]
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
[[ "$output" == *'git gtr editor'* ]]
}

@test "zsh fzf ctrl-e handled via --expect for full terminal access" {
run cmd_init zsh
[ "$status" -eq 0 ]
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
[[ "$output" == *'git gtr editor'* ]]
}

@test "fish fzf ctrl-e handled via --expect for full terminal access" {
run cmd_init fish
[ "$status" -eq 0 ]
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
[[ "$output" == *'git gtr editor'* ]]
}

# ── fzf: ctrl-a (ai) — via --expect ─────────────────────────────────────────

@test "bash fzf ctrl-a runs git gtr ai after fzf exits" {
run cmd_init bash
[ "$status" -eq 0 ]
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
[[ "$output" == *'git gtr ai'* ]]
}

@test "zsh fzf ctrl-a runs git gtr ai after fzf exits" {
run cmd_init zsh
[ "$status" -eq 0 ]
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
[[ "$output" == *'git gtr ai'* ]]
}

@test "fish fzf ctrl-a runs git gtr ai after fzf exits" {
run cmd_init fish
[ "$status" -eq 0 ]
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
[[ "$output" == *'git gtr ai'* ]]
}

# ── fzf: ctrl-d (delete + reload) ───────────────────────────────────────────

@test "bash fzf ctrl-d runs git gtr rm and reloads list" {
run cmd_init bash
[ "$status" -eq 0 ]
[[ "$output" == *"ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)"* ]]
}

@test "zsh fzf ctrl-d runs git gtr rm and reloads list" {
run cmd_init zsh
[ "$status" -eq 0 ]
[[ "$output" == *"ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)"* ]]
}

@test "fish fzf ctrl-d runs git gtr rm and reloads list" {
run cmd_init fish
[ "$status" -eq 0 ]
[[ "$output" == *"ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)"* ]]
}

# ── fzf: ctrl-y (copy) ──────────────────────────────────────────────────────

@test "bash fzf ctrl-y runs git gtr copy on selected branch" {
run cmd_init bash
[ "$status" -eq 0 ]
[[ "$output" == *"ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)"* ]]
}

@test "zsh fzf ctrl-y runs git gtr copy on selected branch" {
run cmd_init zsh
[ "$status" -eq 0 ]
[[ "$output" == *"ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)"* ]]
}

@test "fish fzf ctrl-y runs git gtr copy on selected branch" {
run cmd_init fish
[ "$status" -eq 0 ]
[[ "$output" == *"ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)"* ]]
}

# ── fzf: ctrl-r (refresh) ───────────────────────────────────────────────────

@test "bash fzf ctrl-r reloads worktree list" {
run cmd_init bash
[ "$status" -eq 0 ]
[[ "$output" == *"ctrl-r:reload(git gtr list --porcelain)"* ]]
}

@test "zsh fzf ctrl-r reloads worktree list" {
run cmd_init zsh
[ "$status" -eq 0 ]
[[ "$output" == *"ctrl-r:reload(git gtr list --porcelain)"* ]]
}

@test "fish fzf ctrl-r reloads worktree list" {
run cmd_init fish
[ "$status" -eq 0 ]
[[ "$output" == *"ctrl-r:reload(git gtr list --porcelain)"* ]]
}

# ── fzf: preview window ─────────────────────────────────────────────────────

@test "bash fzf preview shows git log and status" {
run cmd_init bash
[ "$status" -eq 0 ]
[[ "$output" == *"--preview="* ]]
[[ "$output" == *"git -C {1} log --oneline --graph --color=always"* ]]
[[ "$output" == *"git -C {1} status --short"* ]]
[[ "$output" == *"--preview-window=right:50%"* ]]
}

@test "zsh fzf preview shows git log and status" {
run cmd_init zsh
[ "$status" -eq 0 ]
[[ "$output" == *"--preview="* ]]
[[ "$output" == *"git -C {1} log --oneline --graph --color=always"* ]]
[[ "$output" == *"git -C {1} status --short"* ]]
[[ "$output" == *"--preview-window=right:50%"* ]]
}

@test "fish fzf preview shows git log and status" {
run cmd_init fish
[ "$status" -eq 0 ]
[[ "$output" == *"--preview="* ]]
[[ "$output" == *"git -C {1} log --oneline --graph --color=always"* ]]
[[ "$output" == *"git -C {1} status --short"* ]]
[[ "$output" == *"--preview-window=right:50%"* ]]
}

# ── fzf: fallback messages ──────────────────────────────────────────────────

@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 "zsh output shows fzf install hint when no args and no fzf" {
run cmd_init zsh
[ "$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 ]
Expand Down