Skip to content

Instantly share code, notes, and snippets.

@ChristopherA
Last active February 7, 2025 02:07
Show Gist options
  • Save ChristopherA/4643b2f5e024578606b9cd5d2e6815cc to your computer and use it in GitHub Desktop.
Save ChristopherA/4643b2f5e024578606b9cd5d2e6815cc to your computer and use it in GitHub Desktop.
Git Worktree Best Practices and Tools

Git Worktree Best Practices and Tools

Last Updated: 2024-02-06

Overview

Git worktrees allow you to check out multiple branches simultaneously in separate directories, while sharing a single .git metadata store.

THere are some best practices, useful Git aliases, and shell functions for efficiently managing Git worktrees. It covers:

  • Setting up Git aliases for worktree operations.
  • Using shell functions for enhanced worktree management.
  • Automating worktree status and updates.
  • Best practices for keeping worktrees clean.

Example Structure:

~/Documents/Workspace/github/
└── communityORuser/
    ├── githubreponame/
    │   ├── .git/                 # Git metadata (bare repository)
    │   ├── main/                 # Primary branch worktree (main or master)
    │   ├── feature-branch-1/     # Other branches as worktrees
    │   ├── feature-branch-2/

📌 Git Worktree Aliases

These aliases help manage worktrees more efficiently.

1️⃣ Add These to ~/.gitconfig

[alias]
  worktrees = "worktree list"
  prunetrees = "worktree prune"

or

git config --global alias.worktrees "!git worktree list"
git config --global alias.prunetrees "!git worktree prune"

💡 Usage

Alias Command Description
git worktrees git worktree list List all worktrees
git prunetrees git worktree prune Clean up stale worktrees

These aliases work globally in any Git repository

Worktree Shell Functions

Some Git operations, such as creating or switching worktrees, work better with shell functions instead of Git aliases.

Add These Functions to ~/.zshrc

# Quickly switch to a worktree directory
swtree() {
  local dir
  dir=$(git worktree list | grep "$1" | awk '{print $1}')
  if [[ -d "$dir" ]]; then
    cd "$dir" || echo "❌ Worktree not found"
  else
    echo "❌ Worktree '$1' not found"
  fi
}

