From 340ff80cd2f26a6a3f78baa48fb363823b7b043e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 21:37:01 +0000 Subject: [PATCH] Give up cleanly when the kept parent branch is gone during conflict resolution If origin/ was deleted while a child PR sat in conflict (auto-delete head branches left enabled despite the README, or manual deletion), the resume merge failed with "not something we can merge" and git merge --abort failed too ("There is no merge to abort"). The run then exited nonzero after re-posting the misleading conflict comment and the label, repeating on every push. (Not a mid-function set -e kill: update_direct_target is called in an if condition, which suppresses errexit; observed via the new test scenario.) Check origin/ up front like #39 did for the target branch, and only run git merge --abort when MERGE_HEAD exists. https://claude.ai/code/session_01STkeSJ7cLrmrNn4aTDYkwH --- tests/test_conflict_resolution_resume.sh | 30 ++++++++++++++++++++++++ update-pr-stack.sh | 24 +++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/tests/test_conflict_resolution_resume.sh b/tests/test_conflict_resolution_resume.sh index 3f0956a..933611d 100644 --- a/tests/test_conflict_resolution_resume.sh +++ b/tests/test_conflict_resolution_resume.sh @@ -167,5 +167,35 @@ grep -q -- "--base" "$CALLS" && fail "D: base must NOT be edited" [[ "$(git -C "$ORIGIN" rev-parse child)" == "$CHILD_BEFORE" ]] || fail "D: child was pushed" ok "D: missing target detected, no branch mutation, label removed" +# --------------------------------------------------------------------------- +echo "### Scenario E: recorded base branch is gone -> give up cleanly, no crash" +setup_repo +# Advance main with the squash commit so the child is not already up to date and +# the resume would actually reach the merge step. +git checkout -q main +echo squash > s.txt && git add s.txt && git commit -qm squash +SQUASH2=$(git rev-parse main) +git push -q origin main +git checkout -q child +# The kept parent branch was deleted (auto-delete head branches left enabled, or +# manual deletion). Before the up-front check, the resume tried to merge the +# missing ref, failed `git merge --abort` because no merge was in progress, and +# exited nonzero after re-posting a misleading conflict comment and the label, +# repeating on every push. +git push -q origin ":parent" +MOCK_LABELS="autorestack-needs-conflict-resolution" +MOCK_BASE="parent" # matches marker -> not a manual retarget +MOCK_COMMENTS_FILE="$WORK/comments.txt" +{ echo "### conflict"; echo; marker parent main "$SQUASH2"; } > "$MOCK_COMMENTS_FILE" +run_resume + +grep -q "EXIT=" "$WORK/out.log" && fail "E: script exited nonzero: $(cat "$WORK/out.log")" +grep -q "remove-label autorestack-needs-conflict-resolution" "$CALLS" || fail "E: label not removed" +grep -q -- "add-label" "$CALLS" && fail "E: conflict label must NOT be re-added" +grep -q "gh pr comment" "$CALLS" || fail "E: no explanatory comment posted" +grep -q -- "--base" "$CALLS" && fail "E: base must NOT be edited" +[[ "$(git -C "$ORIGIN" rev-parse child)" == "$CHILD_BEFORE" ]] || fail "E: child was pushed" +ok "E: missing base branch detected, no crash, label removed" + echo echo "All conflict-resume tests passed 🎉 ($PASS scenarios)" diff --git a/update-pr-stack.sh b/update-pr-stack.sh index a2d8ca5..94c271e 100755 --- a/update-pr-stack.sh +++ b/update-pr-stack.sh @@ -74,6 +74,16 @@ has_squash_commit() { && git merge-base --is-ancestor SQUASH_COMMIT "$BRANCH" } +# A failed git merge does not always leave a merge in progress: when the ref to +# merge does not exist ("not something we can merge"), there is no MERGE_HEAD, +# and `git merge --abort` itself fails ("There is no merge to abort"). Only +# abort when a merge is actually in progress. +abort_merge_if_in_progress() { + if git rev-parse --verify --quiet MERGE_HEAD >/dev/null; then + log_cmd git merge --abort + fi +} + update_direct_target() { local BRANCH="$1" local BASE_BRANCH="$2" @@ -96,14 +106,14 @@ update_direct_target() { if ! log_cmd git merge --no-edit "origin/$MERGED_BRANCH"; then CONFLICTS+=("origin/$MERGED_BRANCH") BASE_MERGE_CLEAN=false - log_cmd git merge --abort + abort_merge_if_in_progress fi # Only try merging the pre-squash target state if it's not already # included in the merged branch — otherwise the first merge covers it. if ! git merge-base --is-ancestor SQUASH_COMMIT~ "origin/$MERGED_BRANCH"; then if ! log_cmd git merge --no-edit SQUASH_COMMIT~; then CONFLICTS+=( "$(git rev-parse SQUASH_COMMIT~) # $TARGET_BRANCH just before $MERGED_BRANCH was merged" ) - log_cmd git merge --abort + abort_merge_if_in_progress fi fi @@ -256,6 +266,16 @@ continue_after_resolution() { return fi + # Same check for the old base: the resume re-merges origin/$OLD_BASE, so if + # that branch is gone (auto-delete head branches left enabled, or deleted + # manually) the merge can never succeed and the label would re-trigger a + # failing run on every push. Give up cleanly instead. + if ! git rev-parse --verify --quiet "origin/$OLD_BASE" >/dev/null; then + echo "âš ī¸ Recorded base branch '$OLD_BASE' no longer exists; abandoning resume of $PR_BRANCH." + abandon_resume "$PR_BRANCH" "â„šī¸ The branch this PR was based on (\`$OLD_BASE\`) no longer exists, so autorestack stepped back. If this PR still needs its base updated, update its base manually." + return + fi + # The squash-merge run pushed the base merge and asked the user to resolve the # pre-squash merge, but it never recorded the squash itself. Finish that now: # re-run the same merge sequence as the squash-merge path. With the user's