Skip to content

Instantly share code, notes, and snippets.

@mishterk
Last active January 7, 2026 01:28
Show Gist options
  • Select an option

  • Save mishterk/74e434b3c960ec0857872b0a4a7db86f to your computer and use it in GitHub Desktop.

Select an option

Save mishterk/74e434b3c960ec0857872b0a4a7db86f to your computer and use it in GitHub Desktop.
GWT - Git Worktree Management Utilities for AI agent workflows

GWT - Git Worktree Management Utilities

A bash utility for managing git worktrees in a centralized location. Useful for running multiple AI agents on different branches of the same repo, or any workflow where you need isolated working directories.

Features

  • Centralized worktrees - All worktrees stored in one location ($GWT_WORKTREES_ROOT/<repo>/<name>)
  • Auto-navigation - gwt new and gwt cd change to the worktree directory
  • Preview changes - Apply worktree state to main repo for testing (with local server, DB, etc.)
  • Branch cleanup - gwt rm prompts to delete the associated branch
  • AI-friendly - gwt --help provides detailed documentation for AI agents

Installation

1. Download the script

# Create a bin directory if you don't have one
mkdir -p ~/bin

# Download gwt
curl -o ~/bin/gwt https://gist.githubusercontent.com/mishterk/74e434b3c960ec0857872b0a4a7db86f/raw/gwt
chmod +x ~/bin/gwt

2. Add to your shell profile

Add this to your ~/.bashrc, ~/.zshrc, or ~/.bash_profile:

# GWT - Git Worktree Management
export GWT_WORKTREES_ROOT="$HOME/worktrees"
export PATH="$HOME/bin:$PATH"

# Shell function to enable directory changing (gwt cd, gwt new)
gwt() {
  source "$HOME/bin/gwt" "$@"
}

3. Reload your shell

source ~/.bashrc  # or ~/.zshrc

4. Create the worktrees directory

mkdir -p "$GWT_WORKTREES_ROOT"

Usage

# List worktrees for current repo
gwt ls

# Create a new worktree (auto-navigates to it)
gwt new feature-x

# Create worktree with specific branch name
gwt new agent1 my-feature-branch

# Create without navigating
gwt new feature-y --stay

# Navigate to a worktree
gwt cd feature-x

# Open worktree in VS Code (new window)
gwt code feature-x

# Preview worktree state in main repo (for testing)
gwt preview feature-x

# Undo preview
gwt revert

# Merge worktree into main (run from main branch)
gwt merge feature-x

# Hard reset worktree to main (destructive)
gwt reset feature-x

# Remove worktree (prompts to delete branch)
gwt rm feature-x

Preview Modes

gwt preview has two modes:

Default (Replacement)

gwt preview feature-x

Makes your main repo look exactly like the worktree. Use this when you need to test the exact worktree state with your local server, database, etc.

Merge Mode

gwt preview feature-x --merge

Only applies changes the worktree made since it diverged from main. Preserves any changes made to main. May have conflicts if both branches modified the same files.

Typical Workflow

# 1. Create worktree for a feature
gwt new feature-x

# 2. Make changes in the worktree
# ... edit files, commit, etc.

# 3. Go back to main repo
cd /path/to/main/repo  # or: gwt cd main (if you have a 'main' worktree)

# 4. Preview the changes with your local server
gwt preview feature-x
# Test with local server, database, etc.

# 5. Undo the preview
gwt revert

# 6. Merge when ready
gwt merge feature-x

# 7. Clean up
gwt rm feature-x

For AI Agents

Run gwt --help for detailed documentation including all arguments, flags, behaviours, and edge cases. This is designed to give AI agents complete information about how to use each command correctly.

License

MIT - Use freely, no attribution required.

