Last active
October 1, 2025 15:39
-
-
Save LinnJS/76ead4666b94307ba8e917bd8dcc21e8 to your computer and use it in GitHub Desktop.
Complete developer experience toolkit for git worktrees.
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
| # ============================================================================ | |
| # 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