diff --git a/scripts/pr-lib/common.sh b/scripts/pr-lib/common.sh index 2da65193cc3..9d246eaf0fd 100644 --- a/scripts/pr-lib/common.sh +++ b/scripts/pr-lib/common.sh @@ -185,3 +185,151 @@ merge_author_email_candidates() { "${reviewer_id}+${reviewer}@users.noreply.github.com" \ "${reviewer}@users.noreply.github.com" | awk 'NF && !seen[$0]++' } + +common_repo_root() { + if command -v repo_root >/dev/null 2>&1; then + repo_root + return + fi + + local base_dir + base_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + git -C "$base_dir" rev-parse --show-toplevel +} + +worktree_path_for_branch() { + local branch="$1" + local ref="refs/heads/$branch" + + git worktree list --porcelain | awk -v ref="$ref" ' + /^worktree / { + worktree=$2 + next + } + /^branch / { + if ($2 == ref) { + print worktree + found=1 + } + } + END { + if (!found) { + exit 1 + } + } + ' +} + +worktree_is_registered() { + local path="$1" + git worktree list --porcelain | awk -v target="$path" ' + /^worktree / { + if ($2 == target) { + found=1 + } + } + END { + exit found ? 0 : 1 + } + ' +} + +resolve_existing_dir_path() { + local path="$1" + if [ ! -d "$path" ]; then + return 1 + fi + + ( + cd "$path" >/dev/null 2>&1 && + pwd -P + ) +} + +is_repo_pr_worktree_dir() { + local path="$1" + local root + root=$(common_repo_root) + + local worktrees_dir="$root/.worktrees" + local resolved_path + resolved_path=$(resolve_existing_dir_path "$path" 2>/dev/null || true) + if [ -z "$resolved_path" ]; then + return 1 + fi + + local resolved_worktrees_dir + resolved_worktrees_dir=$(resolve_existing_dir_path "$worktrees_dir" 2>/dev/null || true) + if [ -z "$resolved_worktrees_dir" ]; then + return 1 + fi + + case "$resolved_path" in + "$resolved_worktrees_dir"/pr-*) + return 0 + ;; + esac + return 1 +} + +remove_worktree_if_present() { + local path="$1" + if [ ! -e "$path" ]; then + return 0 + fi + + if worktree_is_registered "$path"; then + git worktree remove "$path" --force >/dev/null 2>&1 || true + fi + + if [ ! -e "$path" ]; then + return 0 + fi + + if worktree_is_registered "$path"; then + echo "Warning: failed to remove registered worktree $path" + return 0 + fi + + if ! is_repo_pr_worktree_dir "$path"; then + echo "Warning: refusing to trash non-PR-worktree path $path" + return 0 + fi + + if command -v trash >/dev/null 2>&1; then + trash "$path" >/dev/null 2>&1 || { + echo "Warning: failed to trash orphaned worktree dir $path" + return 0 + } + return 0 + fi + + echo "Warning: orphaned worktree dir remains and trash is unavailable: $path" + return 0 +} + +delete_local_branch_if_safe() { + local branch="$1" + local ref="refs/heads/$branch" + + if ! git show-ref --verify --quiet "$ref"; then + return 0 + fi + + local branch_worktree="" + branch_worktree=$(worktree_path_for_branch "$branch" 2>/dev/null || true) + if [ -n "$branch_worktree" ]; then + echo "Skipping local branch delete for $branch; checked out in worktree $branch_worktree" + return 0 + fi + + if git branch -D "$branch" >/dev/null 2>&1; then + return 0 + fi + if git update-ref -d "$ref" >/dev/null 2>&1; then + return 0 + fi + + echo "Warning: failed to delete local branch $branch" + return 0 +} diff --git a/scripts/pr-lib/merge.sh b/scripts/pr-lib/merge.sh index c505a4314a8..9b16b54e0d8 100644 --- a/scripts/pr-lib/merge.sh +++ b/scripts/pr-lib/merge.sh @@ -227,13 +227,41 @@ Co-authored-by: $reviewer <$reviewer_coauthor_email> Reviewed-by: @$reviewer EOF_BODY + delete_remote_pr_head_branch_after_merge() { + local head_json + head_json=$(gh pr view "$pr" --json headRefName,headRepository,headRepositoryOwner,isCrossRepository,maintainerCanModify) + + local head_ref + head_ref=$(printf '%s\n' "$head_json" | jq -r '.headRefName // ""') + if [ -z "$head_ref" ]; then + return 0 + fi + + local repo_owner + repo_owner=$(printf '%s\n' "$head_json" | jq -r '.headRepositoryOwner.login // ""') + local repo_name + repo_name=$(printf '%s\n' "$head_json" | jq -r '.headRepository.name // ""') + if [ -z "$repo_owner" ] || [ -z "$repo_name" ]; then + echo "Warning: unable to resolve head repository for remote branch cleanup" + return 0 + fi + + local encoded_ref + encoded_ref=$(jq -rn --arg value "heads/$head_ref" '$value|@uri') + if gh api -X DELETE "repos/$repo_owner/$repo_name/git/refs/$encoded_ref" >/dev/null 2>&1; then + return 0 + fi + + echo "Warning: failed to delete remote branch $repo_owner/$repo_name:$head_ref" + return 0 + } + run_merge_with_email() { local email="$1" local merge_output_file merge_output_file=$(mktemp) if gh pr merge "$pr" \ --squash \ - --delete-branch \ --match-head-commit "$PREP_HEAD_SHA" \ --author-email "$email" \ --subject "$pr_title (#$pr_number)" \ @@ -351,10 +379,11 @@ EOF_COMMENT local root root=$(repo_root) cd "$root" - git worktree remove ".worktrees/pr-$pr" --force - git branch -D "temp/pr-$pr" 2>/dev/null || true - git branch -D "pr-$pr" 2>/dev/null || true - git branch -D "pr-$pr-prep" 2>/dev/null || true + delete_remote_pr_head_branch_after_merge + remove_worktree_if_present ".worktrees/pr-$pr" + delete_local_branch_if_safe "temp/pr-$pr" + delete_local_branch_if_safe "pr-$pr" + delete_local_branch_if_safe "pr-$pr-prep" local pr_url pr_url=$(gh pr view "$pr" --json url --jq .url) diff --git a/scripts/pr-lib/worktree.sh b/scripts/pr-lib/worktree.sh index a5f42d208da..df5e56f3064 100644 --- a/scripts/pr-lib/worktree.sh +++ b/scripts/pr-lib/worktree.sh @@ -131,10 +131,10 @@ gc_pr_worktrees() { if [ "$dry_run" = "true" ]; then echo "would remove $dir (PR #$pr state=$state)" else - git worktree remove "$dir" --force - git branch -D "temp/pr-$pr" 2>/dev/null || true - git branch -D "pr-$pr" 2>/dev/null || true - git branch -D "pr-$pr-prep" 2>/dev/null || true + remove_worktree_if_present "$dir" + delete_local_branch_if_safe "temp/pr-$pr" + delete_local_branch_if_safe "pr-$pr" + delete_local_branch_if_safe "pr-$pr-prep" echo "removed $dir (PR #$pr state=$state)" fi removed=$((removed + 1))