#!/usr/bin/env bash
# GWT - Git Worktree Management Utilities
# https://gist.github.com/[will-be-updated]
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
# Check if GWT_WORKTREES_ROOT is set (required, no default)
_gwt_check_env() {
if [[ -z "${GWT_WORKTREES_ROOT}" ]]; then
echo "Error: GWT_WORKTREES_ROOT environment variable is not set"
echo "Please set it in your shell profile:"
echo " export GWT_WORKTREES_ROOT=\"\$HOME/worktrees\""
return 1
fi
return 0
}
# Get repository name from current directory
_gwt_repo_name() {
basename "$(git rev-parse --show-toplevel)"
}
# Get worktree base path for current repo
_gwt_base_path() {
local repo_name="$(_gwt_repo_name)"
echo "${GWT_WORKTREES_ROOT}/${repo_name}"
}
# Get full path to specific worktree
_gwt_path() {
local name="$1"
echo "$(_gwt_base_path)/${name}"
}
# Check if we're in a git repository
_gwt_check_repo() {
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "Error: Not in a git repository"
return 1
fi
return 0
}
# Check if branch exists
_gwt_branch_exists() {
local branch="$1"
git show-ref --verify --quiet "refs/heads/${branch}"
}
# ============================================================================
# COMMAND FUNCTIONS
# ============================================================================
# Show usage and available commands
gwt_help() {
echo "GWT - Git Worktree Management Utilities"
echo ""
echo "Usage: gwt <command> [arguments]"
echo ""
echo "Commands:"
echo " ls List all worktrees for current repository"
echo " new <name> [branch] Create worktree (branch defaults to worktree/<name>)"
echo " --stay: don't auto-navigate to new worktree"
echo " rm <name> Remove worktree (prompts to delete branch)"
echo " cd <name> Navigate to worktree directory"
echo " code <name> Open worktree in new VS Code window"
echo " preview <name> [--merge] Show worktree state in current directory"
echo " Default: replaces current state with worktree's"
echo " --merge: only apply worktree's changes (preserves main)"
echo " revert Undo preview (restores files + removes added files)"
echo " reset <name> Hard reset worktree branch to main (destructive)"
echo " merge <name> Merge worktree into main (run from main branch)"
echo ""
echo "Use 'gwt --help' for detailed documentation (useful for AI agents)"
echo ""
# Show context if in a git repo
if _gwt_check_repo 2>/dev/null && _gwt_check_env 2>/dev/null; then
local repo_name="$(_gwt_repo_name)"
local base_path="$(_gwt_base_path)"
echo "Current repository: ${repo_name}"
echo "Worktrees location: ${base_path}/"
fi
}
# Detailed help for AI agents and advanced users
gwt_help_detailed() {
cat << 'EOF'
GWT - Git Worktree Management Utilities
=======================================
Manages git worktrees in a centralized location defined by GWT_WORKTREES_ROOT.
Worktrees are stored at: $GWT_WORKTREES_ROOT/<repo-name>/<worktree-name>
COMMANDS
--------
gwt ls
List all worktrees for the current repository.
No arguments or flags.
Runs: git worktree list
gwt new <name> [branch] [--stay]
Create a new worktree.
Arguments:
<name> Required. Directory name for the worktree.
[branch] Optional. Branch to checkout. Defaults to "worktree/<name>".
Flags:
--stay Don't auto-navigate to the new worktree after creation.
Behaviour:
- Creates worktree at $GWT_WORKTREES_ROOT/<repo>/<name>
- If branch exists, checks it out; otherwise creates new branch
- Auto-navigates to new worktree unless --stay is used
gwt rm <name>
Remove a worktree and optionally delete its branch.
Arguments:
<name> Required. Worktree to remove.
Behaviour:
- Prompts for confirmation before removal
- After removal, prompts to delete the associated branch
- If branch has unmerged changes, prompts for force deletion
gwt cd <name>
Navigate to a worktree directory.
Arguments:
<name> Required. Worktree to navigate to.
gwt code <name>
Open a worktree in VS Code.
Arguments:
<name> Required. Worktree to open.
Behaviour:
- Opens in a NEW VS Code window (code -n)
gwt preview <name> [--merge]
Apply worktree state to current directory for testing/review.
Arguments:
<name> Required. Worktree to preview.
Flags:
--merge, -m Use three-way merge mode instead of replacement mode.
Modes:
DEFAULT (two-dot diff):
Makes current directory look EXACTLY like the worktree.
Any files main has that worktree doesn't will appear as deletions.
Use this when you need to test the exact worktree state locally
(e.g., with local server, database, etc.).
--merge (three-dot diff):
Only applies changes the worktree made since it diverged from main.
Preserves any changes made to main since the branch point.
May have conflicts if both branches modified same files.
Uses git apply --3way for conflict resolution.
Requirements:
- Clean working directory (no uncommitted changes)
Cleanup:
- Records added files in .git/gwt-preview-added
- Use 'gwt revert' to undo
gwt revert
Undo changes made by gwt preview.
No arguments or flags.
Behaviour:
- Runs 'git checkout .' to restore modified tracked files
- Removes files that were added by the preview (reads from .git/gwt-preview-added)
- Cleans up the added files record
gwt reset <name>
Hard reset a worktree's branch to main/master.
Arguments:
<name> Required. Worktree to reset.
Behaviour:
- Prompts for confirmation (destructive operation)
- Runs 'git reset --hard main' in the worktree directory
- All committed and uncommitted changes on the worktree branch are lost
- Does NOT affect main branch or the primary repo
gwt merge <name>
Merge a worktree's branch into main.
Arguments:
<name> Required. Worktree to merge.
Requirements:
- Must be run from the main branch (not from a worktree)
- Clean working directory (no uncommitted changes)
Behaviour:
- Standard git merge (fast-forward if possible, merge commit if needed)
- If conflicts occur, must be resolved manually
ENVIRONMENT
-----------
GWT_WORKTREES_ROOT Required. Base directory for all worktrees.
Example: export GWT_WORKTREES_ROOT="$HOME/worktrees"
TYPICAL WORKFLOW
----------------
1. gwt new feature-x # Create worktree, auto-navigate to it
2. (make changes in worktree)
3. gwt cd main # Go back to main repo (or just cd to it)
4. gwt preview feature-x # See exact worktree state in main dir
5. (test with local server/db)
6. gwt revert # Undo preview
7. gwt merge feature-x # Merge when ready
8. gwt rm feature-x # Clean up worktree and branch
EOF
}
# List all worktrees
gwt_list() {
_gwt_check_repo || return 1
_gwt_check_env || return 1
local repo_name="$(_gwt_repo_name)"
echo "Worktrees for repository: ${repo_name}"
git worktree list
}
# Create new worktree
gwt_new() {
# Parse flags
local stay_flag=0
local args=()
# Process arguments to separate flags from positional args
while [[ $# -gt 0 ]]; do
case "$1" in
--stay)
stay_flag=1
shift
;;
*)
args+=("$1")
shift
;;
esac
done
# Extract positional arguments
local name="${args[0]}"
local branch="${args[1]}"
# Validate inputs
if [[ -z "$name" ]]; then
echo "Error: Worktree name is required"
echo "Usage: gwt new <name> [branch] [--stay]"
return 1
fi
_gwt_check_repo || return 1
_gwt_check_env || return 1
# Determine branch name
if [[ -z "$branch" ]]; then
branch="worktree/${name}"
fi
local worktree_path="$(_gwt_path "$name")"
# Check if worktree already exists
if [[ -d "$worktree_path" ]]; then
echo "Error: Worktree '${name}' already exists at: ${worktree_path}"
return 1
fi
echo "Creating worktree '${name}'..."
# Create the base directory if it doesn't exist
local base_path="$(_gwt_base_path)"
mkdir -p "$base_path"
# Check if branch exists
local branch_action=""
if _gwt_branch_exists "$branch"; then
branch_action="Checked out existing branch"
git worktree add "$worktree_path" "$branch"
else
branch_action="Created new branch"
git worktree add -b "$branch" "$worktree_path"
fi
if [[ $? -eq 0 ]]; then
echo "✓ ${branch_action}: ${branch}"
echo "✓ Path: ${worktree_path}"
echo "✓ Success! Worktree '${name}' ready on branch '${branch}'"
# Add post-action to change directory (unless --stay flag is used)
if [[ $stay_flag -eq 0 ]]; then
local temp_file="/tmp/gwt-postaction-$$"
echo "cd:${worktree_path}" > "$temp_file"
fi
else
echo "✗ Failed to create worktree"
return 1
fi
}
# Remove worktree
gwt_remove() {
local name="$1"
if [[ -z "$name" ]]; then
echo "Error: Worktree name is required"
echo "Usage: gwt rm <name>"
return 1
fi
_gwt_check_repo || return 1
_gwt_check_env || return 1
local worktree_path="$(_gwt_path "$name")"
# Check if worktree exists
if [[ ! -d "$worktree_path" ]]; then
echo "Error: Worktree '${name}' not found at: ${worktree_path}"
return 1
fi
# Get branch name from worktree
local branch=$(cd "$worktree_path" && git branch --show-current)
echo "Removing worktree '${name}'"
echo "Path: ${worktree_path}"
echo "Branch: ${branch}"
# Prompt for confirmation
read -p "Are you sure? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
git worktree remove "$worktree_path"
if [[ $? -eq 0 ]]; then
echo "✓ Removed worktree '${name}'"
# Offer to delete the branch as well
read -p "Also delete branch '${branch}'? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
# Try safe delete first
if git branch -d "$branch" 2>/dev/null; then
echo "✓ Deleted branch '${branch}'"
else
# Branch has unmerged changes
echo "Branch '${branch}' has unmerged changes"
read -p "Force delete anyway? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
git branch -D "$branch"
echo "✓ Force deleted branch '${branch}'"
else
echo "Branch '${branch}' kept"
fi
fi
fi
else
echo "✗ Failed to remove worktree"
return 1
fi
else
echo "Cancelled"
return 0
fi
}
# Change to worktree directory
gwt_cd() {
local name="$1"
if [[ -z "$name" ]]; then
echo "Error: Worktree name is required"
echo "Usage: gwt cd <name>"
return 1
fi
_gwt_check_repo || return 1
_gwt_check_env || return 1
local worktree_path="$(_gwt_path "$name")"
# Check if worktree exists
if [[ ! -d "$worktree_path" ]]; then
echo "Error: Worktree '${name}' not found at: ${worktree_path}"
return 1
fi
# Use post-action protocol
local temp_file="/tmp/gwt-postaction-$$"
echo "cd:${worktree_path}" > "$temp_file"
}
# Preview worktree changes in current directory
gwt_preview() {
# Parse flags
local merge_mode=0
local name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--merge|-m)
merge_mode=1
shift
;;
*)
name="$1"
shift
;;
esac
done
if [[ -z "$name" ]]; then
echo "Error: Worktree name is required"
echo "Usage: gwt preview <name> [--merge]"
echo " --merge Use three-way merge (preserves main's changes)"
return 1
fi
_gwt_check_repo || return 1
_gwt_check_env || return 1
local worktree_path="$(_gwt_path "$name")"
# Check if worktree exists
if [[ ! -d "$worktree_path" ]]; then
echo "Error: Worktree '${name}' not found at: ${worktree_path}"
return 1
fi
# Safety check: require clean working directory
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Error: Working directory has uncommitted changes"
echo "Please commit or stash your changes first"
return 1
fi
# Get branch names
local branch=$(cd "$worktree_path" && git branch --show-current)
local current_branch=$(git branch --show-current)
# Store in .git so it's repo-specific
local git_dir=$(git rev-parse --git-dir)
local added_files_record="${git_dir}/gwt-preview-added"
if [[ $merge_mode -eq 1 ]]; then
# Three dots: merge-style (only worktree's changes since divergence)
echo "Applying changes from '${branch}' (merge mode)..."
git diff "${current_branch}...${branch}" --name-status | grep "^A" | cut -f2 > "$added_files_record"
if git diff "${current_branch}...${branch}" | git apply --3way; then
echo "✓ Changes from '${name}' applied (merge mode)"
echo " Use 'gwt revert' to undo"
else
echo "✗ Failed to apply changes"
echo " Check for conflict markers (<<<<<<) in affected files"
echo " Use 'gwt revert' to undo partial changes"
return 1
fi
else
# Two dots: replacement-style (make main look exactly like worktree)
echo "Applying '${branch}' state to current directory..."
git diff "${current_branch}..${branch}" --name-status | grep "^A" | cut -f2 > "$added_files_record"
if git diff "${current_branch}..${branch}" | git apply; then
echo "✓ Now showing '${name}' state"
echo " Use 'gwt revert' to restore main"
else
echo "✗ Failed to apply changes"
rm -f "$added_files_record"
return 1
fi
fi
}
# Revert uncommitted changes (undo preview)
gwt_revert() {
_gwt_check_repo || return 1
# Store in .git so it's repo-specific
local git_dir=$(git rev-parse --git-dir)
local added_files_record="${git_dir}/gwt-preview-added"
local has_changes=0
local has_added_files=0
# Check for tracked changes
if ! git diff --quiet; then
has_changes=1
fi
# Check for added files from preview
if [[ -f "$added_files_record" ]] && [[ -s "$added_files_record" ]]; then
has_added_files=1
fi
if [[ $has_changes -eq 0 ]] && [[ $has_added_files -eq 0 ]]; then
echo "Nothing to revert - working directory is clean"
return 0
fi
echo "Reverting uncommitted changes..."
# Revert tracked file changes
if [[ $has_changes -eq 1 ]]; then
git checkout .
fi
# Remove files that were added by the preview
if [[ $has_added_files -eq 1 ]]; then
while IFS= read -r file; do
if [[ -f "$file" ]]; then
rm "$file"
echo " Removed: $file"
fi
done < "$added_files_record"
rm -f "$added_files_record"
fi
echo "✓ Working directory restored"
}
# Hard reset worktree branch to main
gwt_reset() {
local name="$1"
if [[ -z "$name" ]]; then
echo "Error: Worktree name is required"
echo "Usage: gwt reset <name>"
return 1
fi
_gwt_check_repo || return 1
_gwt_check_env || return 1
local worktree_path="$(_gwt_path "$name")"
# Check if worktree exists
if [[ ! -d "$worktree_path" ]]; then
echo "Error: Worktree '${name}' not found at: ${worktree_path}"
return 1
fi
local branch=$(cd "$worktree_path" && git branch --show-current)
# Get main branch name (main or master)
local main_branch
if git show-ref --verify --quiet refs/heads/main; then
main_branch="main"
elif git show-ref --verify --quiet refs/heads/master; then
main_branch="master"
else
echo "Error: Could not find main or master branch"
return 1
fi
echo "This will hard reset '${branch}' to '${main_branch}'"
echo "All uncommitted and committed changes on '${branch}' will be lost"
read -p "Continue? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
# Navigate to worktree and perform hard reset
(cd "$worktree_path" && git reset --hard "$main_branch")
echo "✓ Reset '${branch}' to '${main_branch}'"
else
echo "Cancelled"
fi
}
# Merge worktree branch into main
gwt_merge() {
local name="$1"
if [[ -z "$name" ]]; then
echo "Error: Worktree name is required"
echo "Usage: gwt merge <name>"
return 1
fi
_gwt_check_repo || return 1
_gwt_check_env || return 1
local worktree_path="$(_gwt_path "$name")"
# Check if worktree exists
if [[ ! -d "$worktree_path" ]]; then
echo "Error: Worktree '${name}' not found at: ${worktree_path}"
return 1
fi
local branch=$(cd "$worktree_path" && git branch --show-current)
local current_branch=$(git branch --show-current)
# Get main branch name
local main_branch
if git show-ref --verify --quiet refs/heads/main; then
main_branch="main"
elif git show-ref --verify --quiet refs/heads/master; then
main_branch="master"
else
echo "Error: Could not find main or master branch"
return 1
fi
# Check we're on main branch
if [[ "$current_branch" != "$main_branch" ]]; then
echo "Error: Must be on '${main_branch}' branch to merge"
echo "Currently on: ${current_branch}"
return 1
fi
# Check for clean working directory
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Error: Working directory has uncommitted changes"
return 1
fi
echo "Merging '${branch}' into '${main_branch}'..."
if git merge "$branch"; then
echo "✓ Merged '${branch}' into '${main_branch}'"
else
echo "✗ Merge failed - resolve conflicts manually"
return 1
fi
}
# Open worktree in VS Code
gwt_code() {
local name="$1"
if [[ -z "$name" ]]; then
echo "Error: Worktree name is required"
echo "Usage: gwt code <name>"
return 1
fi
_gwt_check_repo || return 1
_gwt_check_env || return 1
local worktree_path="$(_gwt_path "$name")"
# Check if worktree exists
if [[ ! -d "$worktree_path" ]]; then
echo "Error: Worktree '${name}' not found at: ${worktree_path}"
return 1
fi
echo "Opening worktree '${name}' in VS Code..."
echo "Path: ${worktree_path}"
code -n "$worktree_path"
}
# ============================================================================
# MAIN DISPATCHER
# ============================================================================
# Get command
cmd="$1"
shift
# Route to appropriate function
case "$cmd" in
""|"help")
gwt_help
;;
"--help")
gwt_help_detailed
;;
"ls")
gwt_list "$@"
;;
"new")
gwt_new "$@"
# Process post-actions for new command (when sourced)
temp_file="/tmp/gwt-postaction-$$"
if [[ -f "$temp_file" ]]; then
while IFS=':' read -r action value; do
case "$action" in
"cd")
if [[ -d "$value" ]]; then
cd "$value" || echo "Failed to navigate to: $value"
else
echo "Directory not found: $value"
fi
;;
esac
done < "$temp_file"
rm "$temp_file"
fi
;;
"rm")
gwt_remove "$@"
;;
"cd")
gwt_cd "$@"
# Process post-actions for cd command (when sourced)
temp_file="/tmp/gwt-postaction-$$"
if [[ -f "$temp_file" ]]; then
while IFS=':' read -r action value; do
case "$action" in
"cd")
if [[ -d "$value" ]]; then
cd "$value" || echo "Failed to navigate to: $value"
else
echo "Directory not found: $value"
fi
;;
esac
done < "$temp_file"
rm "$temp_file"
fi
;;
"code")
gwt_code "$@"
;;
"preview")
gwt_preview "$@"
;;
"revert")
gwt_revert "$@"
;;
"reset")
gwt_reset "$@"
;;
"merge")
gwt_merge "$@"
;;
*)
echo "Error: Unknown command '${cmd}'"
echo ""
gwt_help
exit 1
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment