Last active
January 6, 2026 21:06
-
-
Save ephraimduncan/4c11d2bb85c4081450fbe2747cdd30a3 to your computer and use it in GitHub Desktop.
worktree-manager.bash
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Multi-project worktree manager - Bash version | |
| # | |
| # ASSUMPTIONS & SETUP: | |
| # - Works from any git repository in the filesystem | |
| # - Worktrees will be created in: <project-parent-dir>/.worktrees/<project-name>/<branch> | |
| # - New branches will be named: <your-username>/<feature-name> | |
| # | |
| # DIRECTORY STRUCTURE EXAMPLE: | |
| # /path/to/projects/ | |
| # ├── my-app/ (main git repo) | |
| # ├── another-project/ (main git repo) | |
| # └── .worktrees/ | |
| # ├── my-app/ | |
| # │ ├── my-app-feature-x/ (worktree folder includes project name) | |
| # │ └── my-app-bugfix-y/ (worktree) | |
| # └── another-project/ | |
| # └── another-project-new-feature/ (worktree) | |
| # | |
| # INSTALLATION: | |
| # 1. Source this file in your .bashrc: | |
| # source /path/to/worktree-manager.bash | |
| # | |
| # 2. Add bash completion (see bottom of file for setup instructions) | |
| # | |
| # 3. Restart your terminal or run: source ~/.bashrc | |
| # | |
| # USAGE: | |
| # wt <worktree> # cd to worktree (creates if needed) - uses current repo | |
| # wt <worktree> <command> # run command in worktree | |
| # wt --list # list all worktrees for current repo | |
| # wt --list-all # list all worktrees for all sibling repos | |
| # wt --rm <worktree> [worktree...] # remove worktree(s) | |
| # wt --project <project> <worktree> # operate on a specific project | |
| # | |
| # EXAMPLES: | |
| # wt feature-x # cd to feature-x worktree | |
| # wt feature-x code . # open VS Code in worktree | |
| # wt feature-x git status # git status in worktree | |
| # wt --project myapp feature-x # work with myapp's feature-x worktree | |
| # | |
| # POST-CREATE HOOKS: | |
| # You can run commands automatically after worktree creation by adding a .wtrc | |
| # file in your project root: | |
| # | |
| # # .wtrc - Worktree manager configuration | |
| # [post-create] | |
| # npm install | |
| # npm run prepare | |
| # | |
| # Commands run in the new worktree directory. Failures print a warning but | |
| # don't block worktree creation. | |
| # Find git root directory | |
| _find_git_root() { | |
| local dir="$1" | |
| [[ -z "$dir" ]] && dir="$PWD" | |
| while [[ "$dir" != "/" ]]; do | |
| if [[ -d "$dir/.git" ]]; then | |
| echo "$dir" | |
| return 0 | |
| fi | |
| dir="$(dirname "$dir")" | |
| done | |
| return 1 | |
| } | |
| # Run post-create hooks from .wtrc config file | |
| _run_post_create_hooks() { | |
| local git_root="$1" | |
| local wt_path="$2" | |
| local config_file="$git_root/.wtrc" | |
| # Skip if no config file | |
| [[ ! -f "$config_file" ]] && return 0 | |
| local in_post_create=false | |
| local trimmed | |
| while IFS= read -r line || [[ -n "$line" ]]; do | |
| # Trim whitespace from line | |
| trimmed="${line#"${line%%[![:space:]]*}"}" | |
| trimmed="${trimmed%"${trimmed##*[![:space:]]}"}" | |
| # Skip empty lines and comments | |
| [[ -z "$trimmed" || "$trimmed" =~ ^# ]] && continue | |
| # Check for section headers | |
| if [[ "$trimmed" =~ ^\[.*\]$ ]]; then | |
| [[ "$trimmed" == "[post-create]" ]] && in_post_create=true || in_post_create=false | |
| continue | |
| fi | |
| # Run commands in post-create section | |
| if $in_post_create; then | |
| echo "Running post-create hook: $trimmed" | |
| (cd "$wt_path" && eval "$trimmed") || { | |
| echo "Warning: post-create hook failed: $trimmed" | |
| } | |
| fi | |
| done < "$config_file" | |
| } | |
| # Multi-project worktree manager | |
| wt() { | |
| # Handle special flags first | |
| if [[ "$1" == "--help" || "$1" == "-h" ]]; then | |
| cat <<'EOF' | |
| wt - Git worktree manager | |
| wt <name> cd to worktree (creates if needed) | |
| wt <name> <cmd> run command in worktree | |
| wt --list list worktrees | |
| wt --rm <name>... remove worktree(s) | |
| wt --project <p> <n> use a different project | |
| EOF | |
| return 0 | |
| elif [[ "$1" == "--list" ]]; then | |
| local git_root | |
| git_root=$(_find_git_root) | |
| if [[ $? -ne 0 ]]; then | |
| echo "Error: Not in a git repository" | |
| return 1 | |
| fi | |
| local project_name | |
| project_name=$(basename "$git_root") | |
| local parent_dir | |
| parent_dir=$(dirname "$git_root") | |
| local worktrees_dir="$parent_dir/.worktrees/$project_name" | |
| echo "=== Worktrees for $project_name ===" | |
| if [[ -d "$worktrees_dir" ]]; then | |
| for wt in "$worktrees_dir"/*; do | |
| if [[ -d "$wt" ]]; then | |
| local wt_name | |
| wt_name=$(basename "$wt") | |
| wt_name="${wt_name#"$project_name-"}" | |
| echo " • $wt_name" | |
| fi | |
| done | |
| else | |
| echo " No worktrees found" | |
| fi | |
| return 0 | |
| elif [[ "$1" == "--list-all" ]]; then | |
| local git_root | |
| git_root=$(_find_git_root) | |
| if [[ $? -ne 0 ]]; then | |
| echo "Error: Not in a git repository" | |
| return 1 | |
| fi | |
| local parent_dir | |
| parent_dir=$(dirname "$git_root") | |
| local worktrees_base="$parent_dir/.worktrees" | |
| if [[ ! -d "$worktrees_base" ]]; then | |
| echo "No worktrees directory found" | |
| return 0 | |
| fi | |
| echo "=== All Worktrees ===" | |
| for project in "$worktrees_base"/*; do | |
| if [[ -d "$project" ]]; then | |
| local project_name | |
| project_name=$(basename "$project") | |
| echo "" | |
| echo "[$project_name]" | |
| for wt in "$project"/*; do | |
| if [[ -d "$wt" ]]; then | |
| local wt_name | |
| wt_name=$(basename "$wt") | |
| wt_name="${wt_name#"$project_name-"}" | |
| echo " • $wt_name" | |
| fi | |
| done | |
| fi | |
| done | |
| return 0 | |
| elif [[ "$1" == "--rm" ]]; then | |
| shift | |
| if [[ $# -eq 0 ]]; then | |
| echo "Usage: wt --rm <worktree> [worktree...]" | |
| return 1 | |
| fi | |
| local git_root | |
| git_root=$(_find_git_root) | |
| if [[ $? -ne 0 ]]; then | |
| echo "Error: Not in a git repository" | |
| return 1 | |
| fi | |
| local project_name | |
| project_name=$(basename "$git_root") | |
| local parent_dir | |
| parent_dir=$(dirname "$git_root") | |
| local failed=0 | |
| for worktree in "$@"; do | |
| local wt_path="$parent_dir/.worktrees/$project_name/$project_name-$worktree" | |
| if [[ ! -d "$wt_path" ]]; then | |
| echo "Worktree not found: $worktree" | |
| ((failed++)) | |
| continue | |
| fi | |
| echo "Removing: $worktree" | |
| (cd "$git_root" && git worktree remove "$wt_path") || ((failed++)) | |
| done | |
| return $((failed > 0 ? 1 : 0)) | |
| elif [[ "$1" == "--project" ]]; then | |
| shift | |
| local project="$1" | |
| local worktree="$2" | |
| shift 2 | |
| local command=("$@") | |
| if [[ -z "$project" || -z "$worktree" ]]; then | |
| echo "Usage: wt --project <project> <worktree> [command...]" | |
| return 1 | |
| fi | |
| # Find the project directory | |
| local git_root | |
| git_root=$(_find_git_root) | |
| if [[ $? -ne 0 ]]; then | |
| echo "Error: Not in a git repository" | |
| return 1 | |
| fi | |
| local parent_dir | |
| parent_dir=$(dirname "$git_root") | |
| local project_dir="$parent_dir/$project" | |
| if [[ ! -d "$project_dir" ]] || [[ ! -d "$project_dir/.git" ]]; then | |
| echo "Project not found: $project_dir" | |
| return 1 | |
| fi | |
| # Handle worktree operations for the specified project | |
| local worktrees_dir="$parent_dir/.worktrees/$project" | |
| local wt_path="$worktrees_dir/$project-$worktree" | |
| # Create worktree if it doesn't exist | |
| if [[ ! -d "$wt_path" ]]; then | |
| echo "Creating new worktree: $worktree for project $project" | |
| # Ensure worktrees directory exists | |
| mkdir -p "$worktrees_dir" | |
| # Determine branch name (use current username prefix if available) | |
| local branch_name | |
| if [[ -n "$USER" ]]; then | |
| branch_name="$USER/$worktree" | |
| else | |
| branch_name="$worktree" | |
| fi | |
| # Create the worktree | |
| (cd "$project_dir" && git worktree add "$wt_path" -b "$branch_name") || { | |
| echo "Failed to create worktree" | |
| return 1 | |
| } | |
| # Run post-create hooks | |
| _run_post_create_hooks "$project_dir" "$wt_path" | |
| fi | |
| # Execute command or cd to worktree | |
| if [[ ${#command[@]} -eq 0 ]]; then | |
| cd "$wt_path" | |
| else | |
| local old_pwd="$PWD" | |
| cd "$wt_path" | |
| "${command[@]}" | |
| local exit_code=$? | |
| cd "$old_pwd" | |
| return $exit_code | |
| fi | |
| return 0 | |
| fi | |
| # Normal usage: w <worktree> [command...] | |
| local worktree="$1" | |
| shift | |
| local command=("$@") | |
| if [[ -z "$worktree" ]]; then | |
| cat <<EOF | |
| Usage: | |
| wt <worktree> [command...] # Work with worktree in current repo | |
| wt --list # List worktrees for current repo | |
| wt --list-all # List all worktrees for sibling repos | |
| wt --rm <worktree> [worktree...] # Remove worktree(s) | |
| wt --project <project> <worktree> # Work with specific project's worktree | |
| EOF | |
| return 1 | |
| fi | |
| # Find git root of current directory | |
| local git_root | |
| git_root=$(_find_git_root) | |
| if [[ $? -ne 0 ]]; then | |
| echo "Error: Not in a git repository" | |
| return 1 | |
| fi | |
| local project_name | |
| project_name=$(basename "$git_root") | |
| local parent_dir | |
| parent_dir=$(dirname "$git_root") | |
| local worktrees_dir="$parent_dir/.worktrees/$project_name" | |
| local wt_path="$worktrees_dir/$project_name-$worktree" | |
| # If worktree doesn't exist, create it | |
| if [[ ! -d "$wt_path" ]]; then | |
| echo "Creating new worktree: $worktree" | |
| # Ensure worktrees directory exists | |
| mkdir -p "$worktrees_dir" | |
| # Determine branch name (use current username prefix if available) | |
| local branch_name | |
| if [[ -n "$USER" ]]; then | |
| branch_name="$USER/$worktree" | |
| else | |
| branch_name="$worktree" | |
| fi | |
| # Create the worktree | |
| (cd "$git_root" && git worktree add "$wt_path" -b "$branch_name") || { | |
| echo "Failed to create worktree" | |
| return 1 | |
| } | |
| # Run post-create hooks | |
| _run_post_create_hooks "$git_root" "$wt_path" | |
| 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" | |
| "${command[@]}" | |
| local exit_code=$? | |
| cd "$old_pwd" | |
| return $exit_code | |
| fi | |
| } | |
| # Bash completion for the wt function | |
| _wt_completion() { | |
| local cur prev opts | |
| COMPREPLY=() | |
| cur="${COMP_WORDS[COMP_CWORD]}" | |
| prev="${COMP_WORDS[COMP_CWORD-1]}" | |
| # First argument - could be a flag or worktree name | |
| if [[ ${COMP_CWORD} -eq 1 ]]; then | |
| opts="--help -h --list --list-all --rm --project" | |
| # Also add existing worktrees for current project | |
| local git_root | |
| git_root=$(_find_git_root 2>/dev/null) | |
| if [[ $? -eq 0 ]]; then | |
| local project_name | |
| project_name=$(basename "$git_root") | |
| local parent_dir | |
| parent_dir=$(dirname "$git_root") | |
| local worktrees_dir="$parent_dir/.worktrees/$project_name" | |
| if [[ -d "$worktrees_dir" ]]; then | |
| for wt in "$worktrees_dir"/*; do | |
| if [[ -d "$wt" ]]; then | |
| local wt_name | |
| wt_name=$(basename "$wt") | |
| wt_name="${wt_name#"$project_name-"}" | |
| opts="$opts $wt_name" | |
| fi | |
| done | |
| fi | |
| fi | |
| COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) | |
| return 0 | |
| fi | |
| # Handle flag-specific completions | |
| case "${COMP_WORDS[1]}" in | |
| --rm) | |
| # Complete with existing worktrees (allow multiple) | |
| local git_root | |
| git_root=$(_find_git_root 2>/dev/null) | |
| if [[ $? -eq 0 ]]; then | |
| local project_name | |
| project_name=$(basename "$git_root") | |
| local parent_dir | |
| parent_dir=$(dirname "$git_root") | |
| local worktrees_dir="$parent_dir/.worktrees/$project_name" | |
| if [[ -d "$worktrees_dir" ]]; then | |
| local worktrees="" | |
| for wt in "$worktrees_dir"/*; do | |
| if [[ -d "$wt" ]]; then | |
| local wt_name | |
| wt_name=$(basename "$wt") | |
| wt_name="${wt_name#"$project_name-"}" | |
| worktrees="$worktrees $wt_name" | |
| fi | |
| done | |
| COMPREPLY=( $(compgen -W "${worktrees}" -- ${cur}) ) | |
| fi | |
| fi | |
| ;; | |
| --project) | |
| if [[ ${COMP_CWORD} -eq 2 ]]; then | |
| # Complete with sibling projects | |
| local git_root | |
| git_root=$(_find_git_root 2>/dev/null) | |
| if [[ $? -eq 0 ]]; then | |
| local parent_dir | |
| parent_dir=$(dirname "$git_root") | |
| local projects="" | |
| for proj in "$parent_dir"/*; do | |
| if [[ -d "$proj/.git" ]]; then | |
| projects="$projects $(basename "$proj")" | |
| fi | |
| done | |
| COMPREPLY=( $(compgen -W "${projects}" -- ${cur}) ) | |
| fi | |
| elif [[ ${COMP_CWORD} -eq 3 ]]; then | |
| # Complete with worktrees for the specified project | |
| local project="${COMP_WORDS[2]}" | |
| local git_root | |
| git_root=$(_find_git_root 2>/dev/null) | |
| if [[ $? -eq 0 ]]; then | |
| local parent_dir | |
| parent_dir=$(dirname "$git_root") | |
| local worktrees_dir="$parent_dir/.worktrees/$project" | |
| if [[ -d "$worktrees_dir" ]]; then | |
| local worktrees="" | |
| for wt in "$worktrees_dir"/*; do | |
| if [[ -d "$wt" ]]; then | |
| local wt_name | |
| wt_name=$(basename "$wt") | |
| wt_name="${wt_name#"$project-"}" | |
| worktrees="$worktrees $wt_name" | |
| fi | |
| done | |
| COMPREPLY=( $(compgen -W "${worktrees}" -- ${cur}) ) | |
| fi | |
| fi | |
| fi | |
| ;; | |
| *) | |
| # For worktree names followed by commands, use default command completion | |
| if [[ ${COMP_CWORD} -eq 2 ]]; then | |
| COMPREPLY=( $(compgen -c -- ${cur}) ) | |
| fi | |
| ;; | |
| esac | |
| } | |
| # Register bash completion | |
| complete -F _wt_completion wt | |
| # Installation help | |
| if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then | |
| cat <<EOF | |
| Worktree Manager - Bash Version | |
| To install, add this line to your ~/.bashrc: | |
| source $(realpath "${BASH_SOURCE[0]}") | |
| Then restart your terminal or run: | |
| source ~/.bashrc | |
| The 'wt' command will then be available with tab completion. | |
| Usage examples: | |
| wt feature-x # Create/switch to feature-x worktree | |
| wt feature-x git status # Run git status in worktree | |
| wt --list # List current project's worktrees | |
| wt --list-all # List all sibling projects' worktrees | |
| wt --rm feature-x # Remove feature-x worktree | |
| wt --project myapp feature-y # Work with myapp's feature-y worktree | |
| EOF | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment