Created
February 8, 2025 00:36
-
-
Save mgsloan/95be3f8fc5a0c7b582c176d61be527ab 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
| #!/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