Skip to content

Commit cb36bad

Browse files
committed
Improve conflict handling: defer base branch change and branch deletion
When a merge conflict occurs during automatic PR stack update: - Don't change the PR's base branch (keeps the diff readable) - Don't delete the merged branch (user can reference it during resolution) After the user resolves conflicts and pushes: - Update the PR's base branch to trunk - Delete the old base branch only if no other conflicted PRs depend on it Changes: - main(): Track UPDATED_TARGETS vs CONFLICTED_TARGETS separately - main(): Only update base and delete branch for successfully updated PRs - Add has_sibling_conflicts() helper to check if other PRs share the base - continue_after_resolution(): Update base branch and conditionally delete - Update E2E tests to verify the new behavior - Add Scenario 3: sibling conflicts test for branch deletion timing
1 parent 1fd05bf commit cb36bad

3 files changed

Lines changed: 326 additions & 31 deletions

File tree

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,17 @@ When a merge conflict occurs during the automatic update:
2929

3030
1. The action posts a comment on the affected PR with instructions for manual resolution
3131
2. Adds a `autorestack-needs-conflict-resolution` label to the PR
32-
3. Updates the PR's base branch (but doesn't push the conflicted state)
32+
3. Keeps the PR's base branch unchanged (so the diff stays readable)
33+
4. Keeps the merged branch around (so you can reference it during resolution)
3334

3435
After you manually resolve the conflict and push:
3536

3637
1. The push triggers the `synchronize` event
3738
2. The action detects the conflict label and removes it
38-
3. Posts a confirmation comment
39-
4. Continues updating any dependent PRs in the stack
39+
3. Updates the PR's base branch to trunk
40+
4. Deletes the old base branch (if no other conflicted PRs still depend on it)
41+
5. Posts a confirmation comment
42+
6. Continues updating any dependent PRs in the stack
4043

4144
---
4245

tests/test_e2e.sh

Lines changed: 253 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,13 @@
5858
# - Does NOT push any conflicted state to the remote
5959
# - Posts a comment on PR3 explaining the conflict
6060
# - Adds a label "autorestack-needs-conflict-resolution" to PR3
61-
# - Updates PR3's base branch to main (even though merge failed)
61+
# - Does NOT update PR3's base branch (keeps it as feature2 for readable diff)
62+
# - Does NOT delete feature2 branch (still referenced by conflicted PR)
6263
# - Exits with success (conflict is handled gracefully, not a failure)
6364
#
6465
# Verifications:
65-
# - feature2 branch is deleted from remote
66-
# - PR3 base branch is updated to main
66+
# - feature2 branch is NOT deleted from remote (still referenced by conflicted PR3)
67+
# - PR3 base branch stays as feature2 (not updated to main)
6768
# - Conflict comment exists on PR3
6869
# - Conflict label "autorestack-needs-conflict-resolution" exists on PR3
6970
# - feature3 branch was NOT updated (still at pre-conflict SHA)
@@ -74,8 +75,10 @@
7475
# - Push the resolved branch
7576
# - The push triggers the 'synchronize' event on PR3
7677
# - The action detects the conflict label and removes it
78+
# - Updates PR3's base branch to main
79+
# - Deletes feature2 branch (no other conflicted PRs depend on it)
7780
# - The continuation workflow updates feature4 (grandchild) recursively
78-
# - Verify the label is removed, resolution comment is posted, and feature4 is updated
81+
# - Verify the label is removed, base updated, branch deleted, and feature4 is updated
7982
#
8083
# Grandchild Update (feature4):
8184
# - Tests that update_branch_recursive properly handles grandchildren
@@ -676,23 +679,23 @@ fi
676679
# 11. Verification for Conflict Scenario
677680
echo >&2 "11. Verifying the results of the conflict scenario..."
678681
echo >&2 "Fetching latest state from remote..."
679-
log_cmd git fetch origin --prune # Prune deleted branch feature2
682+
log_cmd git fetch origin --prune
680683

681-
# Verify feature2 branch was deleted remotely
684+
# Verify feature2 branch was NOT deleted (still referenced by conflicted PR3)
682685
if git show-ref --verify --quiet refs/remotes/origin/feature2; then
683-
echo >&2 "❌ Verification Failed: Remote branch 'origin/feature2' still exists after merge."
684-
exit 1
686+
echo >&2 "✅ Verification Passed: Remote branch 'origin/feature2' still exists (kept for conflicted PR)."
685687
else
686-
echo >&2 "✅ Verification Passed: Remote branch 'origin/feature2' was deleted."
688+
echo >&2 "❌ Verification Failed: Remote branch 'origin/feature2' was deleted prematurely."
689+
exit 1
687690
fi
688691

689-
# Verify PR3 base branch was updated to main (action updates base even on conflict)
692+
# Verify PR3 base branch was NOT updated (stays as feature2 for readable diff)
690693
echo >&2 "Checking PR #$PR3_NUM base branch..."
691694
PR3_BASE_AFTER_CONFLICT=$(log_cmd gh pr view "$PR3_NUM" --repo "$REPO_FULL_NAME" --json baseRefName --jq .baseRefName)
692-
if [[ "$PR3_BASE_AFTER_CONFLICT" == "main" ]]; then
693-
echo >&2 "✅ Verification Passed: PR #$PR3_NUM base branch updated to 'main'."
695+
if [[ "$PR3_BASE_AFTER_CONFLICT" == "feature2" ]]; then
696+
echo >&2 "✅ Verification Passed: PR #$PR3_NUM base branch stays as 'feature2' (not updated during conflict)."
694697
else
695-
echo >&2 "❌ Verification Failed: PR #$PR3_NUM base branch is '$PR3_BASE_AFTER_CONFLICT', expected 'main'."
698+
echo >&2 "❌ Verification Failed: PR #$PR3_NUM base branch is '$PR3_BASE_AFTER_CONFLICT', expected 'feature2'."
696699
exit 1
697700
fi
698701

@@ -789,19 +792,26 @@ else
789792
exit 1
790793
fi
791794

792-
# Verify resolution acknowledgement comment exists on PR3
793-
echo >&2 "Checking for resolution acknowledgement comment on PR #$PR3_NUM..."
794-
RESOLUTION_COMMENT=$(log_cmd gh pr view "$PR3_URL" --repo "$REPO_FULL_NAME" --json comments --jq '.comments[] | select(.body | contains("Conflict resolved")) | .body')
795-
if [[ -n "$RESOLUTION_COMMENT" ]]; then
796-
echo >&2 "✅ Verification Passed: Resolution acknowledgement comment found on PR #$PR3_NUM."
795+
# Verify PR3 base branch was updated to main after resolution
796+
echo >&2 "Checking PR #$PR3_NUM base branch after resolution..."
797+
PR3_BASE_AFTER_RESOLUTION=$(log_cmd gh pr view "$PR3_NUM" --repo "$REPO_FULL_NAME" --json baseRefName --jq .baseRefName)
798+
if [[ "$PR3_BASE_AFTER_RESOLUTION" == "main" ]]; then
799+
echo >&2 "✅ Verification Passed: PR #$PR3_NUM base branch updated to 'main' after resolution."
797800
else
798-
echo >&2 "❌ Verification Failed: Resolution acknowledgement comment not found on PR #$PR3_NUM."
799-
echo >&2 "--- Comments on PR #$PR3_NUM ---"
800-
gh pr view "$PR3_URL" --repo "$REPO_FULL_NAME" --json comments --jq '.comments[].body' || echo "Failed to get comments"
801-
echo >&2 "-----------------------------"
801+
echo >&2 "❌ Verification Failed: PR #$PR3_NUM base branch is '$PR3_BASE_AFTER_RESOLUTION', expected 'main'."
802802
exit 1
803803
fi
804804

805+
# Verify feature2 was deleted after resolution (no other conflicted PRs depend on it)
806+
echo >&2 "Checking that feature2 branch was deleted after resolution..."
807+
log_cmd git fetch origin --prune
808+
if git show-ref --verify --quiet refs/remotes/origin/feature2; then
809+
echo >&2 "❌ Verification Failed: Remote branch 'origin/feature2' still exists after resolution."
810+
exit 1
811+
else
812+
echo >&2 "✅ Verification Passed: Remote branch 'origin/feature2' was deleted after resolution."
813+
fi
814+
805815
echo >&2 "--- Continuation Workflow Test Completed Successfully ---"
806816

807817
# 15. Verify conflict resolution (content checks)
@@ -863,6 +873,227 @@ fi
863873
echo >&2 "--- Conflict Scenario Test Completed Successfully ---"
864874

865875