# Create a new worktree
newtree() {
  if [[ $# -ne 2 ]]; then
    echo "Usage: newtree <directory> <branch>"
    return 1
  fi
  git worktree add "$1" "$2"
}

# Remove a worktree
rmtree() {
  if [[ $# -ne 1 ]]; then
    echo "Usage: rmtree <directory>"
    return 1
  fi
  git worktree remove "$1"
}

# Show status of all worktrees
worktree-status() {
  for dir in $(git worktree list | awk '{print $1}' | tail -n +2); do
    echo "📂 Checking status in: $dir"
    (cd "$dir" && git status --short)
  done
}

Usage

Command Description
swtree feature-branch Switch to a specific worktree
newtree feature-branch feature-branch Create a new worktree
rmtree feature-branch Remove a worktree
worktree-status Show status of all worktrees

Works globally** after adding to ~/.zshrc and running:

source ~/.zshrc

Clone Worktrees using gh

This script clones a GitHub repository using git worktree, organizing main and branches as a tree.

#!/bin/zsh

# -----------------------------------------------------------------------------
# GitHub Repository Clone & Worktree Setup Script (Best Practices)
# -----------------------------------------------------------------------------
#
# This script clones a GitHub repository using `git worktree`, organizing it as:
#
# ~/Documents/Workspace/github/
# ├── communityORuser/             # GitHub user or organization
# │   ├── githubreponame/          # Repository folder
# │   │   ├── .git/                # Git metadata (bare repository)
# │   │   ├── main/                # Primary branch worktree (main or master)
# │   │   ├── feature-branch-1/    # Other branches as worktrees
# │   │   ├── feature-branch-2/
#
# -----------------------------------------------------------------------------
#
# USAGE:
#   Run this script from ~/Documents/Workspace/github/:
#
#   ./clone-worktree.zsh communityORuser/githubreponame
#
# DEPENDENCIES:
#   - Requires `gh` (GitHub CLI) for detecting the default branch
#   - Requires `git` for cloning and worktree operations
#   - Uses `tree` for directory structure (falls back to `ls -A` if missing)
#
# FEATURES:
#   - Automatically detects if the primary branch is `main` or `master`
#   - Clones the repository as a bare repository
#   - Creates worktrees for all branches
#   - Ensures script is run from the correct directory
#   - Errors out if the repository already exists
#   - Provides clear error messages and fallbacks
#
# -----------------------------------------------------------------------------

# Exit immediately if any command fails
set -e

# Ensure GitHub CLI (`gh`) is installed
if ! command -v gh &> /dev/null; then
    echo "❌ GitHub CLI (gh) not found. Please install it first."
    exit 1
fi

# Ensure Git is installed
if ! command -v git &> /dev/null; then
    echo "❌ Git not found. Please install Git first."
    exit 1
fi

# Define the expected working directory
EXPECTED_DIR="$HOME/Documents/Workspace/github"

# Ensure script is run from ~/Documents/Workspace/github/
if [[ "$PWD" != "$EXPECTED_DIR" ]]; then
    echo "❌ Please run this script from $EXPECTED_DIR"
    exit 1
fi

# Check if a repository argument is provided
if [[ -z "$1" ]]; then
    echo "❌ Usage: $0 communityORuser/githubreponame"
    exit 1
fi

# Extract repository details from argument
REPO="$1"
COMMUNITY_OR_USER="${REPO%/*}"  # Extracts 'communityORuser'
REPO_NAME="${REPO##*/}"         # Extracts 'githubreponame'
REPO_DIR="$PWD/$COMMUNITY_OR_USER/$REPO_NAME"

# Ensure the parent directory for the repository exists
mkdir -p "$PWD/$COMMUNITY_OR_USER"

# Check if the repository directory already exists and prevent overwriting
if [[ -d "$REPO_DIR" ]]; then
    echo "❌ Error: Repository $REPO_NAME already exists in $COMMUNITY_OR_USER/"
    exit 1
fi

# Clone the repository as a bare repo inside ".git"
echo "🚀 Cloning $REPO as a bare repository..."
mkdir -p "$REPO_DIR"
git clone --bare "https://github.com/$REPO.git" "$REPO_DIR/.git"

# Change into repo directory
cd "$REPO_DIR" || exit 1

# Fetch all remote branches
echo "📥 Fetching branch list..."
git --git-dir=.git fetch --all --prune

# Determine the primary branch (handles both 'main' and 'master')
echo "🔎 Detecting default branch..."

# First, try using GitHub CLI (preferred method)
DEFAULT_BRANCH=$(gh repo view "$REPO" --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null)

# If GitHub CLI fails, fall back to `git symbolic-ref`
if [[ -z "$DEFAULT_BRANCH" ]]; then
    DEFAULT_BRANCH=$(git --git-dir=.git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')
fi

# If still not found, manually check for 'main' or 'master'
if [[ -z "$DEFAULT_BRANCH" ]]; then
    DEFAULT_BRANCH=$(git --git-dir=.git branch -r | grep -E 'origin/main|origin/master' | head -n 1 | awk -F'/' '{print $2}')
fi

# If still not found, exit with an error
if [[ -z "$DEFAULT_BRANCH" ]]; then
    echo "❌ Error: Could not determine the primary branch (main or master)."
    exit 1
fi

echo "✅ Primary branch detected: $DEFAULT_BRANCH"

# Create the worktree for the primary branch
echo "📂 Creating worktree for main branch: $DEFAULT_BRANCH..."
git worktree add "main" "$DEFAULT_BRANCH"

# List all branches except the default branch
BRANCHES=$(git --git-dir=.git branch -r | grep -v "origin/$DEFAULT_BRANCH" | sed 's/origin\///')

# Create worktrees for each additional branch
for BRANCH in $BRANCHES; do
    echo "📂 Creating worktree for branch: $BRANCH..."
    git worktree add "$BRANCH" "$BRANCH"
done

# Display the directory structure
echo "✅ Worktree setup complete!"
echo "📂 Directory structure:"

# Check if 'tree' command is available; if not, use 'ls -A'
if command -v tree &> /dev/null; then
    tree -a -L 2 "$REPO_DIR"  # -a ensures hidden files (like .git) are shown
else
    echo "⚠️ 'tree' command not found. Showing directory with 'ls -A':"
    ls -A "$REPO_DIR"
fi

Automating Worktree Updates

This script automatically fetches updates and rebases all worktrees.

#!/bin/zsh

# Fetch latest changes
echo "📥 Fetching latest changes from origin..."
git fetch --all --prune

# Get all worktrees
WORKTREES=$(git worktree list | awk '{print $1}' | tail -n +2)

# Iterate over each worktree and update it
for DIR in $WORKTREES; do
    echo "🔄 Updating worktree: $DIR"
    cd "$DIR" || continue
    BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "detached")

    if [[ "$BRANCH" != "detached" ]]; then
        git rebase origin/"$BRANCH"
    fi
    cd - > /dev/null
done

echo "✅ All worktrees updated!"

Usage

Run:

./update-worktrees.zsh

Keeps all worktrees in sync with the latest remote changes.**

📌 Cleaning Up Worktrees

  • Remove a worktree manually:
    git worktree remove feature-branch
  • Automatically clean up old worktrees:
    git worktree prune
  • Prevent worktrees from being tracked: Add this to .gitignore:
    /*
    !.git
    !main/
    !feature-branch-1/
    !feature-branch-2/
    

📌 Final Summary

Feature Command
List all worktrees git worktrees
Create a new worktree newtree feature-branch feature-branch
Switch to a worktree swtree feature-branch
Remove a worktree rmtree feature-branch
Clean up stale worktrees git prunetrees
Update all worktrees ./update-worktrees.zsh
Show worktree status worktree-status
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment