A powerful CLI tool to safely clean up merged and stale git branches with interactive selection and comprehensive safety features.
- Overview
- Features
- Quick Start
- Installation
- Usage
- Configuration
- Common Use Cases
- CI/CD Integration
- Exit Codes
- Filtering Logic
- Safety Features
- Troubleshooting
- FAQ
- Development
- Contributing
- License
branch-clean helps you maintain a clean git repository by identifying and removing branches that have been merged or are no longer actively developed. Unlike simple git commands, branch-clean provides:
- Smart detection of merged branches (handles squash and rebase merges)
- Interactive selection with visual feedback
- Safety checks to prevent accidental deletion of important branches
- Automation support for CI/CD pipelines
- Configuration management for team consistency
Perfect for developers working on large projects with many feature branches, or teams maintaining multiple repositories.
- Accurate Merge Detection: Uses
git merge-base --is-ancestorto correctly identify merged branches- ✅ Regular merge commits
- ✅ Squash merges
- ✅ Rebase merges
- ✅ All git merge strategies
- Automatic Default Branch Detection: Intelligently detects the default branch from remote HEAD
- Stale Branch Detection: Identifies branches with no activity based on configurable age threshold
- Multi-Select Interface: Use checkboxes to select multiple branches at once
- Visual Feedback: Color-coded status indicators (merged/stale/active)
- Confirmation Prompts: Review selections before deletion
- Dry-Run Mode: Preview changes without making any modifications
- Protected Branches: Glob pattern matching prevents deletion of critical branches
- Current Branch Protection: Cannot delete the branch you're currently on
- Default Branch Protection: Prevents accidental deletion of main/master
- Detailed Error Reporting: Clear feedback when operations fail
- Deletion Summary: Shows count of successful vs failed deletions
- Non-Interactive Flags:
--forceand--yesfor CI/CD pipelines - JSON Output: Machine-readable format for scripting
- Configuration Files: Team-wide defaults via YAML config
- Proper Exit Codes: Script-friendly error handling
- Remote Deletion: Clean up both local and remote branches
- Table Format: Human-readable colorized tables (default)
- JSON Format: Structured data for parsing and automation
- Verbose Mode: Detailed operation logging for debugging
# Install
go install github.com/onamfc/branch-clean@latest
# Navigate to your git repository
cd /path/to/your/repo
# See what branches can be cleaned (dry-run)
branch-clean --dry-run
# Interactive cleanup of merged branches
branch-clean --merged-only
# Non-interactive cleanup of stale branches (30+ days old)
branch-clean --stale-only --force
# List all branches with status information
branch-clean listgo install github.com/onamfc/branch-clean@latestThis installs the latest version directly from GitHub. Ensure $GOPATH/bin or $HOME/go/bin is in your PATH.
# Clone the repository
git clone https://github.com/onamfc/branch-clean.git
cd branch-clean
# Download dependencies
go mod download
# Build the binary
go build -o branch-clean
# Install to your PATH (optional)
sudo mv branch-clean /usr/local/bin/
# Or build with version information
go build -ldflags "-X main.version=1.0.0" -o branch-clean(Coming soon: Download pre-built binaries from GitHub Releases)
branch-clean version# Interactive mode (default)
branch-clean
# List branches with status
branch-clean list
# Show version information
branch-clean version
# Get help
branch-clean --help
branch-clean list --helpThe default interactive mode provides a visual interface for branch selection:
branch-cleanHow it works:
- Shows all merged/stale branches with status indicators
- Use
↑/↓arrow keys to navigate - Press
Enterto toggle selection (checkbox appears) - Navigate to "Confirm selection" and press
Enter - Review the summary and confirm deletion
Example output:
Select branches to delete (↑/↓ to navigate, enter to toggle/confirm)
→ [✓] feature/old-implementation [merged]
[ ] bugfix/login-issue [stale]
[✓] feature/deprecated-api [merged]
✓ Confirm selection
You are about to delete 2 branch(es):
- feature/old-implementation
- feature/deprecated-api
Continue [y/N]: y
✓ Deleted local branch feature/old-implementation
✓ Deleted local branch feature/deprecated-api
Deleted 2 of 2 branches
View all branches with detailed status information:
# Table format (default)
branch-clean list
# JSON format for scripting
branch-clean list --format json
# Filter to specific branch types
branch-clean list --merged-only
branch-clean list --stale-onlyExample table output:
Branch Status Age Last Commit
--------------------------------------------------------------------------------
feature/user-authentication merged 15 days ago 2026-01-24
bugfix/memory-leak stale 45 days ago 2025-12-24
feature/api-v2 active 2 days ago 2026-02-06
Example JSON output:
[
{
"name": "feature/user-authentication",
"is_merged": true,
"is_stale": false,
"last_commit": "2026-01-24T10:30:00Z",
"protected": false
},
{
"name": "bugfix/memory-leak",
"is_merged": false,
"is_stale": true,
"last_commit": "2025-12-24T14:20:00Z",
"protected": false
}
]| Flag | Short | Default | Description |
|---|---|---|---|
--dry-run |
-d |
false |
Preview changes without deleting branches |
--stale-days |
-s |
30 |
Days since last commit to consider branch stale |
--protect |
-p |
main, master, develop, release/* |
Protected branch patterns (glob) |
--merged-only |
-m |
false |
Only show/delete merged branches |
--stale-only |
false |
Only show/delete stale branches | |
--verbose |
-v |
false |
Enable verbose output |
--force |
-f |
false |
Skip confirmation prompt |
--yes |
-y |
false |
Auto-answer yes to all prompts |
--remote |
false |
Also delete branches from remote (origin) |
| Flag | Default | Description |
|---|---|---|
--format |
table |
Output format: table or json |
Create ~/.branch-clean.yaml to set default values:
# Number of days before a branch is considered stale
stale_days: 60
# Protected branch patterns (glob syntax)
protected:
- main
- master
- develop
- staging
- production
- release/*
- hotfix/*
- feature/important-*Settings are applied in this order (highest priority first):
- Command-line flags (e.g.,
--stale-days 90) - Configuration file (
~/.branch-clean.yaml) - Built-in defaults (stale_days: 30, protected: main, master, develop, release/*)
Share a config template with your team:
# Create team configuration
cat > ~/.branch-clean.yaml <<EOF
stale_days: 45
protected:
- main
- develop
- staging
- release/*
- hotfix/*
EOF
# Team members can override specific settings
branch-clean --stale-days 30 # Override just stale_days# Interactive - review before deleting
branch-clean --merged-only
# Non-interactive - for scripts
branch-clean --merged-only --force# See what would be deleted
branch-clean --stale-only --stale-days 90 --dry-run
# Delete after review
branch-clean --stale-only --stale-days 90# Preview changes
branch-clean --merged-only --remote --dry-run
# Execute cleanup
branch-clean --merged-only --remote --force# Only delete branches that are both merged AND stale
branch-clean --merged-only --stale-only --stale-days 60# Export to JSON for analysis
branch-clean list --format json > branches.json
# Parse with jq
branch-clean list --format json | jq '.[] | select(.is_stale == true) | .name'
# Count merged branches
branch-clean list --format json | jq '[.[] | select(.is_merged == true)] | length'# Protect all branches starting with "prod-"
branch-clean --protect "main" --protect "prod-*" --protect "release/*"
# Or add to config file
echo "protected:
- main
- prod-*
- release/*" > ~/.branch-clean.yaml# See detailed operation logs
branch-clean --merged-only --verbosename: Cleanup Stale Branches
on:
schedule:
- cron: '0 0 * * 0' # Every Sunday at midnight
workflow_dispatch: # Allow manual trigger
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history for all branches
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Install branch-clean
run: go install github.com/onamfc/branch-clean@latest
- name: Cleanup merged branches
run: |
branch-clean --merged-only --force --remote
env:
GIT_AUTHOR_NAME: 'GitHub Actions'
GIT_AUTHOR_EMAIL: 'actions@github.com'cleanup_branches:
stage: maintenance
image: golang:1.21
only:
- schedules
script:
- go install github.com/onamfc/branch-clean@latest
- branch-clean --merged-only --force
when: manualpipeline {
agent any
triggers {
cron('0 0 * * 0') // Weekly on Sunday
}
stages {
stage('Cleanup Branches') {
steps {
script {
sh '''
go install github.com/onamfc/branch-clean@latest
branch-clean --merged-only --stale-only --stale-days 60 --force
'''
}
}
}
}
post {
failure {
mail to: 'team@example.com',
subject: "Branch cleanup failed: ${env.JOB_NAME}",
body: "Check ${env.BUILD_URL} for details"
}
}
}# Add to crontab: crontab -e
# Run every Sunday at 2 AM
0 2 * * 0 cd /path/to/repo && branch-clean --merged-only --force >> /var/log/branch-clean.log 2>&1# .git/hooks/pre-push
#!/bin/bash
# Remind about stale branches before push
STALE_COUNT=$(branch-clean list --stale-only --format json | jq 'length')
if [ "$STALE_COUNT" -gt 0 ]; then
echo "⚠️ You have $STALE_COUNT stale branches. Consider running 'branch-clean' to clean up."
fibranch-clean uses standard exit codes for integration with scripts and automation:
| Exit Code | Meaning | Description |
|---|---|---|
0 |
Success | All operations completed successfully |
1 |
General Error | Git errors, validation failures, file I/O errors, etc. |
2 |
Protected Branch | Attempted to delete protected, current, or default branch |
#!/bin/bash
# Basic error handling
branch-clean --merged-only --force
if [ $? -ne 0 ]; then
echo "ERROR: Branch cleanup failed"
exit 1
fi
# Detailed error handling
branch-clean --merged-only --force
EXIT_CODE=$?
case $EXIT_CODE in
0)
echo "✓ Cleanup successful"
;;
1)
echo "✗ Cleanup failed with errors"
exit 1
;;
2)
echo "⚠ Attempted to delete protected branch"
exit 1
;;
*)
echo "✗ Unknown error"
exit 1
;;
esac
# Continue with next steps...Understanding how --merged-only and --stale-only flags interact:
| Flags | Behavior |
|---|---|
| None | Shows branches that are merged OR stale (excludes active branches) |
--merged-only |
Shows only merged branches |
--stale-only |
Shows only stale branches |
--merged-only --stale-only |
Shows branches that are both merged AND stale |
# Scenario 1: No filters - shows merged OR stale
branch-clean
# Shows: merged branches + stale branches
# Excludes: active unmerged branches
# Scenario 2: Merged only
branch-clean --merged-only
# Shows: only merged branches
# Excludes: stale but unmerged, active branches
# Scenario 3: Stale only
branch-clean --stale-only --stale-days 60
# Shows: only branches with no commits in 60+ days
# Excludes: merged but recent, active branches
# Scenario 4: Both filters (strictest)
branch-clean --merged-only --stale-only --stale-days 90
# Shows: only branches that are BOTH merged AND 90+ days old
# Excludes: everything else (safest option)Branches are protected if they match any pattern:
# Default protected patterns
main, master, develop, release/*
# Add custom patterns
branch-clean --protect "staging" --protect "hotfix/*"
# In config file
protected:
- main
- master
- develop
- staging
- production
- release/*
- hotfix/*Pattern Matching:
main- Exact matchrelease/*- Wildcard (e.g., matchesrelease/v1.0,release/v2.0)*-production- Suffix wildcard (e.g., matchesapp-production)
Cannot delete the branch you're currently on:
$ git branch
* feature/my-work
feature/old-stuff
$ branch-clean
# feature/my-work will not appear in the listThe default branch (usually main or master) is automatically protected:
$ branch-clean --merged-only --force
# Will not delete main/master even if it somehow appears mergedUnless using --force or --yes, you'll always see:
You are about to delete 3 branch(es):
- feature/old-feature
- bugfix/ancient-bug
- experiment/failed-test
Continue [y/N]:
Always test with --dry-run first:
$ branch-clean --merged-only --dry-run
[DRY RUN] Would delete:
- feature/completed-feature
- bugfix/fixed-issueCause: You're not in a git repository directory.
Solution:
# Navigate to your git repository first
cd /path/to/your/git/repo
# Verify it's a git repo
git status
# Then run branch-clean
branch-clean listCause: Repository has no branches or no remote configured.
Solution:
# Check if remote exists
git remote -v
# Add remote if missing
git remote add origin https://github.com/user/repo.git
# Fetch from remote
git fetch origin
# Or specify default branch manually (temporary workaround)
git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/mainCause: branch-clean is not in your PATH.
Solution:
# Check if Go bin directory is in PATH
echo $PATH | grep go
# Add to PATH (add to ~/.bashrc or ~/.zshrc)
export PATH="$PATH:$HOME/go/bin"
# Or install to system directory
sudo cp $(which branch-clean) /usr/local/bin/Cause: Local repository is out of sync with remote.
Solution:
# Fetch latest from remote
git fetch --all --prune
# Then run branch-clean
branch-clean listCause: No push permission or remote doesn't exist.
Solution:
# Verify push permission
git push --dry-run
# Check if branch exists on remote
git ls-remote --heads origin
# Delete local branch only (without --remote flag)
branch-clean --merged-onlyCause: Invalid --stale-days value.
Solution:
# Use positive integer
branch-clean --stale-days 30 # ✓ Correct
branch-clean --stale-days -7 # ✗ Wrong
branch-clean --stale-days 0 # ✗ WrongA: No. Active branches (not merged and not stale) are automatically excluded. Use --dry-run to preview before deleting.
A: Local branch deletions can be recovered using git reflog:
# Find the commit hash of the deleted branch
git reflog
# Recreate the branch
git branch recovered-branch <commit-hash>Note: Remote deletions cannot be easily undone if others have already fetched the changes.
A: Yes! branch-clean works with any git repository, regardless of hosting provider. Use --remote to delete from the remote server.
A: Uses git merge-base --is-ancestor which accurately detects all types of merges:
- Regular merge commits
- Squash merges
- Rebase merges
- Fast-forward merges
A: Yes! Use --force and --yes flags for non-interactive operation:
branch-clean --merged-only --force --remoteA: branch-clean tracks failures and reports them:
✓ Deleted feature/branch1
✗ Failed to delete feature/branch2: permission denied
✓ Deleted feature/branch3
Deleted 2 of 3 branches
Exit code will be 1 to indicate partial failure.
A: Use the --protect flag or configuration file:
# Command line
branch-clean --protect "release/*" --protect "hotfix/*"
# Config file
echo "protected:
- release/*
- hotfix/*" > ~/.branch-clean.yamlA: Yes, use --dry-run:
branch-clean --merged-only --dry-runA:
--force: Skips the confirmation prompt (but still shows selection UI)--yes: Auto-answers "yes" to all prompts- Both are useful for automation, but
--forceis more common
- Go 1.21 or higher
- Git
# Clone the repository
git clone https://github.com/onamfc/branch-clean.git
cd branch-clean
# Install dependencies
go mod download
# Build
go build -o branch-clean
# Run tests
go test ./...
# Run tests with coverage
go test -cover ./...
# Run specific tests
go test -v ./internal -run TestFilterBranches# All tests
go test ./...
# With verbose output
go test -v ./...
# With coverage report
go test -cover ./... -coverprofile=coverage.out
go tool cover -html=coverage.out
# Specific package
go test ./internal
# Specific test
go test ./internal -run TestMergeDetectionbranch-clean/
├── main.go # CLI entry point, commands, flags
├── internal/
│ ├── git.go # Git operations, merge detection
│ ├── git_test.go # Git tests
│ ├── ui.go # UI rendering, branch selection
│ ├── ui_test.go # UI tests
│ ├── config.go # Configuration file handling
│ └── config_test.go # Config tests
├── go.mod # Dependencies
├── go.sum # Dependency checksums
├── README.md # This file
├── CHANGELOG.md # Version history
├── IMPROVEMENTS.md # Technical improvements doc
└── LICENSE # MIT License
- Write tests first (TDD approach)
- Update documentation (README, CHANGELOG)
- Follow Go conventions (gofmt, golint)
- Add examples in README
# Enable verbose mode
branch-clean --verbose --merged-only
# Use dry-run for safe testing
branch-clean --dry-run --verbose
# Check git operations directly
git merge-base --is-ancestor feature/branch main
echo $? # 0 = merged, 1 = not mergedWe welcome contributions! Here's how you can help:
- Check if issue already exists
- Provide minimal reproduction steps
- Include version information (
branch-clean version) - Share relevant error messages
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Add tests for new functionality
- Run tests (
go test ./...) - Commit your changes (
git commit -m 'Add amazing feature') - Push to your fork (
git push origin feature/amazing-feature) - Open a Pull Request
- Follow standard Go formatting (
gofmt) - Add comments for exported functions
- Write table-driven tests
- Keep functions focused and small
- Use meaningful variable names
feat: Add support for custom config file locations
fix: Correct merge detection for squash merges
docs: Update installation instructions
test: Add tests for filter logic
chore: Update dependencies
This project is licensed under the MIT License - see the LICENSE file for details.
- Built with Cobra for CLI framework
- Uses promptui for interactive prompts
- Powered by go-git for Git operations
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: This README
Made with ❤️ by developers, for developers