876+
# --- SCENARIO 3: Sibling Conflicts (Multiple PRs from same base, both conflict) ---
877+
# ===================================================================================
878+
# Tests that the old base branch is kept until ALL sibling PRs resolve their conflicts.
879+
#
880+
# Setup:
881+
# - Create a new stack: main <- feature5 <- (feature6, feature7) parallel children
882+
# - feature6 and feature7 both modify line 5 of file.txt
883+
# - main modifies line 5 differently (creating conflict with both siblings)
884+
#
885+
# Expected Behavior:
886+
# - After merging feature5, both feature6 and feature7 have conflicts
887+
# - feature5 branch is kept (referenced by both conflicted PRs)
888+
# - After resolving feature6, feature5 is still kept (feature7 still conflicted)
889+
# - After resolving feature7, feature5 is deleted (no more conflicted siblings)
890+
# ===================================================================================
891+
892+
echo >&2 "--- Testing Sibling Conflicts Scenario ---"
893+
894+
# 16. Create new stack for sibling conflict test
895+
echo >&2 "16. Creating new stack for sibling conflict test..."
896+
log_cmd git checkout main
897+
log_cmd git pull origin main
898+
899+
# Create feature5 based on main (modifies line 2, no conflict with line 5)
900+
log_cmd git checkout -b feature5 main
901+
sed -i '2s/.*/Feature 5 content line 2/' file.txt
902+
log_cmd git add file.txt
903+
log_cmd git commit -m "Add feature 5"
904+
log_cmd git push origin feature5
905+
PR5_URL=$(log_cmd gh pr create --repo "$REPO_FULL_NAME" --base main --head feature5 --title "Feature 5" --body "This is PR 5")
906+
PR5_NUM=$(echo "$PR5_URL" | awk -F'/' '{print $NF}')
907+
echo >&2 "Created PR #$PR5_NUM: $PR5_URL"
908+
909+
# Create feature6 based on feature5 (modifies line 5, will conflict with main)
910+
log_cmd git checkout -b feature6 feature5
911+
sed -i '5s/.*/Feature 6 conflicting content line 5/' file.txt
912+
log_cmd git add file.txt
913+
log_cmd git commit -m "Add feature 6 (modifies line 5)"
914+
log_cmd git push origin feature6
915+
PR6_URL=$(log_cmd gh pr create --repo "$REPO_FULL_NAME" --base feature5 --head feature6 --title "Feature 6" --body "This is PR 6, sibling of PR 7")
916+
PR6_NUM=$(echo "$PR6_URL" | awk -F'/' '{print $NF}')
917+
echo >&2 "Created PR #$PR6_NUM: $PR6_URL"
918+
919+
# Create feature7 based on feature5 (also modifies line 5, will conflict with main)
920+
log_cmd git checkout feature5
921+
log_cmd git checkout -b feature7
922+
sed -i '5s/.*/Feature 7 conflicting content line 5/' file.txt
923+
log_cmd git add file.txt
924+
log_cmd git commit -m "Add feature 7 (also modifies line 5)"
925+
log_cmd git push origin feature7
926+
PR7_URL=$(log_cmd gh pr create --repo "$REPO_FULL_NAME" --base feature5 --head feature7 --title "Feature 7" --body "This is PR 7, sibling of PR 6")
927+
PR7_NUM=$(echo "$PR7_URL" | awk -F'/' '{print $NF}')
928+
echo >&2 "Created PR #$PR7_NUM: $PR7_URL"
929+
930+
# Introduce conflicting change on main (line 5) - this will conflict with feature6/7
931+
# when the action tries to merge SQUASH_COMMIT~ into them
932+
log_cmd git checkout main
933+
sed -i '5s/.*/Main conflicting content line 5/' file.txt
934+
log_cmd git add file.txt
935+
log_cmd git commit -m "Add conflicting change on main line 5"
936+
log_cmd git push origin main
937+
938+
# 17. Merge feature5 to trigger conflicts on both siblings
939+
echo >&2 "17. Squash merging PR #$PR5_NUM (feature5) to trigger sibling conflicts..."
940+
merge_pr_with_retry "$PR5_URL"
941+
MERGE_COMMIT_SHA5=$(gh pr view "$PR5_URL" --repo "$REPO_FULL_NAME" --json mergeCommit -q .mergeCommit.oid)
942+
echo >&2 "PR #$PR5_NUM merged. Squash commit SHA: $MERGE_COMMIT_SHA5"
943+
944+
# Wait for workflow
945+
echo >&2 "Waiting for workflow..."
946+
if ! wait_for_workflow "$PR5_NUM" "feature5" "$MERGE_COMMIT_SHA5" "success"; then
947+
echo >&2 "Workflow for PR5 merge did not complete successfully."
948+
exit 1
949+
fi
950+
951+
# 18. Verify both siblings have conflicts and feature5 is kept
952+
echo >&2 "18. Verifying sibling conflict state..."
953+
log_cmd git fetch origin
954+
955+
# Verify feature5 branch was NOT deleted (both siblings conflicted)
956+
if git show-ref --verify --quiet refs/remotes/origin/feature5; then
957+
echo >&2 "✅ Verification Passed: Remote branch 'origin/feature5' still exists (kept for conflicted siblings)."
958+
else
959+
echo >&2 "❌ Verification Failed: Remote branch 'origin/feature5' was deleted prematurely."
960+
exit 1
961+
fi
962+
963+
# Verify both PRs have conflict labels
964+
PR6_HAS_LABEL=$(gh pr view "$PR6_URL" --repo "$REPO_FULL_NAME" --json labels --jq '.labels[] | select(.name == "autorestack-needs-conflict-resolution") | .name')
965+
PR7_HAS_LABEL=$(gh pr view "$PR7_URL" --repo "$REPO_FULL_NAME" --json labels --jq '.labels[] | select(.name == "autorestack-needs-conflict-resolution") | .name')
966+
967+
if [[ "$PR6_HAS_LABEL" == "autorestack-needs-conflict-resolution" ]]; then
968+
echo >&2 "✅ Verification Passed: PR #$PR6_NUM has conflict label."
969+
else
970+
echo >&2 "❌ Verification Failed: PR #$PR6_NUM does not have conflict label."
971+
exit 1
972+
fi
973+
974+
if [[ "$PR7_HAS_LABEL" == "autorestack-needs-conflict-resolution" ]]; then
975+
echo >&2 "✅ Verification Passed: PR #$PR7_NUM has conflict label."
976+
else
977+
echo >&2 "❌ Verification Failed: PR #$PR7_NUM does not have conflict label."
978+
exit 1
979+
fi
980+
981+
# Verify both PRs still have feature5 as base
982+
PR6_BASE=$(gh pr view "$PR6_NUM" --repo "$REPO_FULL_NAME" --json baseRefName --jq .baseRefName)
983+
PR7_BASE=$(gh pr view "$PR7_NUM" --repo "$REPO_FULL_NAME" --json baseRefName --jq .baseRefName)
984+
985+
if [[ "$PR6_BASE" == "feature5" && "$PR7_BASE" == "feature5" ]]; then
986+
echo >&2 "✅ Verification Passed: Both sibling PRs still have 'feature5' as base."
987+
else
988+
echo >&2 "❌ Verification Failed: PR6 base is '$PR6_BASE', PR7 base is '$PR7_BASE', expected both to be 'feature5'."
989+
exit 1
990+
fi
991+
992+
# 19. Resolve first sibling (feature6) - feature5 should still be kept
993+
echo >&2 "19. Resolving first sibling (feature6)..."
994+
log_cmd git checkout feature6
995+
log_cmd git fetch origin
996+
if git merge origin/main; then
997+
echo >&2 "Merge succeeded unexpectedly (no conflict?)"
998+
else
999+
echo >&2 "Resolving conflict on feature6..."
1000+
log_cmd git checkout --ours file.txt
1001+
log_cmd git add file.txt
1002+
log_cmd git commit --no-edit
1003+
fi
1004+
log_cmd git push origin feature6
1005+
1006+
# Wait for continuation workflow
1007+
echo >&2 "Waiting for continuation workflow for feature6..."
1008+
if ! wait_for_synchronize_workflow "$PR6_NUM" "feature6" "success"; then
1009+
echo >&2 "Continuation workflow for feature6 did not complete successfully."
1010+
exit 1
1011+
fi
1012+
1013+
# 20. Verify feature5 is still kept (feature7 still conflicted)
1014+
echo >&2 "20. Verifying feature5 is still kept after first sibling resolution..."
1015+
log_cmd git fetch origin
1016+
1017+
# feature5 should still exist
1018+
if git show-ref --verify --quiet refs/remotes/origin/feature5; then
1019+
echo >&2 "✅ Verification Passed: Remote branch 'origin/feature5' still exists (feature7 still conflicted)."
1020+
else
1021+
echo >&2 "❌ Verification Failed: Remote branch 'origin/feature5' was deleted prematurely (feature7 still needs it)."
1022+
exit 1
1023+
fi
1024+
1025+
# PR6 base should now be main
1026+
PR6_BASE_AFTER=$(gh pr view "$PR6_NUM" --repo "$REPO_FULL_NAME" --json baseRefName --jq .baseRefName)
1027+
if [[ "$PR6_BASE_AFTER" == "main" ]]; then
1028+
echo >&2 "✅ Verification Passed: PR #$PR6_NUM base updated to 'main' after resolution."
1029+
else
1030+
echo >&2 "❌ Verification Failed: PR #$PR6_NUM base is '$PR6_BASE_AFTER', expected 'main'."
1031+
exit 1
1032+
fi
1033+
1034+
# PR6 should no longer have conflict label
1035+
PR6_LABEL_AFTER=$(gh pr view "$PR6_URL" --repo "$REPO_FULL_NAME" --json labels --jq '.labels[] | select(.name == "autorestack-needs-conflict-resolution") | .name')
1036+
if [[ -z "$PR6_LABEL_AFTER" ]]; then
1037+
echo >&2 "✅ Verification Passed: PR #$PR6_NUM conflict label removed."
1038+
else
1039+
echo >&2 "❌ Verification Failed: PR #$PR6_NUM still has conflict label."
1040+
exit 1
1041+
fi
1042+
1043+
# PR7 should still have conflict label and feature5 as base
1044+
PR7_LABEL_STILL=$(gh pr view "$PR7_URL" --repo "$REPO_FULL_NAME" --json labels --jq '.labels[] | select(.name == "autorestack-needs-conflict-resolution") | .name')
1045+
PR7_BASE_STILL=$(gh pr view "$PR7_NUM" --repo "$REPO_FULL_NAME" --json baseRefName --jq .baseRefName)
1046+
if [[ "$PR7_LABEL_STILL" == "autorestack-needs-conflict-resolution" && "$PR7_BASE_STILL" == "feature5" ]]; then
1047+
echo >&2 "✅ Verification Passed: PR #$PR7_NUM still has conflict label and 'feature5' base."
1048+
else
1049+
echo >&2 "❌ Verification Failed: PR7 label='$PR7_LABEL_STILL', base='$PR7_BASE_STILL'."
1050+
exit 1
1051+
fi
1052+
1053+
# 21. Resolve second sibling (feature7) - now feature5 should be deleted
1054+
echo >&2 "21. Resolving second sibling (feature7)..."
1055+
log_cmd git checkout feature7
1056+
log_cmd git fetch origin
1057+
if git merge origin/main; then
1058+
echo >&2 "Merge succeeded unexpectedly (no conflict?)"
1059+
else
1060+
echo >&2 "Resolving conflict on feature7..."
1061+
log_cmd git checkout --ours file.txt
1062+
log_cmd git add file.txt
1063+
log_cmd git commit --no-edit
1064+
fi
1065+
log_cmd git push origin feature7
1066+
1067+
# Wait for continuation workflow
1068+
echo >&2 "Waiting for continuation workflow for feature7..."
1069+
if ! wait_for_synchronize_workflow "$PR7_NUM" "feature7" "success"; then
1070+
echo >&2 "Continuation workflow for feature7 did not complete successfully."
1071+
exit 1
1072+
fi
1073+
1074+
# 22. Verify feature5 is now deleted (all siblings resolved)
1075+
echo >&2 "22. Verifying feature5 is deleted after all siblings resolved..."
1076+
log_cmd git fetch origin --prune
1077+
1078+
if git show-ref --verify --quiet refs/remotes/origin/feature5; then
1079+
echo >&2 "❌ Verification Failed: Remote branch 'origin/feature5' still exists after all siblings resolved."
1080+
exit 1
1081+
else
1082+
echo >&2 "✅ Verification Passed: Remote branch 'origin/feature5' was deleted after all siblings resolved."
1083+
fi
1084+
1085+
# PR7 base should now be main
1086+
PR7_BASE_FINAL=$(gh pr view "$PR7_NUM" --repo "$REPO_FULL_NAME" --json baseRefName --jq .baseRefName)
1087+
if [[ "$PR7_BASE_FINAL" == "main" ]]; then
1088+
echo >&2 "✅ Verification Passed: PR #$PR7_NUM base updated to 'main' after resolution."
1089+
else
1090+
echo >&2 "❌ Verification Failed: PR #$PR7_NUM base is '$PR7_BASE_FINAL', expected 'main'."
1091+
exit 1
1092+
fi
1093+
1094+
echo >&2 "--- Sibling Conflicts Scenario Test Completed Successfully ---"
1095+
1096+
8661097
# --- Test Succeeded ---
8671098
echo >&2 "--- E2E Test Completed Successfully! ---"
8681099

0 commit comments

Comments
 (0)