gary.info

here be dragons

Git Worktrees but Why?

gitworktrees.md

Git Worktrees: How We Replaced Our Entire Branch Management Strategy With a Feature Nobody Uses

We had 14 developers, 3 active releases, and a monorepo that took 12 minutes to clone. Every context switch was killing us. That's when someone suggested using Git's most ignored feature as our entire development workflow.

Here's how we've been running our 400GB monorepo with instant context switching for 18 months—using a Git feature so obscure that most developers don't know it exists.

The Impossible Situation

Picture this: Critical production bug comes in. You're 800 lines deep into a refactoring. The junior dev who could handle it is knee-deep in their own feature branch. The senior who knows that code is reviewing a PR.

What Everyone Said We Needed

# Option 1: The Stash Dance
git stash save "WIP: refactoring auth system"
git checkout hotfix/critical-bug
# ... fix bug ...
git checkout feature/auth-refactor
git stash pop
# Merge conflicts. Always merge conflicts.

# Option 2: Multiple Clones
cd ~/projects/megacorp-app-clone-1
cd ~/projects/megacorp-app-clone-2
cd ~/projects/megacorp-app-clone-3
# 36 minutes and 1.2TB later...

# Option 3: Fancy Branch Management Tool
Price: $49/developer/month
Setup time: 2 weeks
Learning curve: "It's intuitive!" (narrator: it wasn't)

What We Had

Repository size: 400GB
Clone time: 12 minutes (on a good day)
Developers constantly switching contexts: 14
Budget for tools: $0
Patience for complicated workflows: Also $0

The Stupid Idea That Wasn't

Thursday, 3:47 PM. We're in the war room. Someone's explaining their elaborate branch-switching workflow involving three stashes, a WIP commit, and something they call "the prayer."

That's when Jake, our most junior developer, asks: "Why don't we just use worktrees?"

Silence.

"You know, git worktree. It's like having multiple checkouts but they share the same Git objects."

More silence.

"I used it for my side project..."

First Attempt: Proof of Insanity

# Jake's demo
cd megacorp-app
git worktree add ../megacorp-hotfix hotfix/payment-bug

# Everyone: "Wait, that's it?"
cd ../megacorp-hotfix
# Full checkout. No stashing. No commits. No conflicts.
# My changes still safe in the other directory.

Output: Preparing worktree (checking out 'hotfix/payment-bug')

Time elapsed: 3 seconds

The "Wait, What?" Moment

We ran the numbers:

  • Full clone: 12 minutes
  • Worktree creation: 3 seconds
  • Disk space for second clone: 400GB
  • Disk space for worktree: 2.1GB (just the working files)
  • The worktree was using the same .git objects. No duplication. Full Git functionality. Instant switching.

    Building the Production Monstrosity

    Version 1: Held Together With String

    Our first attempt was naive:

    #!/bin/bash
    # worktree-switch.sh v1
    BRANCH=$1
    WORKTREE_DIR="../megacorp-$BRANCH"
    git worktree add "$WORKTREE_DIR" "$BRANCH"
    cd "$WORKTREE_DIR"

    Failed spectacularly when someone tried to check out feature/updates/2023/q4/final-final-v2-actually-final.

    Version 2: Addressing the Obvious Problems

    #!/bin/bash
    # worktree-manager.sh v2
    BRANCH=$1
    SAFE_NAME=$(echo "$BRANCH" | sed 's/\//-/g' | cut -c1-50)
    WORKTREE_BASE="$HOME/worktrees/megacorp"
    WORKTREE_DIR="$WORKTREE_BASE/$SAFE_NAME"
    
    # Check if already exists
    if [ -d "$WORKTREE_DIR" ]; then
        echo "Worktree exists, switching to it..."
        cd "$WORKTREE_DIR"
        exit 0
    fi
    
    # Create new worktree
    git worktree add "$WORKTREE_DIR" "$BRANCH" || {
        # Branch doesn't exist, create it
        git worktree add -b "$BRANCH" "$WORKTREE_DIR"
    }
    
    cd "$WORKTREE_DIR"

    This worked until someone created 47 worktrees and forgot about them.

    Version 3: The Thing That's Still Running

    #!/bin/bash
    # wt (because we're too lazy to type worktree)
    # 18 months in production and counting
    
    BRANCH=${1:-$(git branch --show-current)}
    WORKTREE_BASE="$HOME/worktrees/$(basename $(git rev-parse --show-toplevel))"
    SAFE_NAME=$(echo "$BRANCH" | sed 's/\//-/g' | cut -c1-50)
    WORKTREE_DIR="$WORKTREE_BASE/$SAFE_NAME"
    
    # Prune dead worktrees first
    git worktree prune
    
    # List existing worktrees with this branch
    EXISTING=$(git worktree list --porcelain | grep -B2 "branch refs/heads/$BRANCH" | grep worktree | cut -d' ' -f2)
    
    if [ -n "$EXISTING" ]; then
        echo "→ Switching to existing worktree"
        cd "$EXISTING"
        exec $SHELL
    fi
    
    # Create new worktree
    mkdir -p "$WORKTREE_BASE"
    echo "→ Creating new worktree for $BRANCH"
    
    if git show-ref --verify --quiet "refs/heads/$BRANCH"; then
        git worktree add "$WORKTREE_DIR" "$BRANCH"
    else
        git worktree add -b "$BRANCH" "$WORKTREE_DIR"
    fi
    
    # Setup hooks
    cp .git/hooks/* "$WORKTREE_DIR/.git" 2>/dev/null || true
    
    # Install dependencies if needed
    cd "$WORKTREE_DIR"
    if [ -f package.json ]; then
        echo "→ Installing dependencies..."
        npm ci --silent
    fi
    
    exec $SHELL

    Then we added the secret sauce—automatic cleanup:

    # In everyone's .zshrc/.bashrc
    alias wtclean='git worktree list | grep -v "$(pwd)" | cut -d" " -f1 | \
        xargs -I {} sh -c "[ ! -d {} ] && echo Removing {} && git worktree remove {}" 2>/dev/null'
    
    # Cron job that runs nightly
    0 2 * * * find ~/worktrees -mindepth 2 -maxdepth 2 -type d -mtime +30 \
        -exec sh -c 'cd {} && git worktree remove {} --force' \; 2>/dev/null

    The Numbers Don't Lie

    After 18 months, here's what our metrics look like:

    Performance

  • Context switch time before: 2-5 minutes (stash, checkout, npm install)
  • Context switch time after: 3 seconds
  • Daily context switches per developer: ~12
  • Time saved per developer per day: 48 minutes
  • Disk Usage

  • Traditional approach (3 clones): 1.2TB per developer
  • Worktree approach (main + 5 active): 410GB per developer
  • Shared Git objects: 398GB (counted only once)
  • Storage saved: 66%
  • Reliability

  • Stash conflicts per week: 14-20
  • WIP commits accidentally pushed: 3-5 per month
  • Lost work from bad stash pops: "It happens"
  • Issues with worktree approach: 0
  • The Unexpected Benefits

  • Parallel CI Testing
  • # Run tests on multiple branches simultaneously
    for branch in feature/auth feature/payments feature/ui; do
        (cd ~/worktrees/megacorp/$branch && npm test) &
    done
    wait

  • Instant PR Reviews
  • # Reviewing PRs became trivial
    wt feature/colleague-branch
    # Full IDE, full build, full context. No stashing required.

  • A/B Testing Performance
  • # Compare performance between branches
    cd ~/worktrees/megacorp/main && npm run benchmark > main.bench
    cd ~/worktrees/megacorp/feature-optimization && npm run benchmark > feature.bench
    diff main.bench feature.bench

    Why This Actually Makes Sense

    Turns out Git was designed for this all along. We just never noticed.

    The Hidden Architecture of Git Worktrees

    Git stores all objects in .git/objects. Worktrees don't duplicate this—they create a lightweight .git file pointing to the main repository:

    # In a worktree's .git file:
    gitdir: /home/dev/megacorp/.git/worktrees/feature-auth

    This means:

  • All worktrees share the same object database
  • Commits in any worktree are immediately visible to all others
  • Branches can't be checked out in multiple places (Git prevents conflicts)
  • Perfect cache utilization (objects loaded once, used everywhere)
  • What Traditional Workflows Get Wrong

    The stash/checkout dance assumes your work is interruptible. It's not. You're holding 47 things in your head, and git stash just added number 48.

    Multiple clones solve the wrong problem. You don't need multiple repositories—you need multiple working directories sharing the same repository.

    The Pattern: Tools Already Solve Problems We Haven't Recognized

    This isn't just about Git worktrees. It's about recognizing that:

  • Built-in features often outperform third-party solutions
  • The "obvious" workflow might be the wrong workflow
  • Junior developers sometimes see solutions seniors miss
  • Other Places This Thinking Applies

  • Using SQLite's WAL mode as a message queue (we do this too)
  • Treating nginx as a programmable CDN
  • DNS as a service discovery mechanism
  • File system hard links as a deployment strategy
  • Try This In Your Architecture

    Before you implement that complex branch management strategy, ask:

  • What does Git already provide?
  • Why isn't everyone using it? (Usually: they don't know it exists)
  • What would happen if you used it as your primary workflow?
  • The Worktree Checklist

  • [ ] Is your repo over 1GB?
  • [ ] Do developers switch contexts more than twice daily?
  • [ ] Are you using stash/WIP commits as a workflow?
  • [ ] Do you have disk space for exactly one more copy of working files?
If you answered yes to any of these, you need worktrees.

Your Mission

# Right now. Don't overthink it.
git worktree add ../testing-worktrees main
cd ../testing-worktrees

# Notice how all your Git commands just work
git log --oneline -10
git remote -v
git status

# Create another one
git worktree add ../another-test feature/your-branch

# List them all
git worktree list

# Be amazed that this was always there

The Confession

Is using Git worktrees for everything a best practice? The Git documentation barely mentions them.

Has it eliminated context-switching friction for 14 developers for 18 months? Absolutely.

Would I go back to the stash/checkout dance? Looks at the 48 minutes saved per day. Looks at zero merge conflicts from stash pops.

Not even if you paid me.


P.S. - Three months after implementing this, Jake (remember Jake?) used the time saved to build our entire deployment pipeline using Git hooks and hard links. That's a story for next week.

What Git features are you not using because nobody told you they existed? What's your most successful "I can't believe this was here all along" discovery?

Next week: How we replaced our entire deployment pipeline with creative abuse of symbolic links and Git hooks. Spoiler: It's 100x faster than our previous "proper" solution.