Last active
November 28, 2025 01:06
-
-
Save clydebarrow/c747ed879b264dcef769dd8725a84f5b to your computer and use it in GitHub Desktop.
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
| #!/usr/bin/env bash | |
| # Script to delete local and remote branches for merged PRs | |
| # Usage: ./cleanup-merged-branches [--execute] [--branch <branch_name>] | |
| set -euo pipefail | |
| # Colors for output (using printf instead of echo -e for better compatibility) | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| BLUE='\033[0;34m' | |
| MAGENTA='\033[0;35m' | |
| CYAN='\033[0;36m' | |
| YELLOW='\033[1;33m' | |
| NC='\033[0m' # No Color | |
| # Parse command line arguments | |
| EXECUTE_MODE=false | |
| FILTER_BRANCH="" | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| --execute) | |
| EXECUTE_MODE=true | |
| shift | |
| ;; | |
| --branch) | |
| FILTER_BRANCH="$2" | |
| shift 2 | |
| ;; | |
| *) | |
| echo "Unknown option: $1" | |
| echo "Usage: $0 [--execute] [--branch <branch_name>]" | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| # Check if gh is installed | |
| if ! command -v gh &> /dev/null; then | |
| printf "${RED}Error: gh CLI is not installed${NC}\n" | |
| echo "Install it from: https://cli.github.com/" | |
| exit 1 | |
| fi | |
| # Check if we're authenticated | |
| if ! gh auth status &> /dev/null; then | |
| printf "${RED}Error: Not authenticated with gh CLI${NC}\n" | |
| echo "Run: gh auth login" | |
| exit 1 | |
| fi | |
| # Get current user | |
| GITHUB_USER=$(gh api user -q .login) | |
| # Detect the upstream repo (org/repo) from git remotes | |
| # Look for a remote that's NOT the user's fork | |
| UPSTREAM_REPO=$(git remote -v | grep -v "${GITHUB_USER}/" | grep -oE '[:/][^/]+/[^/]+(\.git)?( \(push\)|$)' | head -n1 | sed -E 's|^[:/]||; s|\.git.*||; s| .*||') | |
| if [[ -z "$UPSTREAM_REPO" ]]; then | |
| printf "${RED}Error: Could not detect upstream repository${NC}\n" | |
| printf "${RED}Make sure you have an upstream remote configured${NC}\n" | |
| exit 1 | |
| fi | |
| # Extract org and repo name | |
| UPSTREAM_ORG=$(echo "$UPSTREAM_REPO" | cut -d'/' -f1) | |
| UPSTREAM_NAME=$(echo "$UPSTREAM_REPO" | cut -d'/' -f2) | |
| # Get the remote name for the user's fork | |
| REMOTE=$(git remote -v | grep -E "${GITHUB_USER}/${UPSTREAM_NAME}|github.com[:/]${GITHUB_USER}/${UPSTREAM_NAME}" | head -n1 | awk '{print $1}') | |
| if [[ -z "$REMOTE" ]]; then | |
| printf "${RED}Error: Could not find remote for ${GITHUB_USER}/${UPSTREAM_NAME}${NC}\n" | |
| printf "${RED}Make sure you have your fork configured as a remote${NC}\n" | |
| exit 1 | |
| fi | |
| printf "${GREEN}Upstream repo: ${CYAN}${UPSTREAM_REPO}${NC}\n" | |
| printf "${GREEN}Fetching merged PRs for user: ${GITHUB_USER}${NC}\n" | |
| printf "${GREEN}Fork remote: ${REMOTE}${NC}\n" | |
| if [[ -n "$FILTER_BRANCH" ]]; then | |
| printf "${GREEN}Filtering for branch: ${CYAN}${FILTER_BRANCH}${NC}\n" | |
| fi | |
| echo "" | |
| # Fetch all remote branches once for performance | |
| printf "${GREEN}Fetching remote branches from ${REMOTE}...${NC}\n" | |
| REMOTE_BRANCHES=$(git ls-remote --heads "$REMOTE" | awk '{print $2}' | sed 's|refs/heads/||') | |
| # Fetch remote branch info to get latest commit dates | |
| printf "${GREEN}Fetching remote branch metadata...${NC}\n" | |
| git fetch "$REMOTE" --quiet 2>/dev/null || true | |
| echo "" | |
| # Fetch merged PRs authored by current user | |
| MERGED_PRS=$(gh pr list --repo "$UPSTREAM_REPO" --author "$GITHUB_USER" --state merged --json headRefName,number,title,mergedAt --limit 100) | |
| if [[ $(echo "$MERGED_PRS" | jq length) -eq 0 ]]; then | |
| printf "${YELLOW}No merged PRs found${NC}\n" | |
| exit 0 | |
| fi | |
| # Display mode | |
| if [[ "$EXECUTE_MODE" == false ]]; then | |
| printf "${MAGENTA}=== TEST MODE ===${NC}\n" | |
| echo "The following branches would be deleted:" | |
| echo "" | |
| fi | |
| BRANCHES_TO_DELETE=() | |
| # Process each merged PR | |
| echo "$MERGED_PRS" | jq -r '.[] | "\(.headRefName)|\(.number)|\(.title)|\(.mergedAt)"' | while IFS='|' read -r BRANCH PR_NUMBER TITLE MERGED_AT; do | |
| # Skip if filtering for a specific branch and this isn't it | |
| if [[ -n "$FILTER_BRANCH" ]] && [[ "$BRANCH" != "$FILTER_BRANCH" ]]; then | |
| continue | |
| fi | |
| # Check if branch exists locally | |
| LOCAL_EXISTS=false | |
| if git show-ref --verify --quiet "refs/heads/$BRANCH" 2>/dev/null; then | |
| LOCAL_EXISTS=true | |
| fi | |
| # Check if branch exists on remote (using cached list) | |
| REMOTE_EXISTS=false | |
| if echo "$REMOTE_BRANCHES" | grep -Fxq "$BRANCH"; then | |
| REMOTE_EXISTS=true | |
| fi | |
| if [[ "$LOCAL_EXISTS" == false ]] && [[ "$REMOTE_EXISTS" == false ]]; then | |
| continue | |
| fi | |
| # Check if branch was modified after merge | |
| MODIFIED_AFTER_MERGE=false | |
| if [[ "$REMOTE_EXISTS" == true ]]; then | |
| # Get the last commit date on the remote branch | |
| LAST_COMMIT_DATE=$(git log -1 --format=%cI "$REMOTE/$BRANCH" 2>/dev/null || echo "") | |
| if [[ -n "$LAST_COMMIT_DATE" ]] && [[ -n "$MERGED_AT" ]]; then | |
| # Convert dates to timestamps for comparison | |
| LAST_COMMIT_TS=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${LAST_COMMIT_DATE%+*}" "+%s" 2>/dev/null || echo "0") | |
| MERGED_AT_TS=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${MERGED_AT%+*}" "+%s" 2>/dev/null || echo "0") | |
| # If last commit is after merge date, skip this branch | |
| if [[ $LAST_COMMIT_TS -gt $MERGED_AT_TS ]]; then | |
| MODIFIED_AFTER_MERGE=true | |
| fi | |
| fi | |
| fi | |
| if [[ "$MODIFIED_AFTER_MERGE" == true ]]; then | |
| printf "${YELLOW}PR #${PR_NUMBER}:${NC} ${TITLE}\n" | |
| printf " Branch: ${CYAN}${BRANCH}${NC}\n" | |
| printf " ${YELLOW}⚠ Skipping: Branch has commits after merge date${NC}\n" | |
| echo "" | |
| continue | |
| fi | |
| printf "${GREEN}PR #${PR_NUMBER}:${NC} ${TITLE}\n" | |
| printf " Branch: ${CYAN}${BRANCH}${NC}\n" | |
| if [[ "$LOCAL_EXISTS" == true ]]; then | |
| if [[ "$EXECUTE_MODE" == true ]]; then | |
| printf " ${RED}Deleting local branch...${NC}\n" | |
| git branch -D "$BRANCH" || printf " ${RED}Failed to delete local branch${NC}\n" | |
| else | |
| printf " Would run (local): ${BLUE}git branch -D ${BRANCH}${NC}\n" | |
| fi | |
| fi | |
| if [[ "$REMOTE_EXISTS" == true ]]; then | |
| if [[ "$EXECUTE_MODE" == true ]]; then | |
| printf " ${RED}Deleting remote branch...${NC}\n" | |
| git push "$REMOTE" --delete "$BRANCH" || printf " ${RED}Failed to delete remote branch${NC}\n" | |
| else | |
| printf " Would run (remote): ${BLUE}git push ${REMOTE} --delete ${BRANCH}${NC}\n" | |
| fi | |
| fi | |
| echo "" | |
| done | |
| if [[ "$EXECUTE_MODE" == false ]]; then | |
| printf "${MAGENTA}=== TEST MODE ===${NC}\n" | |
| echo "No branches were actually deleted." | |
| echo "To execute the deletions, run: $0 --execute" | |
| else | |
| printf "${GREEN}Branch cleanup complete!${NC}\n" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment