Skip to content

Instantly share code, notes, and snippets.

@mgsloan
Created February 8, 2025 00:36
Show Gist options
  • Save mgsloan/95be3f8fc5a0c7b582c176d61be527ab to your computer and use it in GitHub Desktop.
Save mgsloan/95be3f8fc5a0c7b582c176d61be527ab to your computer and use it in GitHub Desktop.
#!/bin/bash
# In retrospect I'd have preferred a python script, but hey this works.
#
# https://claude.ai/chat/baf4d2fd-ddbe-4852-9361-91c0a091f40c
#
# https://claude.site/artifacts/9b588d36-cf7e-4bf9-aed1-187adc0b8ef7
#
# > How can I automatically delete my local git branches if the
# > associated github PR has been merged (the remote branch is on the
# > main git repo)? Would also be great if this mechanism verified
# > that the merged version was the same as the branch
#
# > The PRs are squashed and rebased on merge, so they won't appear in
# > the history. I think maybe use of github's API or the "hub" cli
# > tool will be needed
#
# > Please add a dry-run mode that does not delete any branches, but
# > lists the ones that would be deleted
#
# > It works! Looks like "git branch" includes prefixes like "*" and
# > "+" on current branches, maybe its invocation should be somehow
# > updated?
#
# > Please update this to default to dry run mode and only do it if
# > the argument "--apply" is passed
#
# > When PRs are merged to main, the first line of the message will
# > have a (#PR_NUMBER) suffix. Please add code to check that the
# > diff of the merged PR is identical to the diff of the branch
# > relative to its merge base
#
# > /home/mgsloan/.local/bin/delete-merged-branches-fancy: line 55: syntax error near unexpected token }'
# >
# > /home/mgsloan/.local/bin/delete-merged-branches-fancy: line 55: }'
#
# With manual fix adding "--state merged" to gh pr list
# Default to dry-run mode
DRY_RUN=true
# Parse command line arguments
for arg in "$@"; do
case $arg in
--apply)
DRY_RUN=false
echo "Running in apply mode - branches will be deleted"
shift
;;
esac
done
if [ "$DRY_RUN" = true ]; then
echo "Running in dry-run mode - no branches will be deleted"
echo "Use --apply to actually delete branches"
fi
# Ensure gh CLI is installed
if ! command -v gh &> /dev/null; then
echo "GitHub CLI (gh) is not installed. Please install it first:"
echo "https://cli.github.com/"
exit 1
fi
# Ensure user is authenticated with gh
if ! gh auth status &> /dev/null; then
echo "Please authenticate with GitHub first using: gh auth login"
exit 1
fi
# Function to get PR status for a branch
get_pr_status() {
local branch=$1
# Get PR number and state, plus merge commit info
pr_info=$(gh pr list --head "$branch" --json number,state,mergeCommit,title,headRefName --state merged -L 1)
echo "$pr_info"
}
# Function to verify PR content matches branch
verify_pr_content() {
local branch=$1
local pr_number=$2
echo "Verifying content matches for PR #$pr_number..."
# Get the merge base commit
merge_base=$(git merge-base $branch main)
if [ $? -ne 0 ]; then
echo "Failed to find merge base for $branch"
return 1
fi
# Get the commit message of the merge to find PR number
merge_commit=$(git log main --grep="(#$pr_number)" -n 1 --format="%H")
if [ -z "$merge_commit" ]; then
echo "Could not find merge commit for PR #$pr_number"
return 1
fi
# Create temporary files for diffs
local_diff_file=$(mktemp)
pr_diff_file=$(mktemp)
# Get the diff of the local branch against merge base
git diff $merge_base $branch > "$local_diff_file"
# Get the diff of the merge commit against its parent
# Use the first parent to compare against main branch
git diff ${merge_commit}^1 $merge_commit > "$pr_diff_file"
# Compare diffs, ignoring whitespace
if diff -Bw "$local_diff_file" "$pr_diff_file" > /dev/null; then
echo "✓ Branch content matches merged PR"
rm "$local_diff_file" "$pr_diff_file"
return 0
else
echo "✗ Branch content differs from merged PR"
echo " This could mean:"
echo " - The branch has new commits after the PR was merged"
echo " - The PR was modified during merge"
rm "$local_diff_file" "$pr_diff_file"
return 1
fi
}
# Track branches that would be deleted
branches_to_delete=()
# Ensure we're up to date with remote
git fetch origin
# Get list of local branches
local_branches=$(git branch --format="%(refname:short)")
for branch in $local_branches; do
# Skip main/master branch
if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then
continue
fi
echo "Checking branch: $branch"
# Get PR status
pr_info=$(get_pr_status "$branch")
if [ -z "$pr_info" ]; then
echo "No PR found for branch $branch"
if [ "$DRY_RUN" = true ]; then
echo "Would prompt for deletion confirmation in apply mode"
else
read -p "No PR found. Delete this branch? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
git branch -D "$branch"
fi
fi
continue
fi
# Check if PR is merged
if echo "$pr_info" | jq -e '.[0].state == "MERGED"' &> /dev/null; then
pr_number=$(echo "$pr_info" | jq -r '.[0].number')
echo "PR #$pr_number for branch $branch was merged"
# Verify content matches
if verify_pr_content "$branch" "$pr_number"; then
if [ "$DRY_RUN" = true ]; then
branches_to_delete+=("$branch")
echo "Would delete branch $branch"
else
echo "Deleting branch..."
git branch -D "$branch"
fi
else
echo "Skipping branch deletion due to content mismatch"
fi
else
pr_number=$(echo "$pr_info" | jq -r '.[0].number')
if [ "$pr_number" != "null" ]; then
pr_state=$(echo "$pr_info" | jq -r '.[0].state')
echo "PR #$pr_number is still $pr_state. Keeping branch."
fi
fi
done
# Show summary of branches that would be/were deleted
if [ ${#branches_to_delete[@]} -gt 0 ]; then
echo
if [ "$DRY_RUN" = true ]; then
echo "Summary of branches that would be deleted:"
else
echo "Summary of branches deleted:"
fi
printf '%s\n' "${branches_to_delete[@]}"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment