Skip to content

Instantly share code, notes, and snippets.

@LinnJS
Last active October 1, 2025 15:39
Show Gist options
  • Save LinnJS/76ead4666b94307ba8e917bd8dcc21e8 to your computer and use it in GitHub Desktop.
Save LinnJS/76ead4666b94307ba8e917bd8dcc21e8 to your computer and use it in GitHub Desktop.
Complete developer experience toolkit for git worktrees.
# ============================================================================
# worktrees.plugin.zsh
# ============================================================================
# Lightweight developer experience enhancement for git worktrees.
#
# This plugin provides a streamlined workflow for managing git worktrees with
# optional integrations for fzf (fuzzy finding), tmux (session management),
# and direnv (environment isolation).
#
# Features:
# - Quick access to worktrees with a single command
# - Automatic worktree creation and switching
# - Worktree status monitoring (dirty files, ahead/behind tracking)
# - Bulk cleanup operations (merged branches, old worktrees)
# - Worktree protection (lock/unlock) to prevent accidental removal
# - Archive management (hide inactive worktrees)
# - Per-worktree file ignoring (skip-worktree and local excludes)
# - GitHub PR integration (create worktrees from PRs)
# - Remote branch support (create from remote branches)
# - Interactive fuzzy finding with fzf
# - Automatic tmux session management
# - Directory-specific environment variables with direnv
# - Template system (copy files to new worktrees)
# - Hooks system (run commands on worktree switch)
#
# Requirements:
# - git (required)
# - fzf (optional, for interactive pickers)
# - tmux (optional, for session management)
# - direnv (optional, for per-worktree environment variables)
# - gh (optional, for GitHub PR integration)
#
# Installation:
# 1. Clone into Oh My Zsh custom plugins:
# git clone https://gist.github.com/LinnJS/76ead4666b94307ba8e917bd8dcc21e8.git ~/.oh-my-zsh/custom/plugins/worktrees.plugin.zsh
# 2. Add 'worktrees' to plugins array in ~/.zshrc:
# plugins=(... worktrees)
# 3. Reload shell: source ~/.zshrc
# 4. Run 'gwt help' for usage
#
# Quick Start:
# gwt main # Switch to main branch worktree
# gwt feature-new # Create and switch to feature-new
# gwt-status # Show status of all worktrees
# gwt-clean --merged # Clean up merged branches
# gwt-lock main # Lock main to prevent removal
# gwt-pr 123 # Create worktree from PR #123
# gwt help # Show full help
#
# ============================================================================
# ============================================================================
# Configuration Variables
# ============================================================================
# Override these in your ~/.zshrc before loading the plugin to customize behavior.
#
# Example:
# export GWT_ROOT="$HOME/projects/worktrees"
# export GWT_OPEN_CMD="nvim"
# export GWT_USE_TMUX="0"
# export GWT_AUTO_TRACK="1"
# export GWT_ON_SWITCH_CMD="echo 'Switched to:'"
# export GWT_TEMPLATE_DIR="$HOME/.config/worktree-templates"
# GWT_ROOT: Base directory where all worktrees will be organized.
# Structure: $GWT_ROOT/<repo-name>/<branch-name>
: ${GWT_ROOT:="$HOME/.worktrees"}
# GWT_OPEN_CMD: Command used to open worktrees in your editor/IDE.
# Common values: "code", "nvim", "idea", "subl", "emacs"
: ${GWT_OPEN_CMD:="code"}
# GWT_USE_TMUX: Enable automatic tmux session creation/attachment.
# Set to "1" to enable, "0" to disable.
: ${GWT_USE_TMUX:="0"}
# GWT_DIR_ENVRC: Automatically create a template .envrc file in new worktrees.
# Set to "1" to enable, "0" to disable. Requires direnv to be installed.
: ${GWT_DIR_ENVRC:="0"}
# GWT_PRUNE_ON_RM: Automatically prune worktree references after removal.
# Set to "1" to enable, "0" to disable.
: ${GWT_PRUNE_ON_RM:="1"}
# GWT_AUTO_TRACK: Automatically set up branch tracking to origin when creating worktrees.
# Set to "1" to enable, "0" to disable.
: ${GWT_AUTO_TRACK:="1"}
# GWT_ON_SWITCH_CMD: Command to run after switching worktrees.
# Useful for running setup scripts, installing dependencies, etc.
# The command receives the worktree path as the first argument.
: ${GWT_ON_SWITCH_CMD:=""}
# GWT_TEMPLATE_DIR: Directory containing template files to copy to new worktrees.
# Files from this directory will be copied to each new worktree.
: ${GWT_TEMPLATE_DIR:=""}
# ============================================================================
# Internal Helper Functions
# ============================================================================
# These functions are prefixed with _ to indicate they are internal utilities.
# They are not intended for direct user interaction.
# Get the absolute path to the root of the current git repository.
#
# Returns:
# The absolute path to the repo root, or exits with error if not in a repo.
#
# Example:
# repo_root=$(_gwt_repo)
_gwt_repo() {
git rev-parse --show-toplevel 2>/dev/null || {
echo "Error: Not in a git repository." >&2
return 1
}
}
# Get the name of the current git repository (basename of repo root).
#
# Returns:
# The repository name as a string.
#
# Example:
# repo_name=$(_gwt_name)
_gwt_name() {
local repo_root="$(_gwt_repo)" || return 1
# For worktrees, get the main repo name from the commondir if available
local git_dir="$(cd "$repo_root" && git rev-parse --git-common-dir 2>/dev/null)"
if [[ -n "$git_dir" ]] && [[ "$git_dir" != ".git" ]]; then
# We're in a worktree, get the parent directory of the common git dir
# Convert to absolute path if relative
if [[ "$git_dir" != /* ]]; then
git_dir="$repo_root/$git_dir"
fi
basename "$(dirname "$git_dir")"
else
basename "$repo_root"
fi
}
# Get the current branch name.
#
# Returns:
# The current branch name, or empty string if detached HEAD.
#
# Example:
# current_branch=$(_gwt_branch)
_gwt_branch() {
git rev-parse --abbrev-ref HEAD 2>/dev/null
}
# Construct the full path for a worktree given repo and branch names.
#
# Arguments:
# $1 - Repository name
# $2 - Branch name
#
# Returns:
# The constructed path: $GWT_ROOT/<repo>/<branch>
# Returns 1 if GWT_ROOT is not accessible
#
# Example:
# path=$(_gwt_path_for "myrepo" "feature-branch")
#
# NOTE: Branch names containing slashes (e.g., "user/feature") are sanitized
# by replacing "/" with "-" to create valid directory names.
_gwt_path_for() {
local repo="$1"
local br="$2"
# Validate GWT_ROOT exists and is writable
if [[ ! -d "$GWT_ROOT" ]]; then
echo "Error: GWT_ROOT directory does not exist: $GWT_ROOT" >&2
echo "Create it with: mkdir -p \"$GWT_ROOT\"" >&2
return 1
fi
if [[ ! -w "$GWT_ROOT" ]]; then
echo "Error: GWT_ROOT directory is not writable: $GWT_ROOT" >&2
return 1
fi
# Replace forward slashes with hyphens to avoid nested directories
local sanitized_br="${br//\//-}"
echo "$GWT_ROOT/$repo/$sanitized_br"
}
# Get the absolute path to the current worktree directory.
#
# Returns:
# The current working directory (worktree path).
#
# NOTE: This validates the current directory is part of a git worktree
# before returning the path.
_gwt_current_path() {
git rev-parse --path-format=absolute --git-path worktrees 2>/dev/null >/dev/null || true
pwd
}
# Check if fzf (fuzzy finder) is available in PATH.
#
# Returns:
# 0 if fzf is available, 1 otherwise.
_gwt_has_fzf() {
command -v fzf >/dev/null 2>&1
}
# Check if tmux (terminal multiplexer) is available in PATH.
#
# Returns:
# 0 if tmux is available, 1 otherwise.
_gwt_has_tmux() {
command -v tmux >/dev/null 2>&1
}
# Check if direnv (directory environment manager) is available in PATH.
#
# Returns:
# 0 if direnv is available, 1 otherwise.
_gwt_has_direnv() {
command -v direnv >/dev/null 2>&1
}
# ============================================================================
# Primary Commands
# ============================================================================
# The main entry points for the plugin - help and quick access.
# Display help information for all worktree commands.
#
# Shows a comprehensive overview of all available commands, their usage,
# and brief descriptions to help users navigate the plugin.
#
# Usage:
# gwt-help
# gwt help
#
# Returns:
# 0 (always succeeds)
#
# Example:
# $ gwt-help
gwt-help() {
cat <<'EOF'
╭─────────────────────────────────────────────────────────────────────────╮
│ Git Worktrees - Quick Reference │
╰─────────────────────────────────────────────────────────────────────────╯
MAIN COMMANDS
gwt <branch> Quick access - create or switch to worktree
gwt help Show this help message
WORKTREE MANAGEMENT
gwt-list [--plain] List all worktrees (🔒 shows locked)
gwt-add <branch> Create a new worktree
[--from <base>] ├─ Create from specific branch
[--track] ├─ Track remote branch
[--remote] └─ Create from remote branch (fetch)
gwt-switch [branch] Switch to a worktree (uses fzf picker)
gwt-open [branch] Open worktree in editor (uses fzf picker)
gwt-rm <branch> Remove worktree and branch
gwt-prune Clean up stale worktree references
gwt-mv <old> <new> Move/rename a worktree
gwt-main Navigate to main repository
gwt-migrate Convert main repo to worktrees workflow
gwt-status [-v] Show status of all worktrees
gwt-clean [opts] Bulk remove worktrees
[--merged] ├─ Remove merged branches
[--older-than <days>] ├─ Remove old worktrees
[--all] ├─ Remove all (with confirmation)
[--include-archived] ├─ Also clean archived worktrees
[--dry-run] └─ Preview without removing
GITHUB INTEGRATION
gwt-pr <number> Create worktree from PR number
gwt-diff <br1> <br2> Compare two worktrees
ADVANCED FEATURES
gwt-stash-list List stashes across all worktrees
gwt-archive <branch> Archive a worktree (hide from list)
gwt-unarchive <branch> Restore archived worktree
gwt-archives List archived worktrees
WORKTREE PROTECTION
gwt-lock [branch] Lock worktree to prevent removal
gwt-unlock <branch> Unlock worktree to allow removal
gwt-locks List all locked worktrees
FILE IGNORING
Skip-worktree (for tracked files with local changes):
gwt-ignore <file> Ignore local changes to tracked files
gwt-unignore <file> Stop ignoring file changes
gwt-ignored List skip-worktree files
Local excludes (for untracked patterns, per-worktree):
gwt-excludes <pattern> Add pattern to .git/info/exclude
gwt-excludes-list List local exclude patterns
gwt-excludes-edit Edit .git/info/exclude directly
EXAMPLES
# First time setup (if you have an existing repo)
gwt-migrate # Migrate from traditional git to worktrees
# Basic usage
gwt main # Switch to main branch worktree
gwt feature-auth # Create and switch to feature-auth
gwt-add fix-bug --from main # Create from main branch
gwt-add feat --remote # Create from remote branch
gwt-switch # Interactive picker (requires fzf)
gwt-main # Go to main repository
# Status and cleanup
gwt-status -v # Show detailed status
gwt-clean --merged # Remove merged branches
gwt-clean --older-than 30 # Remove worktrees older than 30 days
gwt-clean --older-than 180 --include-archived # Clean old archives
# GitHub integration
gwt-pr 123 # Create worktree from PR #123
gwt-diff main feature-auth # Compare two branches
# Archive management
gwt-archive old-feature # Archive a worktree
gwt-archives # List archived worktrees
gwt-unarchive old-feature # Restore from archive
# Worktree protection
gwt-lock main # Lock main branch to prevent removal
gwt-unlock main # Unlock main branch
# File ignoring
gwt-ignore config.json # Ignore local changes (skip-worktree)
gwt-excludes *.local # Local exclude pattern
CONFIGURATION VARIABLES
GWT_ROOT Base directory for worktrees
Default: $HOME/.worktrees
GWT_OPEN_CMD Editor command (code, nvim, idea, etc.)
Default: code
GWT_USE_TMUX Auto-create/attach tmux sessions (0 or 1)
Default: 0 (disabled)
GWT_DIR_ENVRC Auto-create .envrc files (0 or 1)
Default: 0 (disabled)
GWT_PRUNE_ON_RM Auto-prune after removal (0 or 1)
Default: 1 (enabled)
GWT_ON_SWITCH_CMD Command to run after switching worktrees
Receives worktree path as argument
Default: "" (disabled)
GWT_TEMPLATE_DIR Directory with template files to copy
to new worktrees
Default: "" (disabled)
OVERRIDE CONFIGURATION
Add to your ~/.zshrc before loading the plugin:
export GWT_ROOT="$HOME/projects/worktrees"
export GWT_OPEN_CMD="nvim"
export GWT_USE_TMUX="0"
export GWT_ON_SWITCH_CMD="npm install"
export GWT_TEMPLATE_DIR="$HOME/.config/worktree-templates"
OPTIONAL DEPENDENCIES
fzf - Interactive fuzzy finder for pickers
gh - GitHub CLI for PR integration (gwt-pr)
tmux - Terminal multiplexer for session management
direnv - Per-directory environment management
For detailed help on any command, check the inline documentation in:
~/.oh-my-zsh/custom/plugins/worktrees/worktrees.plugin.zsh
EOF
}
# Quick worktree access: create if needed, then switch to it.
#
# This is the main convenience command that combines gwt-add and gwt-switch.
# If the worktree exists, it switches to it. If not, it creates it first.
# This provides a fast workflow for jumping between branches.
#
# Usage:
# gwt <branch>
# gwt help
#
# Arguments:
# <branch> - Name of the branch to switch to or create (required)
#
# Returns:
# 0 on success, 1 on error
#
# Examples:
# # Switch to existing worktree
# $ gwt main
#
# # Create new worktree and switch to it
# $ gwt feature-new-thing
#
# # Show help
# $ gwt help
#
# NOTE: When creating a new worktree:
# - If GWT_TEMPLATE_DIR is set, template files will be copied
# - If GWT_DIR_ENVRC is enabled, a .envrc file will be created
#
# When switching:
# - If GWT_ON_SWITCH_CMD is set, it will be executed with the worktree path
# - If GWT_USE_TMUX is enabled, automatically creates or attaches to a tmux session
#
# This is the recommended command for daily worktree management as it handles
# both creation and switching seamlessly.
gwt() {
local br="$1"
# Handle help command
if [[ "$br" == "help" ]] || [[ "$br" == "--help" ]] || [[ "$br" == "-h" ]]; then
gwt-help
return 0
fi
if [[ -z "$br" ]]; then
echo "Usage: gwt <branch>" >&2
echo "" >&2
echo "Quick access to worktrees - creates if needed, then switches to it." >&2
echo "" >&2
echo "Examples:" >&2
echo " gwt main # Switch to main branch worktree" >&2
echo " gwt feature-auth # Create and switch to feature-auth" >&2
echo " gwt help # Show detailed help" >&2
return 1
fi
local repo="$(_gwt_name)" || return 1
local dest="$(_gwt_path_for "$repo" "$br")" || return 1
# Check if worktree directory exists
if [[ -d "$dest" ]]; then
# Worktree exists, switch to it
cd "$dest" || return 1
else
# Check if we're in the main repo and the current branch matches
local current_branch="$(_gwt_branch)"
local repo_root="$(_gwt_repo)"
local git_common_dir="$(cd "$repo_root" && git rev-parse --git-common-dir 2>/dev/null)"
# If we're in main repo (not a worktree) and already on the target branch
if [[ "$git_common_dir" == ".git" ]] && [[ "$current_branch" == "$br" ]]; then
echo "Error: Branch '$br' is already checked out in main repository"
echo "Location: $repo_root"
echo ""
echo "Quick fix - migrate to worktrees:"
echo " gwt-migrate"
echo ""
echo "Or manually:"
echo " 1. git checkout main"
echo " 2. gwt $br"
return 1
fi
# Try to create worktree
gwt-add "$br" || return 1
cd "$dest" || return 1
fi
# Run post-switch hook if configured
if [[ -n "$GWT_ON_SWITCH_CMD" ]]; then
eval "$GWT_ON_SWITCH_CMD \"$dest\"" || {
echo "Warning: GWT_ON_SWITCH_CMD failed" >&2
}
fi
# Optional: create/attach tmux session
if [[ "$GWT_USE_TMUX" == "1" ]] && _gwt_has_tmux; then
# Sanitize session name (alphanumeric and hyphens only)
local sess="${repo//[^A-Za-z0-9]/-}-${br//[^A-Za-z0-9]/-}"
if tmux has-session -t "$sess" 2>/dev/null; then
# Attach to existing session
tmux attach -t "$sess"
else
# Create new session with descriptive window name
tmux new -s "$sess" "$SHELL" \; rename-window "$repo:$br"
fi
fi
}
# ============================================================================
# Worktree Management Commands
# ============================================================================
# Core commands for creating, listing, switching, and removing worktrees.
# List all worktrees for the current repository.
#
# Outputs a sorted list of absolute paths to all worktrees for the current repo.
# Locked worktrees are indicated with a 🔒 symbol.
# Archived worktrees are not shown (use gwt-archives to see them).
#
# Usage:
# gwt-list [--plain]
#
# Options:
# --plain Hide lock status indicators (🔒)
#
# Returns:
# 0 on success, 1 if not in a git repository or no worktrees exist.
#
# Examples:
# $ gwt-list
# /Users/user/.worktrees/myrepo/feature-a
# /Users/user/.worktrees/myrepo/feature-b
# /Users/user/.worktrees/myrepo/main 🔒
#
# $ gwt-list --plain
# /Users/user/.worktrees/myrepo/feature-a
# /Users/user/.worktrees/myrepo/feature-b
# /Users/user/.worktrees/myrepo/main
#
# NOTE: Archived worktrees (in .archive/) are excluded from this list.
gwt-list() {
local repo="$(_gwt_name)" || return 1
local base="$GWT_ROOT/$repo"
local show_locks=1
# Parse options
if [[ "$1" == "--plain" ]]; then
show_locks=0
fi
if [[ -d "$base" ]]; then
local worktrees=(${(f)"$(find "$base" -maxdepth 1 -mindepth 1 -type d | sort)"})
for worktree in "${worktrees[@]}"; do
# Skip archived worktrees directory
if [[ "$(basename "$worktree")" == ".archive" ]]; then
continue
fi
if [[ $show_locks -eq 1 ]] && [[ -f "$worktree/.gwt-lock" ]]; then
echo "$worktree 🔒"
else
echo "$worktree"
fi
done
else
echo "No worktrees found for '$repo' under $base" >&2
return 0
fi
}
# Create a new worktree for a given branch.
#
# This command creates a new git worktree in the standardized location
# ($GWT_ROOT/<repo>/<branch>). If the branch doesn't exist, it will be created
# based on the current HEAD or a specified base branch.
#
# Usage:
# gwt-add <branch> [--track] [--from <base-branch>] [--remote]
#
# Arguments:
# <branch> - Name of the branch to create a worktree for
# --track - Track the remote branch (fetches and sets up tracking)
# --from <base> - Create new branch from <base> instead of current HEAD
# --remote - Create from remote branch (fetch and checkout from origin)
#
# Returns:
# 0 on success, 1 on error
#
# Examples:
# # Create worktree for existing branch
# $ gwt-add feature-login
#
# # Create new branch and worktree from current HEAD
# $ gwt-add feature-new
#
# # Create new branch and worktree from 'main'
# $ gwt-add feature-auth --from main
#
# # Track remote branch
# $ gwt-add feature-remote --track
#
# # Create worktree from remote branch
# $ gwt-add feature-remote --remote
#
# NOTE: If GWT_DIR_ENVRC is enabled, a template .envrc file will be created
# automatically in the new worktree directory.
gwt-add() {
local br="$1"
shift
if [[ -z "$br" ]]; then
echo "Usage: gwt-add <branch> [--track] [--from <base-branch>] [--remote]" >&2
return 1
fi
local repo_root="$(_gwt_repo)" || return 1
local repo="$(_gwt_name)" || return 1
local dest="$(_gwt_path_for "$repo" "$br")" || return 1
# Ensure parent directory exists
mkdir -p "$(dirname "$dest")" || {
echo "Error: Failed to create parent directory for worktree" >&2
return 1
}
# Check if branch already exists, if not create it
if git show-ref --verify --quiet "refs/heads/$br"; then
# Branch exists, use it as-is
:
else
# Branch doesn't exist - create it
local base="$(git rev-parse --abbrev-ref HEAD)"
local track_remote=0
local from_remote=0
# Parse optional arguments
while [[ "$1" != "" ]]; do
case "$1" in
--from)
shift
base="$1"
shift
;;
--track)
track_remote=1
shift
;;
--remote)
from_remote=1
shift
;;
*)
shift
;;
esac
done
# Handle remote creation (--remote flag)
if [[ $from_remote -eq 1 ]]; then
git fetch origin || {
echo "Error: Failed to fetch from origin" >&2
return 1
}
if git show-ref --verify --quiet "refs/remotes/origin/$br"; then
git branch --track "$br" "origin/$br" || return 1
echo "✓ Created branch from remote: origin/$br"
else
echo "Error: Remote branch 'origin/$br' not found" >&2
return 1
fi
# Handle remote tracking (--track flag)
elif [[ $track_remote -eq 1 ]]; then
git fetch --all --prune
if git branch --track "$br" "origin/$br" 2>/dev/null; then
echo "✓ Tracking remote branch: origin/$br"
else
echo "Warning: Remote branch 'origin/$br' not found. Creating local branch from $base instead." >&2
git branch "$br" "$base" || return 1
fi
else
# Create the branch if it still doesn't exist
if ! git show-ref --verify --quiet "refs/heads/$br"; then
git branch "$br" "$base" || return 1
fi
fi
fi
# Create the worktree
git worktree add "$dest" "$br" || return 1
# Set up branch tracking if enabled and remote branch exists
if [[ "$GWT_AUTO_TRACK" == "1" ]]; then
(cd "$dest" && {
local upstream="$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null)"
if [[ -z "$upstream" ]]; then
# No upstream set, check if remote branch exists
if git show-ref --verify --quiet "refs/remotes/origin/$br"; then
git branch --set-upstream-to="origin/$br" "$br" >/dev/null 2>&1 && \
echo "✓ Set upstream to origin/$br"
fi
fi
})
fi
# Add .gwt-lock to local excludes (never commit lock files)
local git_dir="$(cd "$dest" && git rev-parse --git-dir)" || true
if [[ -n "$git_dir" ]]; then
mkdir -p "$git_dir/info" || {
echo "Warning: Failed to create $git_dir/info directory" >&2
}
if [[ ! -f "$git_dir/info/exclude" ]] || ! grep -Fxq ".gwt-lock" "$git_dir/info/exclude" 2>/dev/null; then
echo ".gwt-lock" >> "$git_dir/info/exclude" || {
echo "Warning: Failed to add .gwt-lock to exclude file" >&2
}
fi
fi
# Optional: seed .envrc if direnv is enabled
if [[ "$GWT_DIR_ENVRC" == "1" ]] && _gwt_has_direnv; then
if [[ ! -f "$dest/.envrc" ]]; then
cat > "$dest/.envrc" <<'EOF'
# direnv configuration for this worktree
#
# Examples:
# layout node # Load node environment
# layout python # Load python environment
# export NODE_ENV=development
# export DEBUG=true
EOF
fi
(cd "$dest" && direnv allow >/dev/null 2>&1 || true)
fi
# Copy template files if template directory is configured
if [[ -n "$GWT_TEMPLATE_DIR" ]] && [[ -d "$GWT_TEMPLATE_DIR" ]]; then
echo "Copying template files..."
cp -r "$GWT_TEMPLATE_DIR"/. "$dest/" 2>/dev/null || {
echo "Warning: Failed to copy some template files" >&2
}
fi
echo "✓ Added worktree: $dest"
}
# Switch to an existing worktree directory.
#
# Changes the current directory to a specified worktree. If no branch is
# specified and fzf is available, presents an interactive picker.
# Optionally creates or attaches to a tmux session for the worktree.
#
# Usage:
# gwt-switch [branch]
#
# Arguments:
# [branch] - Optional. Branch name to switch to. If omitted, uses fzf picker.
#
# Returns:
# 0 on success, 1 on error or if no worktree is selected
#
# Examples:
# # Switch using fuzzy finder (requires fzf)
# $ gwt-switch
#
# # Switch to specific worktree
# $ gwt-switch feature-login
#
# NOTE: If GWT_USE_TMUX is enabled, automatically creates or attaches to a
# tmux session named <repo>-<branch>.
gwt-switch() {
local repo="$(_gwt_name)" || return 1
local pick="$1"
# If no branch specified, use fzf picker or error
if [[ -z "$pick" ]]; then
if _gwt_has_fzf; then
# Use --plain to avoid emoji in paths for fzf selection
pick="$(gwt-list --plain | fzf --prompt="worktrees> " --height=15 --reverse)"
[[ -z "$pick" ]] && return 1
else
echo "Error: Please specify a branch or install fzf for interactive picker." >&2
echo "Usage: gwt-switch <branch>" >&2
return 1
fi
else
# Allow branch name shortcut - expand to full path
local dest="$(_gwt_path_for "$repo" "$pick")"
if [[ -d "$dest" ]]; then
pick="$dest"
fi
fi
# Change to the selected worktree directory
cd "$pick" || return 1
# Run post-switch hook if configured
if [[ -n "$GWT_ON_SWITCH_CMD" ]]; then
eval "$GWT_ON_SWITCH_CMD \"$pick\"" || {
echo "Warning: GWT_ON_SWITCH_CMD failed" >&2
}
fi
# Optional: create/attach tmux session
if [[ "$GWT_USE_TMUX" == "1" ]] && _gwt_has_tmux; then
# Extract branch name from path by removing the repo base directory
local base="$GWT_ROOT/$repo"
local br="${pick#$base/}"
# Sanitize session name (alphanumeric and hyphens only)
local sess="${repo//[^A-Za-z0-9]/-}-${br//[^A-Za-z0-9]/-}"
if tmux has-session -t "$sess" 2>/dev/null; then
# Attach to existing session
tmux attach -t "$sess"
else
# Create new session with descriptive window name
tmux new -s "$sess" "$SHELL" \; rename-window "$repo:$br"
fi
fi
}
# Open a worktree in your configured editor/IDE.
#
# Opens the specified worktree directory using the command set in GWT_OPEN_CMD.
# If no branch is specified, uses fzf picker if available or opens current directory.
#
# Usage:
# gwt-open [branch]
#
# Arguments:
# [branch] - Optional. Branch name of worktree to open.
# If omitted, uses fzf picker or current directory.
#
# Returns:
# 0 on success, 1 on error or if worktree doesn't exist
#
# Examples:
# # Open using fuzzy finder (requires fzf)
# $ gwt-open
#
# # Open specific worktree
# $ gwt-open feature-login
#
# NOTE: The editor command is configured via GWT_OPEN_CMD (default: "code")
gwt-open() {
local repo="$(_gwt_name)" || return 1
local dest=""
if [[ -z "$1" ]]; then
# No argument - use picker or current path
if _gwt_has_fzf; then
# Use --plain to avoid emoji in paths for fzf selection
dest="$(gwt-list --plain | fzf --prompt="open> " --height=15 --reverse)"
[[ -z "$dest" ]] && return 1
else
dest="$(_gwt_current_path)"
fi
else
# Branch name provided
dest="$(_gwt_path_for "$repo" "$1")"
fi
# Validate destination exists
if [[ ! -d "$dest" ]]; then
echo "Error: No such worktree: $dest" >&2
return 1
fi
# Open in configured editor
# Split command on spaces for proper execution (supports "code --new-window" etc.)
local -a cmd_parts
cmd_parts=("${(@s/ /)GWT_OPEN_CMD}")
"${cmd_parts[@]}" "$dest"
}
# Remove a worktree and optionally its associated branch.
#
# Removes both the worktree directory and the git branch. Includes safety
# checks to prevent removing a worktree you're currently in.
#
# Usage:
# gwt-rm <branch>
#
# Arguments:
# <branch> - Name of the branch/worktree to remove (required)
#
# Returns:
# 0 on success, 1 on error
#
# Examples:
# $ gwt-rm feature-old
#
# Safety Features:
# - Prevents removal of locked worktrees (use gwt-unlock first)
# - Prevents removal if you're currently in the worktree directory
# - Optionally prunes stale worktree references (controlled by GWT_PRUNE_ON_RM)
# - Force-deletes the branch to ensure complete cleanup
#
# WARNING: This operation is destructive and cannot be undone. Ensure any
# important changes are committed and pushed before removing a worktree.
gwt-rm() {
local br="$1"
if [[ -z "$br" ]]; then
echo "Usage: gwt-rm <branch>" >&2
return 1
fi
local repo="$(_gwt_name)" || return 1
local dest="$(_gwt_path_for "$repo" "$br")"
# Validate worktree exists
if [[ ! -d "$dest" ]]; then
echo "Error: No such worktree directory: $dest" >&2
return 1
fi
# Safety check: prevent removing locked worktree
if [[ -f "$dest/.gwt-lock" ]]; then
echo "Error: Worktree '$br' is locked." >&2
echo "Run 'gwt-unlock $br' first to allow removal." >&2
return 1
fi
# Safety check: prevent removing current directory
local current_dir="$(pwd)"
if [[ "$current_dir" == "$dest" ]] || [[ "$current_dir" == "$dest/"* ]]; then
echo "Error: You are currently inside this worktree." >&2
echo "Please change to a different directory first." >&2
return 1
fi
# Remove the worktree
git worktree remove "$dest" || return 1
# Remove the associated branch if it exists
if git show-ref --verify --quiet "refs/heads/$br"; then
git branch -D "$br" || true
fi
# Optional: prune stale worktree references
if [[ "$GWT_PRUNE_ON_RM" == "1" ]]; then
git worktree prune >/dev/null 2>&1 || true
fi
echo "✓ Removed worktree and branch: $br"
}
# Prune stale worktree administrative files.
#
# Cleans up worktree references for directories that have been manually deleted
# or are otherwise stale. This is useful for maintenance and cleanup.
#
# Usage:
# gwt-prune
#
# Returns:
# Exits with git worktree prune's return code
#
# Example:
# $ gwt-prune
#
# NOTE: This is automatically called after gwt-rm if GWT_PRUNE_ON_RM is enabled.
gwt-prune() {
git worktree prune
}
# Show status of all worktrees (uncommitted changes, ahead/behind remote).
#
# Lists all worktrees and their git status including:
# - Uncommitted changes (modified, added, deleted files)
# - Branch tracking status (ahead/behind remote)
# - Lock status
#
# Usage:
# gwt-status [--verbose]
#
# Options:
# --verbose Show detailed file-level changes
#
# Returns:
# 0 on success, 1 if not in a git repository
#
# Examples:
# $ gwt-status
# main [clean] [↑2] 🔒
# feature-auth [3 modified, 1 added]
# feature-new [clean]
#
# $ gwt-status --verbose
# main [↑2] 🔒
# M src/index.js
# M src/app.js
gwt-status() {
local repo="$(_gwt_name)" || return 1
local base="$GWT_ROOT/$repo"
local verbose=0
if [[ "$1" == "--verbose" ]] || [[ "$1" == "-v" ]]; then
verbose=1
fi
if [[ ! -d "$base" ]]; then
echo "No worktrees found for '$repo' under $base" >&2
return 1
fi
for worktree in "$base"/*; do
if [[ ! -d "$worktree" ]]; then
continue
fi
local branch="$(basename "$worktree")"
local lock_indicator=""
local status_info=""
# Check lock status
if [[ -f "$worktree/.gwt-lock" ]]; then
lock_indicator=" 🔒"
fi
# Get git status
(
cd "$worktree" || return
# Check for uncommitted changes
local modified=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
local tracking_info=$(git status -sb 2>/dev/null | head -n1)
# Parse ahead/behind
local ahead_behind=""
if [[ "$tracking_info" =~ \[ahead\ ([0-9]+)\] ]]; then
ahead_behind="${ahead_behind}↑${match[1]}"
fi
if [[ "$tracking_info" =~ \[behind\ ([0-9]+)\] ]]; then
ahead_behind="${ahead_behind}↓${match[1]}"
fi
if [[ "$tracking_info" =~ \[ahead\ ([0-9]+),\ behind\ ([0-9]+)\] ]]; then
ahead_behind="↑${match[1]}↓${match[2]}"
fi
# Build status string
if [[ $modified -eq 0 ]]; then
status_info="[clean]"
else
status_info="[${modified} changes]"
fi
if [[ -n "$ahead_behind" ]]; then
status_info="${status_info} [$ahead_behind]"
fi
echo "$branch $status_info$lock_indicator"
# Show verbose output
if [[ $verbose -eq 1 ]] && [[ $modified -gt 0 ]]; then
git status --short 2>/dev/null | sed 's/^/ /'
fi
)
done
}
# Navigate to the main (bare) repository directory.
#
# Changes directory to the main repository location (the one with .git folder).
# Useful for operations that need to be run from the main repo.
#
# Usage:
# gwt-main
#
# Returns:
# 0 on success, 1 if not in a git repository
#
# Example:
# $ gwt-main
# $ pwd
# /Users/user/original-repo-location
gwt-main() {
local repo_root="$(_gwt_repo)" || return 1
local git_common_dir="$(cd "$repo_root" && git rev-parse --git-common-dir 2>/dev/null)"
if [[ -n "$git_common_dir" ]] && [[ "$git_common_dir" != ".git" ]]; then
# We're in a worktree, navigate to main repo
if [[ "$git_common_dir" != /* ]]; then
git_common_dir="$repo_root/$git_common_dir"
fi
local main_repo="$(dirname "$git_common_dir")"
cd "$main_repo" || return 1
echo "Switched to main repository: $main_repo"
else
# Already in main repo
echo "Already in main repository: $repo_root"
fi
}
# Convert main repository to use worktrees workflow.
#
# This command helps migrate from a traditional git workflow to worktrees by:
# 1. Moving the current branch to a worktree
# 2. Checking out a "safe" branch (main/master) in the original location
#
# Usage:
# gwt-migrate [--branch <branch>]
#
# Options:
# --branch <name> Branch to keep in main repo (default: main or master)
#
# Returns:
# 0 on success, 1 on error
#
# Example:
# $ pwd
# /Users/user/Developer/my-project
# $ git branch
# * feature-branch
# $ gwt-migrate
# # Creates worktree for feature-branch, checks out main in original location
#
# NOTE: After migration, use 'gwt <branch>' to work with worktrees.
gwt-migrate() {
local safe_branch=""
local force_detach=0
# Parse arguments
while [[ "$1" != "" ]]; do
case "$1" in
--branch)
shift
safe_branch="$1"
shift
;;
--detach)
force_detach=1
shift
;;
*)
echo "Unknown option: $1" >&2
echo "Usage: gwt-migrate [--branch <branch>] [--detach]" >&2
return 1
;;
esac
done
local repo_root="$(_gwt_repo)" || return 1
local git_common_dir="$(cd "$repo_root" && git rev-parse --git-common-dir 2>/dev/null)"
# Check if already in a worktree
if [[ "$git_common_dir" != ".git" ]]; then
echo "Already in a worktree. Migration only works from main repository."
return 1
fi
local current_branch="$(_gwt_branch)"
local repo="$(_gwt_name)" || return 1
# If --detach is specified, just detach and exit
if [[ $force_detach -eq 1 ]]; then
echo "Detaching HEAD in main repository..."
git checkout --detach HEAD || return 1
echo "✓ Main repo is now in detached HEAD state"
echo ""
echo "You can now use: gwt <branch-name> to create/switch to worktrees"
return 0
fi
# Determine safe branch if not specified
if [[ -z "$safe_branch" ]]; then
if git show-ref --verify --quiet "refs/heads/main"; then
safe_branch="main"
elif git show-ref --verify --quiet "refs/heads/master"; then
safe_branch="master"
else
echo "Error: No main/master branch found. Specify branch with --branch <name> or use --detach" >&2
return 1
fi
fi
# Check if safe branch exists
if ! git show-ref --verify --quiet "refs/heads/$safe_branch"; then
echo "Error: Branch '$safe_branch' does not exist" >&2
return 1
fi
# Check if safe branch is the current branch - if so, nothing to migrate
if [[ "$current_branch" == "$safe_branch" ]]; then
echo "Already on '$safe_branch'. Nothing to migrate."
echo "You can now use: gwt <branch-name> to create worktrees"
return 0
fi
echo "Migrating to worktrees workflow..."
echo ""
echo "Current branch: $current_branch"
echo "Target: $safe_branch (or detached HEAD if already used)"
echo ""
# Check for uncommitted changes
if ! git diff-index --quiet HEAD 2>/dev/null; then
echo "Error: You have uncommitted changes. Please commit or stash them first." >&2
return 1
fi
# Check if current branch worktree already exists
local dest="$(_gwt_path_for "$repo" "$current_branch")" || return 1
if [[ -d "$dest" ]]; then
echo "Note: Worktree already exists for '$current_branch' at: $dest"
echo "Skipping worktree creation..."
else
echo "Step 1: Creating worktree for '$current_branch'..."
gwt-add "$current_branch" || return 1
fi
echo ""
echo "Step 2: Switching main repo to '$safe_branch'..."
# Check if safe_branch is already used by a worktree
local safe_branch_dest="$(_gwt_path_for "$repo" "$safe_branch")"
if [[ -d "$safe_branch_dest" ]]; then
echo "Note: '$safe_branch' worktree already exists at: $safe_branch_dest"
echo "Checking out detached HEAD instead to free up the main repo..."
git checkout --detach HEAD || return 1
echo ""
echo "Main repo is now in detached HEAD state (this is safe and expected)."
else
git checkout "$safe_branch" || return 1
fi
echo ""
echo "✓ Migration complete!"
echo ""
echo "Your work is now at: $dest"
echo "To continue working: gwt $current_branch"
echo ""
echo "Main repo location: $repo_root"
echo "└─ You can safely ignore this directory now - work in worktrees instead!"
echo ""
echo "Future workflow:"
echo " gwt <branch-name> # Switch to or create worktree"
echo " gwt-list # List all worktrees"
echo " gwt help # Show all commands"
}
# Clean up worktrees based on criteria (merged, stale, old).
#
# Bulk removal of worktrees that match specified criteria:
# - Merged branches (already merged into main/master)
# - Stale worktrees (not modified in X days)
# - All unlocked worktrees (with confirmation)
# - Optionally include archived worktrees
#
# Usage:
# gwt-clean [--merged] [--older-than <days>] [--all] [--include-archived] [--dry-run]
#
# Options:
# --merged Remove worktrees for branches merged into main
# --older-than N Remove worktrees not modified in N days
# --all Remove all unlocked worktrees (requires confirmation)
# --include-archived Also clean archived worktrees (default: skip archives)
# --dry-run Show what would be removed without removing
#
# Returns:
# 0 on success, 1 on error
#
# Examples:
# $ gwt-clean --merged
# $ gwt-clean --older-than 30
# $ gwt-clean --merged --dry-run
# $ gwt-clean --all
# $ gwt-clean --older-than 180 --include-archived # Clean old archives too
#
# Safety Features:
# - Never removes locked worktrees
# - By default, skips archived worktrees (use --include-archived to clean them)
# - Shows confirmation prompt for --all
# - Dry-run mode available
#
# Archive vs Lock:
# - Locked = "Active and important, never delete"
# - Archived = "Inactive, out of the way, but preserved until explicitly cleaned"
gwt-clean() {
local repo="$(_gwt_name)" || return 1
local base="$GWT_ROOT/$repo"
local check_merged=0
local check_age=0
local age_days=0
local remove_all=0
local dry_run=0
local include_archived=0
# Parse arguments
while [[ "$1" != "" ]]; do
case "$1" in
--merged)
check_merged=1
shift
;;
--older-than)
check_age=1
shift
age_days="$1"
shift
;;
--all)
remove_all=1
shift
;;
--include-archived)
include_archived=1
shift
;;
--dry-run)
dry_run=1
shift
;;
*)
echo "Unknown option: $1" >&2
echo "Usage: gwt-clean [--merged] [--older-than <days>] [--all] [--include-archived] [--dry-run]" >&2
return 1
;;
esac
done
if [[ ! -d "$base" ]]; then
echo "No worktrees found for '$repo' under $base" >&2
return 1
fi
# Get main branch name
local main_branch="main"
if ! git show-ref --verify --quiet "refs/heads/main"; then
if git show-ref --verify --quiet "refs/heads/master"; then
main_branch="master"
fi
fi
local -a to_remove
local removed_count=0
# Function to check and collect worktrees from a directory
_check_worktrees() {
local check_base="$1"
local is_archive="$2"
for worktree in "$check_base"/*; do
if [[ ! -d "$worktree" ]]; then
continue
fi
local branch="$(basename "$worktree")"
# Skip archived worktrees directory when checking main base
if [[ "$is_archive" == "0" ]] && [[ "$branch" == ".archive" ]]; then
continue
fi
# Skip locked worktrees
if [[ -f "$worktree/.gwt-lock" ]]; then
continue
fi
local should_remove=0
# Check if --all flag is set
if [[ $remove_all -eq 1 ]]; then
should_remove=1
fi
# Check if merged
if [[ $check_merged -eq 1 ]]; then
(
cd "$worktree" || exit
if git branch --merged "$main_branch" 2>/dev/null | grep -q "^[* ]*$(git rev-parse --abbrev-ref HEAD)$"; then
should_remove=1
fi
)
fi
# Check age
if [[ $check_age -eq 1 ]]; then
local mod_time=$(stat -f %m "$worktree" 2>/dev/null || stat -c %Y "$worktree" 2>/dev/null)
local current_time=$(date +%s)
local age_seconds=$((age_days * 86400))
if [[ $((current_time - mod_time)) -gt $age_seconds ]]; then
should_remove=1
fi
fi
if [[ $should_remove -eq 1 ]]; then
if [[ "$is_archive" == "1" ]]; then
# Store full path for archived worktrees with marker
to_remove+=("ARCHIVE:$worktree")
else
to_remove+=("$branch")
fi
fi
done
}
# Collect worktrees to remove from main directory
_check_worktrees "$base" "0"
# If --include-archived is set, also check archived worktrees
if [[ $include_archived -eq 1 ]] && [[ -d "$base/.archive" ]]; then
_check_worktrees "$base/.archive" "1"
fi
# Show what will be removed
if [[ ${#to_remove[@]} -eq 0 ]]; then
echo "No worktrees match the criteria for removal."
return 0
fi
echo "The following worktrees will be removed:"
for item in "${to_remove[@]}"; do
if [[ "$item" == ARCHIVE:* ]]; then
local path="${item#ARCHIVE:}"
echo " - $(basename "$path") (archived)"
else
echo " - $item"
fi
done
if [[ $dry_run -eq 1 ]]; then
echo ""
echo "(Dry run - nothing was actually removed)"
return 0
fi
# Confirmation for --all
if [[ $remove_all -eq 1 ]]; then
echo ""
echo -n "Are you sure you want to remove ${#to_remove[@]} worktrees? [y/N] "
read confirm
if [[ "$confirm" != "y" ]] && [[ "$confirm" != "Y" ]]; then
echo "Cancelled."
return 0
fi
fi
# Remove worktrees
echo ""
for item in "${to_remove[@]}"; do
if [[ "$item" == ARCHIVE:* ]]; then
# Archived worktree - remove directly
local path="${item#ARCHIVE:}"
local branch="$(basename "$path")"
if rm -rf "$path" 2>/dev/null; then
echo "✓ Removed archived worktree: $branch"
((removed_count++))
else
echo "✗ Failed to remove archived worktree: $branch" >&2
fi
else
# Regular worktree - use gwt-rm
if gwt-rm "$item" 2>/dev/null; then
((removed_count++))
fi
fi
done
echo ""
echo "✓ Removed $removed_count worktree(s)"
}
# Move or rename a worktree.
#
# Moves a worktree to a new location or renames its branch.
# Uses git worktree move under the hood.
#
# Usage:
# gwt-mv <old-branch> <new-branch>
#
# Arguments:
# <old-branch> - Current branch name
# <new-branch> - New branch name
#
# Returns:
# 0 on success, 1 on error
#
# Examples:
# $ gwt-mv feature-old feature-new
# $ gwt-mv temp-fix permanent-fix
#
# NOTE: This renames the worktree directory and the branch.
gwt-mv() {
local old_br="$1"
local new_br="$2"
if [[ -z "$old_br" ]] || [[ -z "$new_br" ]]; then
echo "Usage: gwt-mv <old-branch> <new-branch>" >&2
return 1
fi
local repo="$(_gwt_name)" || return 1
local old_dest="$(_gwt_path_for "$repo" "$old_br")" || return 1
local new_dest="$(_gwt_path_for "$repo" "$new_br")" || return 1
# Validate old worktree exists
if [[ ! -d "$old_dest" ]]; then
echo "Error: No such worktree: $old_br" >&2
return 1
fi
# Validate new worktree doesn't exist
if [[ -d "$new_dest" ]]; then
echo "Error: Worktree already exists: $new_br" >&2
return 1
fi
# Move the worktree
git worktree move "$old_dest" "$new_dest" || return 1
# Rename the branch if it exists
if git show-ref --verify --quiet "refs/heads/$old_br"; then
(cd "$new_dest" && git branch -m "$old_br" "$new_br") || {
echo "Warning: Failed to rename branch from $old_br to $new_br" >&2
}
fi
echo "✓ Moved worktree: $old_br → $new_br"
}
# Create worktree from a GitHub pull request.
#
# Fetches and creates a worktree for a GitHub pull request using gh CLI.
# Requires the GitHub CLI (gh) to be installed and authenticated.
#
# Usage:
# gwt-pr <pr-number>
#
# Arguments:
# <pr-number> - GitHub pull request number
#
# Returns:
# 0 on success, 1 on error
#
# Examples:
# $ gwt-pr 123
# $ gwt-pr 456
#
# NOTE: Requires 'gh' (GitHub CLI) to be installed.
gwt-pr() {
local pr_number="$1"
if [[ -z "$pr_number" ]]; then
echo "Usage: gwt-pr <pr-number>" >&2
return 1
fi
# Check if gh is available
if ! command -v gh >/dev/null 2>&1; then
echo "Error: GitHub CLI (gh) is not installed." >&2
echo "Install it from: https://cli.github.com/" >&2
return 1
fi
# Get PR information
local pr_info=$(gh pr view "$pr_number" --json headRefName,number,title 2>&1)
if [[ $? -ne 0 ]]; then
echo "Error: Failed to fetch PR #$pr_number" >&2
echo "$pr_info" >&2
return 1
fi
local branch=$(echo "$pr_info" | grep -o '"headRefName":"[^"]*"' | cut -d'"' -f4)
local title=$(echo "$pr_info" | grep -o '"title":"[^"]*"' | cut -d'"' -f4)
if [[ -z "$branch" ]]; then
echo "Error: Could not determine branch name for PR #$pr_number" >&2
return 1
fi
echo "Creating worktree for PR #$pr_number: $title"
echo "Branch: $branch"
echo ""
# Checkout the PR
gh pr checkout "$pr_number" || {
echo "Error: Failed to checkout PR #$pr_number" >&2
return 1
}
# Create worktree if it doesn't exist
local repo="$(_gwt_name)" || return 1
local dest="$(_gwt_path_for "$repo" "$branch")" || return 1
if [[ ! -d "$dest" ]]; then
gwt-add "$branch" || return 1
fi
# Switch to the worktree
cd "$dest" || return 1
echo "✓ Switched to PR #$pr_number worktree: $dest"
}
# Compare two worktrees (show diff).
#
# Shows the git diff between two worktree branches.
# Useful for comparing changes across different worktrees.
#
# Usage:
# gwt-diff <branch1> <branch2> [git-diff-options]
#
# Arguments:
# <branch1> - First branch to compare
# <branch2> - Second branch to compare
# [diff-options] - Additional git diff options (--stat, --name-only, etc.)
#
# Returns:
# 0 on success, 1 on error
#
# Examples:
# $ gwt-diff main feature-auth
# $ gwt-diff main feature-auth --stat
# $ gwt-diff feature-a feature-b --name-only
gwt-diff() {
local branch1="$1"
local branch2="$2"
shift 2
if [[ -z "$branch1" ]] || [[ -z "$branch2" ]]; then
echo "Usage: gwt-diff <branch1> <branch2> [git-diff-options]" >&2
return 1
fi
local repo="$(_gwt_name)" || return 1
local dest1="$(_gwt_path_for "$repo" "$branch1")" || return 1
local dest2="$(_gwt_path_for "$repo" "$branch2")" || return 1
# Validate both worktrees exist
if [[ ! -d "$dest1" ]]; then
echo "Error: No such worktree: $branch1" >&2
return 1
fi
if [[ ! -d "$dest2" ]]; then
echo "Error: No such worktree: $branch2" >&2
return 1
fi
# Run git diff
git diff "$branch1" "$branch2" "$@"
}
# List stashes across all worktrees.
#
# Shows all stashes with indication of which worktree they belong to.
# Helps manage stashes when working with multiple worktrees.
#
# Usage:
# gwt-stash-list
#
# Returns:
# 0 on success, 1 on error
#
# Example:
# $ gwt-stash-list
# [main] stash@{0}: WIP on main: abc1234 Fix bug
# [feature-auth] stash@{1}: WIP on feature-auth: def5678 Add login
gwt-stash-list() {
local repo="$(_gwt_name)" || return 1
local base="$GWT_ROOT/$repo"
if [[ ! -d "$base" ]]; then
echo "No worktrees found for '$repo' under $base" >&2
return 1
fi
local found_stashes=0
for worktree in "$base"/*; do
if [[ ! -d "$worktree" ]]; then
continue
fi
local branch="$(basename "$worktree")"
(
cd "$worktree" || exit
local stashes=$(git stash list 2>/dev/null)
if [[ -n "$stashes" ]]; then
echo "$stashes" | while IFS= read -r line; do
echo "[$branch] $line"
done
found_stashes=1
fi
)
done
if [[ $found_stashes -eq 0 ]]; then
echo "No stashes found in any worktree."
fi
}
# Archive a worktree (mark as inactive without removing).
#
# Moves a worktree to an archive subdirectory to keep it but mark as inactive.
# Archived worktrees don't appear in regular listings.
#
# Usage:
# gwt-archive <branch>
#
# Arguments:
# <branch> - Branch name to archive
#
# Returns:
# 0 on success, 1 on error
#
# Example:
# $ gwt-archive old-feature
gwt-archive() {
local br="$1"
if [[ -z "$br" ]]; then
echo "Usage: gwt-archive <branch>" >&2
return 1
fi
local repo="$(_gwt_name)" || return 1
local src="$(_gwt_path_for "$repo" "$br")" || return 1
local archive_base="$GWT_ROOT/$repo/.archive"
local dest="$archive_base/$br"
# Validate worktree exists
if [[ ! -d "$src" ]]; then
echo "Error: No such worktree: $br" >&2
return 1
fi
# Create archive directory
mkdir -p "$archive_base" || {
echo "Error: Failed to create archive directory" >&2
return 1
}
# Move worktree to archive
mv "$src" "$dest" || {
echo "Error: Failed to archive worktree" >&2
return 1
}
echo "✓ Archived worktree: $br → .archive/$br"
}
# Unarchive a worktree (restore from archive).
#
# Restores an archived worktree back to active status.
#
# Usage:
# gwt-unarchive <branch>
#
# Arguments:
# <branch> - Branch name to unarchive
#
# Returns:
# 0 on success, 1 on error
#
# Example:
# $ gwt-unarchive old-feature
gwt-unarchive() {
local br="$1"
if [[ -z "$br" ]]; then
echo "Usage: gwt-unarchive <branch>" >&2
return 1
fi
local repo="$(_gwt_name)" || return 1
local archive_base="$GWT_ROOT/$repo/.archive"
local src="$archive_base/$br"
local dest="$(_gwt_path_for "$repo" "$br")" || return 1
# Validate archived worktree exists
if [[ ! -d "$src" ]]; then
echo "Error: No archived worktree: $br" >&2
return 1
fi
# Validate destination doesn't exist
if [[ -d "$dest" ]]; then
echo "Error: Worktree already exists: $br" >&2
return 1
fi
# Move worktree from archive
mv "$src" "$dest" || {
echo "Error: Failed to unarchive worktree" >&2
return 1
}
echo "✓ Unarchived worktree: .archive/$br → $br"
}
# List archived worktrees.
#
# Shows all worktrees that have been archived.
#
# Usage:
# gwt-archives
#
# Returns:
# 0 on success
#
# Example:
# $ gwt-archives
# old-feature
# temp-experiment
gwt-archives() {
local repo="$(_gwt_name)" || return 1
local archive_base="$GWT_ROOT/$repo/.archive"
if [[ ! -d "$archive_base" ]]; then
echo "No archived worktrees found." >&2
return 0
fi
local found=0
for worktree in "$archive_base"/*; do
if [[ -d "$worktree" ]]; then
basename "$worktree"
found=1
fi
done
if [[ $found -eq 0 ]]; then
echo "No archived worktrees found." >&2
fi
}
# ============================================================================
# Worktree Protection Commands
# ============================================================================
# Commands for locking/unlocking worktrees to prevent accidental removal.
# Lock a worktree to prevent accidental removal.
#
# Creates a lock file in the worktree directory to mark it as protected.
# Locked worktrees cannot be removed with gwt-rm without first unlocking.
#
# Usage:
# gwt-lock [branch]
#
# Arguments:
# [branch] - Optional. Branch name to lock. If omitted, locks current worktree.
#
# Returns:
# 0 on success, 1 on error
#
# Examples:
# # Lock current worktree
# $ gwt-lock
#
# # Lock specific worktree
# $ gwt-lock main
#
# NOTE: Useful for protecting main branches or active development worktrees.
gwt-lock() {
local repo="$(_gwt_name)" || return 1
local dest=""
if [[ -z "$1" ]]; then
# No argument - lock current worktree
dest="$(_gwt_current_path)"
else
# Branch name provided
dest="$(_gwt_path_for "$repo" "$1")"
fi
# Validate destination exists
if [[ ! -d "$dest" ]]; then
echo "Error: No such worktree: $dest" >&2
return 1
fi
# Check if already locked
if [[ -f "$dest/.gwt-lock" ]]; then
echo "⊘ Already locked: $(basename "$dest")" >&2
return 0
fi
# Create lock file with timestamp and user
cat > "$dest/.gwt-lock" <<EOF
# Worktree lock file
# This worktree is protected from accidental removal
# Created: $(date)
# By: ${USER:-unknown}
EOF
echo "🔒 Locked worktree: $(basename "$dest")"
}
# Unlock a worktree to allow removal.
#
# Removes the lock file from the worktree directory.
#
# Usage:
# gwt-unlock <branch>
#
# Arguments:
# <branch> - Branch name to unlock (required)
#
# Returns:
# 0 on success, 1 on error
#
# Examples:
# $ gwt-unlock main
# $ gwt-unlock feature-login
gwt-unlock() {
local br="$1"
if [[ -z "$br" ]]; then
echo "Usage: gwt-unlock <branch>" >&2
return 1
fi
local repo="$(_gwt_name)" || return 1
local dest="$(_gwt_path_for "$repo" "$br")"
# Validate destination exists
if [[ ! -d "$dest" ]]; then
echo "Error: No such worktree: $dest" >&2
return 1
fi
# Check if locked
if [[ ! -f "$dest/.gwt-lock" ]]; then
echo "⊘ Not locked: $br" >&2
return 0
fi
# Remove lock file
rm -f "$dest/.gwt-lock"
echo "🔓 Unlocked worktree: $br"
}
# List all locked worktrees.
#
# Shows which worktrees are currently locked and protected from removal.
#
# Usage:
# gwt-locks
#
# Returns:
# 0 on success, 1 if not in a git repository
#
# Example:
# $ gwt-locks
# main
# production
gwt-locks() {
local repo="$(_gwt_name)" || return 1
local base="$GWT_ROOT/$repo"
if [[ ! -d "$base" ]]; then
echo "No worktrees found for '$repo' under $base" >&2
return 0
fi
local found=0
for worktree in "$base"/*; do
if [[ -d "$worktree" ]] && [[ -f "$worktree/.gwt-lock" ]]; then
echo "$(basename "$worktree")"
found=1
fi
done
if [[ $found -eq 0 ]]; then
echo "No locked worktrees for '$repo'" >&2
fi
return 0
}
# ============================================================================
# Skip-Worktree Commands
# ============================================================================
# Commands for managing skip-worktree flags on tracked files.
# Use these when you want to ignore local changes to tracked files.
# Mark files to be ignored by git (skip-worktree).
#
# Uses git's skip-worktree feature to tell git to ignore local changes to
# tracked files. Useful for configuration files that differ per worktree.
#
# Usage:
# gwt-ignore <file> [<file2> ...]
#
# Arguments:
# <file> - Path to file(s) to ignore (relative to repo root)
#
# Returns:
# 0 on success, 1 on error
#
# Examples:
# $ gwt-ignore .env.local
# $ gwt-ignore config/settings.json .env.local
#
# NOTE: This is different from .gitignore. These files are tracked in git but
# local changes won't appear in git status or be committed.
gwt-ignore() {
if [[ -z "$1" ]]; then
echo "Usage: gwt-ignore <file> [<file2> ...]" >&2
echo "" >&2
echo "Mark tracked files to ignore local changes (skip-worktree)" >&2
return 1
fi
local repo_root="$(_gwt_repo)" || return 1
local success=0
for file in "$@"; do
if [[ -f "$file" ]]; then
git update-index --skip-worktree "$file" && \
echo "✓ Ignoring: $file" || \
{ echo "✗ Failed to ignore: $file" >&2; success=1; }
else
echo "✗ File not found: $file" >&2
success=1
fi
done
return $success
}
# Unmark files to stop ignoring them (remove skip-worktree).
#
# Removes the skip-worktree flag from files so git will track changes again.
#
# Usage:
# gwt-unignore <file> [<file2> ...]
#
# Arguments:
# <file> - Path to file(s) to unignore (relative to repo root)
#
# Returns:
# 0 on success, 1 on error
#
# Examples:
# $ gwt-unignore .env.local
# $ gwt-unignore config/settings.json .env.local
gwt-unignore() {
if [[ -z "$1" ]]; then
echo "Usage: gwt-unignore <file> [<file2> ...]" >&2
echo "" >&2
echo "Remove skip-worktree flag to track changes again" >&2
return 1
fi
local repo_root="$(_gwt_repo)" || return 1
local success=0
for file in "$@"; do
git update-index --no-skip-worktree "$file" && \
echo "✓ No longer ignoring: $file" || \
{ echo "✗ Failed to unignore: $file" >&2; success=1; }
done
return $success
}
# List all files currently marked as skip-worktree.
#
# Shows which tracked files are being ignored in the current worktree.
#
# Usage:
# gwt-ignored
#
# Returns:
# 0 on success, 1 if not in a git repository
#
# Example:
# $ gwt-ignored
# .env.local
# config/settings.json
#
# NOTE: Files marked with 'S' in git ls-files -v are skip-worktree files.
gwt-ignored() {
local repo_root="$(_gwt_repo)" || return 1
local ignored_files="$(git ls-files -v | grep '^S' | cut -c3-)"
if [[ -z "$ignored_files" ]]; then
echo "No files are currently ignored (skip-worktree)" >&2
return 0
fi
echo "$ignored_files"
}
# ============================================================================
# Local Excludes Commands
# ============================================================================
# Commands for managing .git/info/exclude patterns.
# Use these for untracked patterns that are local to this worktree.
# Add local ignore patterns to .git/info/exclude (worktree-specific).
#
# Appends patterns to the worktree's info/exclude file so they're ignored only
# in this worktree. Unlike .gitignore, these patterns are never committed and
# won't affect other worktrees or clones.
#
# Usage:
# gwt-excludes <pattern> [<pattern2> ...]
#
# Arguments:
# <pattern> - Gitignore-style pattern(s) to ignore locally
#
# Returns:
# 0 on success, 1 on error
#
# Examples:
# $ gwt-excludes prompts-local/
# $ gwt-excludes *.prompt.md .env.local scratch/
#
# Use Cases:
# - Local development files that shouldn't be tracked
# - Per-worktree test data or configuration
# - Personal notes or scratch files
#
# NOTE: Patterns follow .gitignore syntax. Use '/' suffix for directories.
gwt-excludes() {
if [[ -z "$1" ]]; then
echo "Usage: gwt-excludes <pattern> [<pattern2> ...]" >&2
echo "" >&2
echo "Add patterns to .git/info/exclude (local to this worktree)" >&2
echo "" >&2
echo "Examples:" >&2
echo " gwt-excludes prompts-local/" >&2
echo " gwt-excludes *.prompt.md .env.local" >&2
return 1
fi
local repo_root="$(_gwt_repo)" || return 1
# Get the worktree-specific git directory
local git_dir="$(git rev-parse --git-dir)" || return 1
local exclude_file="$git_dir/info/exclude"
# Ensure .git/info directory exists
mkdir -p "$(dirname "$exclude_file")" || {
echo "Error: Failed to create info directory: $(dirname "$exclude_file")" >&2
return 1
}
# Create exclude file if it doesn't exist
if [[ ! -f "$exclude_file" ]]; then
touch "$exclude_file" || {
echo "Error: Failed to create exclude file: $exclude_file" >&2
return 1
}
fi
# Add each pattern
for pattern in "$@"; do
# Check if pattern already exists
if grep -Fxq "$pattern" "$exclude_file" 2>/dev/null; then
echo "⊘ Already ignored: $pattern"
else
echo "$pattern" >> "$exclude_file"
echo "✓ Locally ignoring: $pattern"
fi
done
return 0
}
# List patterns in .git/info/exclude (worktree-specific ignores).
#
# Shows all local ignore patterns for the current worktree.
#
# Usage:
# gwt-excludes-list
#
# Returns:
# 0 on success, 1 if not in a git repository
#
# Example:
# $ gwt-excludes-list
# prompts-local/
# *.prompt.md
# scratch/
gwt-excludes-list() {
local repo_root="$(_gwt_repo)" || return 1
# Get the worktree-specific git directory
local git_dir="$(git rev-parse --git-dir)" || return 1
local exclude_file="$git_dir/info/exclude"
if [[ ! -f "$exclude_file" ]]; then
echo "No local ignore patterns (.git/info/exclude doesn't exist)" >&2
return 0
fi
# Filter out comments and empty lines
grep -v '^#' "$exclude_file" | grep -v '^[[:space:]]*$' || {
echo "No local ignore patterns in .git/info/exclude" >&2
return 0
}
}
# Edit .git/info/exclude file directly.
#
# Opens the worktree's info/exclude file in your default editor for manual editing.
#
# Usage:
# gwt-excludes-edit
#
# Returns:
# 0 on success, 1 if not in a git repository
#
# Example:
# $ gwt-excludes-edit
#
# NOTE: Uses $EDITOR environment variable, falls back to vim.
gwt-excludes-edit() {
local repo_root="$(_gwt_repo)" || return 1
# Get the worktree-specific git directory
local git_dir="$(git rev-parse --git-dir)" || return 1
local exclude_file="$git_dir/info/exclude"
# Ensure .git/info directory exists
mkdir -p "$(dirname "$exclude_file")" || {
echo "Error: Failed to create info directory: $(dirname "$exclude_file")" >&2
return 1
}
# Create with helpful header if it doesn't exist
if [[ ! -f "$exclude_file" ]]; then
cat > "$exclude_file" <<'EOF'
# Local ignore patterns for this worktree
# Patterns here work like .gitignore but are never committed
#
# Examples:
# prompts-local/
# *.prompt.md
# scratch/
# .env.local
EOF
fi
# Open in editor
${EDITOR:-vim} "$exclude_file"
}
# ============================================================================
# Shell Completion
# ============================================================================
# Enable tab completion for worktree commands.
#
# This provides intelligent tab completion for branch names across all gwt
# commands. When you press TAB, it will show available branches from your
# git repository.
#
# Supported commands:
# - gwt
# - gwt-add
# - gwt-rm
# - gwt-open
# - gwt-switch
# - gwt-help
# - gwt-lock (worktree protection)
# - gwt-unlock (worktree protection)
# - gwt-locks (worktree protection)
# - gwt-ignore (skip-worktree)
# - gwt-unignore (skip-worktree)
# - gwt-ignored (skip-worktree)
# - gwt-excludes (.git/info/exclude)
# - gwt-excludes-list (.git/info/exclude)
# - gwt-excludes-edit (.git/info/exclude)
# - gwt-status (status commands)
# - gwt-main (navigation)
# - gwt-clean (bulk operations)
# - gwt-mv (move/rename)
# - gwt-pr (GitHub integration)
# - gwt-diff (comparison)
# - gwt-stash-list (stash management)
# - gwt-archive (archive management)
# - gwt-unarchive (archive management)
# - gwt-archives (archive management)
compdef _gwt_branches gwt gwt-add gwt-rm gwt-open gwt-switch gwt-lock gwt-unlock gwt-archive gwt-unarchive
compdef _gwt_help_completion gwt-help gwt-locks gwt-status gwt-main gwt-stash-list gwt-archives gwt-list gwt-prune gwt-migrate
compdef _files gwt-ignore gwt-unignore gwt-excludes
compdef _gwt_help_completion gwt-ignored gwt-excludes-list gwt-excludes-edit
compdef _gwt_mv_completion gwt-mv
compdef _gwt_diff_completion gwt-diff
compdef _gwt_clean_completion gwt-clean
compdef _gwt_pr_completion gwt-pr
# Completion function that provides branch name suggestions.
#
# Returns:
# List of all local git branches for completion
_gwt_branches() {
local -a branches
branches=(${(f)"$(git for-each-ref --format='%(refname:short)' refs/heads 2>/dev/null)"})
_describe 'branches' branches
}
# Completion function for gwt-help (no completions needed).
_gwt_help_completion() {
# No completions for help command
return 0
}
# Completion function for gwt-mv (two branch arguments).
_gwt_mv_completion() {
local -a branches
branches=(${(f)"$(git for-each-ref --format='%(refname:short)' refs/heads 2>/dev/null)"})
_describe 'branches' branches
}
# Completion function for gwt-diff (two branch arguments).
_gwt_diff_completion() {
local -a branches
branches=(${(f)"$(git for-each-ref --format='%(refname:short)' refs/heads 2>/dev/null)"})
_describe 'branches' branches
}
# Completion function for gwt-clean (flags).
_gwt_clean_completion() {
local -a options
options=(
'--merged:Remove merged branches'
'--older-than:Remove worktrees older than N days'
'--all:Remove all unlocked worktrees'
'--include-archived:Also clean archived worktrees'
'--dry-run:Preview without removing'
)
_describe 'options' options
}
# Completion function for gwt-pr (PR numbers - no completion).
_gwt_pr_completion() {
# Could potentially fetch PR numbers from gh, but that's slow
return 0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment