Skip to content

Instantly share code, notes, and snippets.

@clydebarrow
Last active November 28, 2025 01:06
Show Gist options
  • Select an option

  • Save clydebarrow/c747ed879b264dcef769dd8725a84f5b to your computer and use it in GitHub Desktop.

Select an option

Save clydebarrow/c747ed879b264dcef769dd8725a84f5b to your computer and use it in GitHub Desktop.
#!/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