Skip to content

Instantly share code, notes, and snippets.

@hannesoid
Last active September 5, 2025 09:17
Show Gist options
  • Save hannesoid/8a624f61a847e8ec6237ec92de44e786 to your computer and use it in GitHub Desktop.
Save hannesoid/8a624f61a847e8ec6237ec92de44e786 to your computer and use it in GitHub Desktop.
gws: interactive git worktree switcher for zsh
# Install
# - Add this function to ~/.zprofile (login shells) or ~/.zshrc (interactive shells).
# - Append directly and reload:
# curl -fsSL https://gist.github.com/hannesoid/8a624f61a847e8ec6237ec92de44e786/raw/gws.zsh >> ~/.zprofile && source ~/.zprofile
# - Or save as ~/bin/gws.zsh and source it from your shell init.
#
# gws — Git Worktree Switcher (zsh)
#
# Features
# - No argument: lists all worktrees and lets you pick one.
# - Uses `fzf` if available; falls back to a numbered select menu.
# - With an argument: case-insensitive substring search across worktree dir
# names and branch names; deduplicates to unique worktree paths.
# - If exactly one unique worktree matches, switches to it.
# - If multiple unique worktrees match, shows the same interactive picker.
# - Exact key match (dir name or branch) still jumps directly.
#
# Usage
# gws # interactive list of worktrees
# gws wipeout # case-insensitive substring match (deduped by path)
# gws my-branch # exact match on branch or worktree dir name
#
# Notes
# - Must be run inside a git repo (uses `git worktree list --porcelain`).
# - Optional: install `fzf` to get a better picker; otherwise zsh `select` is used.
gws() {
local target="$1"
local repo_root
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "Not in a git repo"; return 1; }
# Build maps and lists from `git worktree list --porcelain`
local -A key_to_path # key (worktree dir name or branch) -> path
local -A path_seen # path -> seen flag
local -a lines paths labels
lines=("${(@f)$(git worktree list --porcelain)}")
local i=1 path branch name
while (( i <= ${#lines} )); do
if [[ ${lines[i]} == worktree\ * ]]; then
path=${lines[i]#worktree }
((i++))
branch=""
while (( i <= ${#lines} )) && [[ ${lines[i]} != worktree\ * ]]; do
if [[ ${lines[i]} == branch\ * ]]; then
branch=${lines[i]#branch refs/heads/}
fi
((i++))
done
name=${path:t}
key_to_path[$name]="$path"
[[ -n $branch ]] && key_to_path[$branch]="$path"
if [[ -z ${path_seen[$path]} ]]; then
path_seen[$path]=1
paths+="$path"
if [[ -n $branch ]]; then
labels+="$name [$branch]"
else
labels+="$name"
fi
fi
continue
fi
((i++))
done
# helper: interactive selection (fzf if available, else zsh select)
local _select_path
_select_path() {
local -a _labels=("$@")
local choice
if command -v fzf >/dev/null 2>&1; then
choice=$(printf '%s\n' "${_labels[@]}" | fzf --prompt='gws> ' --height=40% --reverse)
[[ -z $choice ]] && return 1
local idx=1 l
for l in "${_labels[@]}"; do
[[ $l == $choice ]] && { REPLY=$idx; return 0; }
((idx++))
done
return 1
else
echo "Select worktree:"
select choice in "${_labels[@]}"; do
if [[ -n $REPLY && $REPLY -ge 1 && $REPLY -le ${#_labels} ]]; then
return 0
fi
echo "Invalid selection."
done
fi
}
# No argument: list and pick interactively
if [[ -z "$target" ]]; then
if (( ${#paths} == 0 )); then
echo "No worktrees found"
return 1
fi
_select_path "${labels[@]}" || return 1
cd -- "${paths[$REPLY]}"
return
fi
# Exact key match (dir name or branch)
if [[ -n ${key_to_path[$target]} ]]; then
cd -- "${key_to_path[$target]}"
return
fi
# Fuzzy/partial match (case-insensitive): dedupe by path so branch+dirname pointing
# to the same worktree count as one unique choice
local -A seen
local -a matched_paths matched_labels
local k p idx
local _t=${target:l}
for k in ${(k)key_to_path}; do
if [[ ${k:l} == *$_t* ]]; then
path=${key_to_path[$k]}
if [[ -z ${seen[$path]} ]]; then
seen[$path]=1
matched_paths+="$path"
idx=1
for p in "${paths[@]}"; do
if [[ $p == $path ]]; then
matched_labels+="${labels[$idx]}"
break
fi
((idx++))
done
fi
fi
done
local n=${#matched_paths}
if (( n == 0 )); then
echo "No worktree or branch matching '$target'"
return 1
fi
if (( n == 1 )); then
cd -- "${matched_paths[1]}"
return
fi
_select_path "${matched_labels[@]}" || return 2
cd -- "${matched_paths[$REPLY]}"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment