Files
pnpm/shell/cleanup-worktrees.sh
Zoltan Kochan 76be86dcb5 chore: add script to clean up worktrees with merged PRs (#10953)
* chore: add script to clean up worktrees with merged PRs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use safe arithmetic to avoid set -e exit on zero increment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 13:48:35 +01:00

102 lines
2.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Removes git worktrees whose branches are associated with merged PRs.
# Usage: ./cleanup-worktrees.sh [--dry-run]
#
# Requires: gh (GitHub CLI), git
set -euo pipefail
DRY_RUN=false
if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=true
echo "=== DRY RUN MODE ==="
echo
fi
# Get the GitHub repo (owner/name) from the origin remote
GH_REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null)
if [[ -z "$GH_REPO" ]]; then
echo "Error: Could not determine GitHub repository. Make sure 'gh' is authenticated." >&2
exit 1
fi
echo "Repository: $GH_REPO"
echo
# Get the current worktree path so we don't remove it
CURRENT_WORKTREE="$(pwd -P)"
removed=0
skipped=0
while IFS= read -r line; do
[[ -z "$line" ]] && continue
worktree_path=$(echo "$line" | awk '{print $1}')
branch=$(echo "$line" | awk '{print $3}' | tr -d '[]')
# Skip detached HEAD entries
[[ "$branch" == "detached" ]] && continue
# Skip bare repo root (shown as "(bare)")
echo "$line" | grep -q '(bare)$' && continue
# Skip protected long-lived branches
case "$branch" in
main|master|v[0-9]*)
echo "SKIP (protected branch): $worktree_path [$branch]"
skipped=$((skipped + 1))
continue
;;
esac
# Skip the current worktree
real_wt="$(cd "$worktree_path" 2>/dev/null && pwd -P)" || continue
if [[ "$real_wt" == "$CURRENT_WORKTREE" ]]; then
echo "SKIP (current worktree): $worktree_path [$branch]"
skipped=$((skipped + 1))
continue
fi
# Look for merged PRs with this branch as the head
merged_pr=$(gh pr list \
--repo "$GH_REPO" \
--head "$branch" \
--state merged \
--json number,title \
--jq 'if length > 0 then .[0] | "\(.number)\t\(.title)" else "" end' \
2>/dev/null || true)
if [[ -n "$merged_pr" ]]; then
pr_number=$(echo "$merged_pr" | cut -f1)
pr_title=$(echo "$merged_pr" | cut -f2-)
echo "MERGED: $worktree_path"
echo " Branch: $branch"
echo " PR #$pr_number: $pr_title"
if [[ "$DRY_RUN" == false ]]; then
git worktree remove --force "$worktree_path" && \
echo " -> Removed worktree" || \
echo " -> Failed to remove worktree"
# Also delete the branch
git branch -D "$branch" 2>/dev/null && \
echo " -> Deleted branch $branch" || true
else
echo " -> Would remove worktree and delete branch"
fi
echo
removed=$((removed + 1))
else
echo "SKIP (no merged PR): $worktree_path [$branch]"
skipped=$((skipped + 1))
fi
done < <(git worktree list)
echo
echo "---"
if [[ "$DRY_RUN" == true ]]; then
echo "Would remove $removed worktree(s). Skipped $skipped."
echo "Run without --dry-run to actually remove them."
else
echo "Removed $removed worktree(s). Skipped $skipped."
fi