150+ Stale Branch Cleanup: Repository Hygiene
The repository contained 157 stale branches from features completed months ago, experiments abandoned, and hotfixes long since merged. Running git branch -a produced six pages of output. Finding the current branch you needed required scrolling past dozens of outdated names like feature/2023-01-old-experiment and bugfix/temp-fix-delete-later.
Stale branches create cognitive overhead. Every time a developer needs to check out a branch, push code, or review branch status, they wade through noise. Meaningful branch names disappear in a sea of irrelevant history. We performed a systematic cleanup, deleting 150+ branches while preserving work in progress and documented history.
The Problem: Branch Proliferation
Git branches are cheap to create and easy to forget. A developer creates a feature branch, completes the work, merges to main, and forgets to delete the branch. Six months later, the branch still exists remotely, cluttering the repository.
Branch Growth Pattern:
Month 1: 20 branches
Month 3: 45 branches
Month 6: 89 branches
Month 12: 157 branches
Growth rate: ~12 branches/month created, ~3 branches/month deleted
Net growth: +9 branches/month
At 9 branches per month, the repository gained 100+ branches per year. Without intervention, this growth was unsustainable.
Before: Branch Clutter
Branch List (git branch -a)
┌──────────────────────────────────────┐
│ feature/old-feature-2023-01 │
│ feature/experiment-abandoned │
│ bugfix/issue-123-2023-02 │
│ bugfix/temp-fix-1 │
│ hotfix/prod-2023-03 │
│ develop-backup │
│ test-branch-delete-me │
│ ... (150+ more branches) │
│ feature/current-work │
│ bugfix/active-fix │
└──────────────────────────────────────┘
Hard to find current branches
Developers reported spending 30-60 seconds searching for the right branch. Some resorted to grep:
git branch -a | grep "current-work"
This was a symptom of poor repository hygiene.
Impact on Developer Experience:
- Slow branch discovery - Finding branches requires searching through pages of results
- Namespace pollution - Similar names confuse developers (feature/auth vs feature/auth-v2 vs feature/auth-refactor)
- Accidental checkouts - Developers check out old branches thinking they're current
- Git UI slowness - GUIs (GitKraken, SourceTree) slow down with 100+ branches
- Mental overhead - Every branch list view includes irrelevant information
The problem wasn't just aesthetic—it actively slowed development.
The Solution: Systematic Branch Cleanup
We performed a three-phase cleanup:
Phase 1: Identify and delete merged branches Phase 2: Archive abandoned experimental branches Phase 3: Delete confirmed stale branches
Each phase involved verification to prevent accidental deletion of active work.
After: Clean Branch List
Branch List (Cleaned)
┌──────────────────────────────────────┐
│ main │
│ develop │
│ feature/content-duo │
│ feature/adaptive-learning │
│ bugfix/tts-speech-marks │
│ hotfix/auth-redirect │
└──────────────────────────────────────┘
Only active branches remain
The cleaned repository contained 7 active branches. Running git branch -a produced a single screen of output, making navigation instant.
Implementation Details
Phase 1: Delete Merged Branches
Branches already merged into main or develop are safe to delete—their changes are preserved in the target branch.
Identify merged branches:
# Find branches merged into main
git branch -r --merged origin/main | grep -v "main\|develop"
# Example output:
# origin/feature/old-feature-2023
# origin/bugfix/issue-456
# origin/hotfix/prod-fix-january
This command lists remote branches that have been merged into main. The grep -v excludes main and develop themselves.
Verify before deletion:
Before deleting, we verified each branch:
- Check last commit date:
git log -1 --format="%ai" origin/feature/old-feature-2023 - Verify merge status:
git log main --oneline | grep "feature/old-feature" - Confirm no unique commits:
git log origin/feature/old-feature-2023 ^main --oneline
If a branch had no unique commits (all commits were in main), it was safe to delete.
Delete merged branches:
# Delete remote branch
git push origin --delete feature/old-feature-2023
# Delete local tracking branch
git branch -d feature/old-feature-2023
# Prune stale remote references
git remote prune origin
We created a script to automate this:
scripts/cleanup_merged_branches.sh:
#!/bin/bash
# Cleanup merged branches
MERGED_BRANCHES=$(git branch -r --merged origin/main |
grep -v "main\|develop" |
sed 's/origin\///')
echo "Found $(echo "$MERGED_BRANCHES" | wc -l) merged branches"
echo "$MERGED_BRANCHES"
echo ""
echo "Delete these branches? (y/n)"
read CONFIRM
if [ "$CONFIRM" = "y" ]; then
for BRANCH in $MERGED_BRANCHES; do
echo "Deleting $BRANCH..."
git push origin --delete "$BRANCH"
done
echo "Pruning remote references..."
git remote prune origin
echo "Done! Deleted $(echo "$MERGED_BRANCHES" | wc -l) branches"
fi
This script provided a safe, interactive deletion process.
Phase 1 Results:
- Branches identified: 89
- Branches deleted: 87
- Branches preserved: 2 (appeared merged but had commits not in main—false positives)
Phase 2: Archive Experimental Branches
Some branches represented experiments or spike work—valuable for historical reference but not active. We archived these by tagging them before deletion.
Identify experimental branches:
Branches with names like:
experiment/*spike/*test/*poc/*(proof of concept)
Archive process:
# Create tag preserving branch history
git tag archive/experiment-adaptive-v1 origin/experiment-adaptive-v1
# Push tag to remote
git push origin archive/experiment-adaptive-v1
# Delete branch
git push origin --delete experiment-adaptive-v1
Tags preserve the commit history without cluttering the branch list. Developers can access archived branches via tags:
# List archived branches
git tag -l "archive/*"
# Check out archived branch
git checkout tags/archive/experiment-adaptive-v1
Phase 2 Results:
- Experimental branches identified: 23
- Archived as tags: 23
- Branches deleted: 23
Phase 3: Delete Stale Inactive Branches
The final phase targeted branches that were:
- Not merged
- Not experimental
- Inactive for >6 months
- No clear owner
Identify stale branches:
# Find branches with no commits in last 180 days
for BRANCH in $(git branch -r | grep -v "main\|develop"); do
LAST_COMMIT=$(git log -1 --format="%ai" $BRANCH)
LAST_COMMIT_DATE=$(date -d "$LAST_COMMIT" +%s)
NOW=$(date +%s)
DAYS_AGO=$(( ($NOW - $LAST_COMMIT_DATE) / 86400 ))
if [ $DAYS_AGO -gt 180 ]; then
echo "$BRANCH: $DAYS_AGO days old"
fi
done
Verification process:
For each stale branch:
-
Check for unique commits:
git log origin/feature/old-branch ^origin/main --onelineIf output is empty, branch has no unique work.
-
Review commit messages:
git log origin/feature/old-branch --oneline -10Identify if work was abandoned or moved elsewhere.
-
Check for linked issues/PRs:
# Search commit messages for issue numbers git log origin/feature/old-branch --oneline | grep -E "#[0-9]+"Verify if linked issues are closed.
-
Contact branch owner (if identifiable):
# Find last committer git log origin/feature/old-branch -1 --format="%an <%ae>"Send Slack message asking if branch is still needed.
Deletion process:
After verification, we deleted stale branches:
# For confirmed stale branches
git push origin --delete feature/stale-branch-name
We documented deleted branches in a CSV for future reference:
deleted_branches.csv:
branch_name,last_commit_date,last_committer,deletion_date,reason
feature/old-auth,2023-03-15,john@example.com,2024-02-01,Merged in different branch
bugfix/temp-fix,2023-04-20,jane@example.com,2024-02-01,Temporary fix no longer needed
Phase 3 Results:
- Stale branches identified: 47
- Owners contacted: 12
- Branches preserved after owner feedback: 2
- Branches deleted: 45
Total Cleanup Results
Summary:
- Phase 1 (Merged branches): 87 deleted
- Phase 2 (Experimental branches): 23 archived & deleted
- Phase 3 (Stale branches): 45 deleted
- Total: 155 branches removed
Before vs After:
Branch Count
Before: 157 branches
After: 7 branches (main, develop, 5 active features)
Reduction: 95.5%
Preventing Future Buildup
To prevent branch proliferation, we implemented policies and automation:
Policy 1: Delete After Merge
Updated PR merge checklist:
## PR Merge Checklist
- [ ] All tests pass
- [ ] Code review approved
- [ ] Merge PR
- [ ] **Delete source branch** ✓
GitHub's "Delete branch" button appears after merge—we made clicking it mandatory.
Policy 2: 60-Day Inactive Branch Alert
We configured a GitHub Action to alert on stale branches:
.github/workflows/stale-branches.yml:
name: Stale Branch Alert
on:
schedule:
- cron: '0 0 * * 0' # Weekly on Sunday
jobs:
check-stale-branches:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Find stale branches
run: |
git branch -r | while read BRANCH; do
LAST_COMMIT=$(git log -1 --format="%ai" $BRANCH)
DAYS_AGO=$(( ($(date +%s) - $(date -d "$LAST_COMMIT" +%s)) / 86400 ))
if [ $DAYS_AGO -gt 60 ]; then
echo "::warning::Branch $BRANCH is $DAYS_AGO days old"
fi
done
This action creates GitHub annotations for branches inactive >60 days, reminding developers to clean up.
Policy 3: Branch Naming Convention
We standardized branch names to make purpose clear:
feature/short-description # New features
bugfix/issue-number-desc # Bug fixes
hotfix/critical-issue # Production hotfixes
experiment/spike-name # Experimental work (auto-archive after 30 days)
Clear naming makes it obvious when a branch is outdated.
Automation: Auto-Delete Experimental Branches
We added a GitHub Action to auto-delete experimental branches after 30 days:
.github/workflows/cleanup-experiments.yml:
name: Cleanup Experimental Branches
on:
schedule:
- cron: '0 0 * * *' # Daily
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Archive and delete old experiments
run: |
for BRANCH in $(git branch -r | grep "experiment/"); do
LAST_COMMIT=$(git log -1 --format="%ai" $BRANCH)
DAYS_AGO=$(( ($(date +%s) - $(date -d "$LAST_COMMIT" +%s)) / 86400 ))
if [ $DAYS_AGO -gt 30 ]; then
TAG_NAME="archive/$(echo $BRANCH | sed 's/origin\///')"
git tag $TAG_NAME $BRANCH
git push origin $TAG_NAME
git push origin --delete $(echo $BRANCH | sed 's/origin\///')
echo "Archived and deleted $BRANCH"
fi
done
Experimental branches automatically archive after 30 days, preventing buildup.
Developer Feedback
Before Cleanup:
"Finding the right branch is painful. I spend more time scrolling through old branches than actually coding."
After Cleanup:
"Git operations feel instant now. Everything I see in the branch list is relevant."
On Automation:
"The stale branch alerts are helpful—they remind me to clean up after finishing work."
Metrics
Branch Discovery Time:
Before: 30-60 seconds (scrolling, searching)
After: 2-3 seconds (instant visibility)
Time saved per branch checkout: ~45 seconds
Checkouts per developer per day: ~10
Time saved per developer per day: ~7.5 minutes
Time saved across team (5 developers): ~37.5 min/day
Git Operations Speed:
git fetch --all
Before: 18 seconds
After: 4 seconds
git branch -a
Before: 6 pages of output, slow rendering in terminal
After: 1 screen, instant rendering
Repository Clone Time:
Before: 2 min 15 sec (fetching 157 branch heads)
After: 1 min 30 sec (fetching 7 branch heads)
Improvement: 33% faster
Key Takeaways
- Stale branches are technical debt - They slow down every git operation
- Automate cleanup - Manual cleanup is a one-time win; automation prevents recurrence
- Delete after merge - Make branch deletion part of the PR merge process
- Archive, don't delete blindly - Tags preserve experimental work for historical reference
- Set clear policies - Branch naming and retention policies prevent future buildup
Cleaning up 155 stale branches took two days of work. The immediate benefit—faster git operations and clearer repository navigation—was obvious. The long-term benefit—preventing future buildup through automation and policy—ensures the problem doesn't recur.
Repository hygiene isn't glamorous work, but it's high-leverage. Every developer benefits from a clean branch list every time they run git commands. Over a year, 7.5 minutes saved per developer per day compounds to weeks of recovered productivity. The investment in cleanup and automation pays for itself within weeks.