Created
September 11, 2025 17:50
-
-
Save nabilfreeman/ff489d0876d2e2463d244b10633b1187 to your computer and use it in GitHub Desktop.
Bash function to clean up all locally merged branches and worktrees. Add to your .zshrc or .bashrc and run `gitcleanup` in terminal to make the magic happen
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # gitcleanup: remove merged branches and their worktrees, after ensuring everything is up‑to‑date | |
| # Usage: gitcleanup [target-branch] | |
| # | |
| # 1. Ensure the target branch exists, is clean, and up‑to‑date with origin. | |
| # 2. For every local branch already merged into the target: | |
| # • If the branch has a worktree and that worktree is clean, fast‑forward it with `git pull --ff-only`. | |
| # • If the worktree remains clean, remove the worktree and delete the branch. | |
| # • If the branch has no worktree, delete the branch directly. | |
| # 3. Stale worktree records are pruned first. | |
| gitcleanup() { | |
| local target=${1:-main} # merge base (default: main) | |
| # 1. Sync refs | |
| echo "Fetching latest refs from all remotes…" | |
| git fetch --prune --all || return 1 | |
| # Remember where we were, so we can come back later | |
| local current_branch | |
| current_branch=$(git symbolic-ref --quiet --short HEAD || true) | |
| # 2. Make sure $target is checked out in the primary worktree | |
| if [[ "$current_branch" != "$target" ]]; then | |
| echo "Checking out $target…" | |
| git switch --quiet "$target" || git checkout "$target" || { | |
| echo "❌ Unable to switch to branch '$target'"; return 1; } | |
| fi | |
| # 3. Bail out if $target worktree is dirty | |
| if ! git diff-index --quiet HEAD --; then | |
| echo "❌ Target branch '$target' has uncommitted changes – aborting cleanup." | |
| [[ -n "$current_branch" && "$current_branch" != "$target" ]] && git switch --quiet "$current_branch" | |
| return 1 | |
| fi | |
| # 4. Fast‑forward $target | |
| echo "Pulling latest commits into $target…" | |
| git pull --ff-only --quiet origin "$target" || { | |
| echo "❌ Pull failed – aborting cleanup."; return 1; } | |
| # 5. Get rid of stale worktree refs first | |
| git worktree prune | |
| # 6. Iterate over every local branch already merged to $target (except $target itself) | |
| git for-each-ref --format='%(refname:short)' --merged "$target" refs/heads | | |
| grep -v "^${target}$" | | |
| while read -r branch; do | |
| # Does this branch live in an active worktree? | |
| local wt_path | |
| wt_path=$( | |
| git worktree list --porcelain | | |
| awk -v b="refs/heads/$branch" ' | |
| $1=="worktree" { w=$2 } | |
| $1=="branch" && $2==b { print w } | |
| ' | |
| ) | |
| if [[ -n "$wt_path" ]]; then | |
| # Worktree exists – is it clean? | |
| if git -C "$wt_path" diff-index --quiet HEAD --; then | |
| echo "Fast‑forwarding branch $branch in worktree $wt_path…" | |
| if git -C "$wt_path" pull --ff-only --quiet; then | |
| echo "Removing clean worktree $wt_path (branch $branch)…" | |
| if git worktree remove "$wt_path"; then | |
| git branch -d "$branch" | |
| else | |
| echo "⚠️ Couldn’t remove worktree $wt_path – skipping branch deletion" | |
| fi | |
| else | |
| echo "⚠️ Couldn’t pull branch $branch – skipping" | |
| fi | |
| else | |
| echo "⚠️ Skipping $wt_path (branch $branch) – uncommitted changes present" | |
| fi | |
| else | |
| # No worktree. Just delete the local branch (already merged). | |
| git branch -d "$branch" | |
| fi | |
| done | |
| # 7. Return to original branch if needed | |
| if [[ -n "$current_branch" && "$current_branch" != "$target" ]]; then | |
| git switch --quiet "$current_branch" | |
| fi | |
| echo "✅ Cleanup complete." | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment