-
-
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 |
Adding
# Add custom completions directory to fpath
fpath=(~/.zsh/completions $fpath)
autoload -U compinit && compinit
seems to do the job!
Adding
# Add custom completions directory to fpath fpath=(~/.zsh/completions $fpath) autoload -U compinit && compinitseems 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
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.
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)"
completions/_w
Ah yep - that's in the updated version now 😄
I am using ohmyzsh and was having some issues getting auto completion working. I ended up creating a custom plugin. I updated the gist instructions for creating a custom plugin here: https://gist.github.com/dmbr0/f179d5e2f1e34c5d3a1ae385dec6a328
thank you for this! has been great.
I updated your gist with some improvements. Mostly to source instead of copy/paste in .zshrc
https://gist.github.com/mathd/5ccfe70edc70c129828fadb9f54c7f5e
I built an interative TUI here: https://github.com/ephraimduncan/worktree-manager
Ah this might be with me using some unusual completion set up. Locally I have:
Feel free to try that, I'll have a little debug and see if there's a neater way to do it