Skip to content

Instantly share code, notes, and snippets.

@rorydbain
Last active July 17, 2025 07:22
Show Gist options
  • Save rorydbain/e20e6ab0c7cc027fc1599bd2e430117d to your computer and use it in GitHub Desktop.
Save rorydbain/e20e6ab0c7cc027fc1599bd2e430117d to your computer and use it in GitHub Desktop.
#!/usr/bin/env zsh
# Multi-project worktree manager with Claude support
#
# ASSUMPTIONS & SETUP:
# - Your git projects live in: ~/projects/
# - Worktrees will be created in: ~/projects/worktrees/<project>/<branch>
# - New branches will be named: <your-username>/<feature-name>
#
# DIRECTORY STRUCTURE EXAMPLE:
# ~/projects/
# ├── my-app/ (main git repo)
# ├── another-project/ (main git repo)
# └── worktrees/
# ├── my-app/
# │ ├── feature-x/ (worktree)
# │ └── bugfix-y/ (worktree)
# └── another-project/
# └── new-feature/ (worktree)
#
# CUSTOMIZATION:
# To use different directories, modify these lines in the w() function:
# local projects_dir="$HOME/projects"
# local worktrees_dir="$HOME/projects/worktrees"
#
# INSTALLATION:
# 1. Add to your .zshrc (in this order):
# fpath=(~/.zsh/completions $fpath)
# autoload -U compinit && compinit
#
# 2. Copy this entire script to your .zshrc (after the lines above)
#
# 3. Restart your terminal or run: source ~/.zshrc
#
# 4. Test it works: w <TAB> should show your projects
#
# If tab completion doesn't work:
# - Make sure the fpath line comes BEFORE the w function in your .zshrc
# - Restart your terminal completely
#
# USAGE:
# w <project> <worktree> # cd to worktree (creates if needed)
# w <project> <worktree> <command> # run command in worktree
# w --list # list all worktrees
# w --rm <project> <worktree> # remove worktree
#
# EXAMPLES:
# w myapp feature-x # cd to feature-x worktree
# w myapp feature-x claude # run claude in worktree
# w myapp feature-x gst # git status in worktree
# w myapp feature-x gcmsg "fix: bug" # git commit in worktree
# Multi-project worktree manager
w() {
local projects_dir="$HOME/projects"
local worktrees_dir="$HOME/projects/worktrees"
# Handle special flags
if [[ "$1" == "--list" ]]; then
echo "=== All Worktrees ==="
# Check new location
if [[ -d "$worktrees_dir" ]]; then
for project in $worktrees_dir/*(/N); do
project_name=$(basename "$project")
echo "\n[$project_name]"
for wt in $project/*(/N); do
echo " • $(basename "$wt")"
done
done
fi
# Also check old core-wts location
if [[ -d "$projects_dir/core-wts" ]]; then
echo "\n[core] (legacy location)"
for wt in $projects_dir/core-wts/*(/N); do
echo " • $(basename "$wt")"
done
fi
return 0
elif [[ "$1" == "--rm" ]]; then
shift
local project="$1"
local worktree="$2"
if [[ -z "$project" || -z "$worktree" ]]; then
echo "Usage: w --rm <project> <worktree>"
return 1
fi
# Check both locations for core
if [[ "$project" == "core" && -d "$projects_dir/core-wts/$worktree" ]]; then
(cd "$projects_dir/$project" && git worktree remove "$projects_dir/core-wts/$worktree")
else
local wt_path="$worktrees_dir/$project/$worktree"
if [[ ! -d "$wt_path" ]]; then
echo "Worktree not found: $wt_path"
return 1
fi
(cd "$projects_dir/$project" && git worktree remove "$wt_path")
fi
return $?
fi
# Normal usage: w <project> <worktree> [command...]
local project="$1"
local worktree="$2"
shift 2
local command=("$@")
if [[ -z "$project" || -z "$worktree" ]]; then
echo "Usage: w <project> <worktree> [command...]"
echo " w --list"
echo " w --rm <project> <worktree>"
return 1
fi
# Check if project exists
if [[ ! -d "$projects_dir/$project" ]]; then
echo "Project not found: $projects_dir/$project"
return 1
fi
# Determine worktree path - check multiple locations
local wt_path=""
if [[ "$project" == "core" ]]; then
# For core, check old location first
if [[ -d "$projects_dir/core-wts/$worktree" ]]; then
wt_path="$projects_dir/core-wts/$worktree"
elif [[ -d "$worktrees_dir/$project/$worktree" ]]; then
wt_path="$worktrees_dir/$project/$worktree"
fi
else
# For other projects, check new location
if [[ -d "$worktrees_dir/$project/$worktree" ]]; then
wt_path="$worktrees_dir/$project/$worktree"
fi
fi
# If worktree doesn't exist, create it
if [[ -z "$wt_path" || ! -d "$wt_path" ]]; then
echo "Creating new worktree: $worktree"
# Ensure worktrees directory exists
mkdir -p "$worktrees_dir/$project"
# Determine branch name (use current username prefix)
local branch_name="$USER/$worktree"
# Create the worktree in new location
wt_path="$worktrees_dir/$project/$worktree"
(cd "$projects_dir/$project" && git worktree add "$wt_path" -b "$branch_name") || {
echo "Failed to create worktree"
return 1
}
fi
# Execute based on number of arguments
if [[ ${#command[@]} -eq 0 ]]; then
# No command specified - just cd to the worktree
cd "$wt_path"
else
# Command specified - run it in the worktree without cd'ing
local old_pwd="$PWD"
cd "$wt_path"
eval "${command[@]}"
local exit_code=$?
cd "$old_pwd"
return $exit_code
fi
}
# Setup completion if not already done
if [[ ! -f ~/.zsh/completions/_w ]]; then
mkdir -p ~/.zsh/completions
cat > ~/.zsh/completions/_w << 'EOF'
#compdef w
_w() {
local curcontext="$curcontext" state line
typeset -A opt_args
local projects_dir="$HOME/projects"
local worktrees_dir="$HOME/projects/worktrees"
# Define the main arguments
_arguments -C \
'(--rm)--list[List all worktrees]' \
'(--list)--rm[Remove a worktree]' \
'1: :->project' \
'2: :->worktree' \
'3: :->command' \
'*:: :->command_args' \
&& return 0
case $state in
project)
if [[ "${words[1]}" == "--list" ]]; then
# No completion needed for --list
return 0
fi
# Get list of projects (directories in ~/projects that are git repos)
local -a projects
for dir in $projects_dir/*(N/); do
if [[ -d "$dir/.git" ]]; then
projects+=(${dir:t})
fi
done
_describe -t projects 'project' projects && return 0
;;
worktree)
local project="${words[2]}"
if [[ -z "$project" ]]; then
return 0
fi
local -a worktrees
# For core project, check both old and new locations
if [[ "$project" == "core" ]]; then
# Check old location
if [[ -d "$projects_dir/core-wts" ]]; then
for wt in $projects_dir/core-wts/*(N/); do
worktrees+=(${wt:t})
done
fi
# Check new location
if [[ -d "$worktrees_dir/core" ]]; then
for wt in $worktrees_dir/core/*(N/); do
# Avoid duplicates
if [[ ! " ${worktrees[@]} " =~ " ${wt:t} " ]]; then
worktrees+=(${wt:t})
fi
done
fi
else
# For other projects, check new location only
if [[ -d "$worktrees_dir/$project" ]]; then
for wt in $worktrees_dir/$project/*(N/); do
worktrees+=(${wt:t})
done
fi
fi
if (( ${#worktrees} > 0 )); then
_describe -t worktrees 'existing worktree' worktrees
else
_message 'new worktree name'
fi
;;
command)
# Suggest common commands when user has typed project and worktree
local -a common_commands
common_commands=(
'claude:Start Claude Code session'
'gst:Git status'
'gaa:Git add all'
'gcmsg:Git commit with message'
'gp:Git push'
'gco:Git checkout'
'gd:Git diff'
'gl:Git log'
'npm:Run npm commands'
'yarn:Run yarn commands'
'make:Run make commands'
)
_describe -t commands 'command' common_commands
# Also complete regular commands
_command_names -e
;;
command_args)
# Let zsh handle completion for the specific command
words=(${words[4,-1]})
CURRENT=$((CURRENT - 3))
_normal
;;
esac
}
_w "$@"
EOF
# Add completions to fpath if not already there
fpath=(~/.zsh/completions $fpath)
fi
# Initialize completions
autoload -U compinit && compinit
@rorydbain
Copy link
Author

Hey @rorydbain - I tried this out, works great, except for the auto completion I can't seem to get working without an additional step. It appears I need to do the following to get it working:

source ~/.zsh/completions/_w

which outputs:

_arguments:comparguments:327: can only be called from completion function

Is there a missing step about installing autocompletion or an assumption about zsh completions which I'm missing?

Ah this might be with me using some unusual completion set up. Locally I have:

antigen bundle zsh-users/zsh-completions

...

# Add custom completions directory to fpath
fpath=(~/.zsh/completions $fpath)
autoload -U compinit && compinit

Feel free to try that, I'll have a little debug and see if there's a neater way to do it

@edhgoose
Copy link

Adding

# Add custom completions directory to fpath
fpath=(~/.zsh/completions $fpath)
autoload -U compinit && compinit

seems to do the job!

@rorydbain
Copy link
Author

Adding

# Add custom completions directory to fpath
fpath=(~/.zsh/completions $fpath)
autoload -U compinit && compinit

seems to do the job!

Perfect - thanks for the feedback on it! I pushed a little update to include that in the instructions at the top too

@edhgoose
Copy link

Looks like I spoke too soon... I'm now finding it's not working. I rebooted my computer last night, and it seems to have stopped.

Running source ~/.zsh/completions/_w seems to be important.

@kjrocker
Copy link

You may be able to remove the project argument, if you assume the project name is the name of the folder.

basename "$(git rev-parse --show-toplevel)"

@rorydbain
Copy link
Author

completions/_w

Ah yep - that's in the updated version now 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment