Skip to content

Commit 88ffe55

Browse files
authored
fix: handle cherry-pick auto-completion after AI conflict resolution (#1041) (#1042)
## Summary - After AI resolves cherry-pick conflicts, check if cherry-pick is still in progress before running `--continue` - For modify/delete conflicts with a single file, git auto-completes the cherry-pick when the resolved file is staged - Previously this caused `cherry-pick --continue` to fail with "no cherry-pick or revert in progress" ## Test plan - [x] New test `test_cherry_pick_ai_resolves_modify_delete_conflict` validates the auto-complete scenario - [x] Existing cherry-pick AI tests continue to pass - [x] Full test suite passes (1435 tests, 90.40% coverage) Closes #1041 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Enhanced cherry-pick conflict resolution with pre-verification of cherry-pick state before finalizing operations. * Improved handling of already-completed cherry-picks with distinct logging for in-progress vs completed scenarios. * **Tests** * Added comprehensive test coverage for modify/delete conflict scenarios during cherry-pick resolution. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 38422af commit 88ffe55

2 files changed

Lines changed: 89 additions & 6 deletions

File tree

webhook_server/libs/handlers/runner_handler.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -784,16 +784,27 @@ async def _resolve_cherry_pick_with_ai(
784784
self.logger.error(f"{self.log_prefix} Failed to stage AI-resolved files: {err}")
785785
return False
786786

787-
# Complete the cherry-pick
788-
rc, _, err = await run_command(
789-
command=f"{git_cmd} -c core.editor=true cherry-pick --continue",
787+
# Check if cherry-pick is still in progress (it may have auto-completed
788+
# after staging resolved files, e.g. for modify/delete conflicts)
789+
rc_check, _, _ = await run_command(
790+
command=f"{git_cmd} rev-parse --verify CHERRY_PICK_HEAD",
790791
log_prefix=self.log_prefix,
791792
redact_secrets=[github_token],
792793
mask_sensitive=self.github_webhook.mask_sensitive,
793794
)
794-
if not rc:
795-
self.logger.error(f"{self.log_prefix} cherry-pick --continue failed after AI resolution: {err}")
796-
return False
795+
if rc_check:
796+
# Cherry-pick still in progress, finalize it
797+
rc, _, err = await run_command(
798+
command=f"{git_cmd} -c core.editor=true cherry-pick --continue",
799+
log_prefix=self.log_prefix,
800+
redact_secrets=[github_token],
801+
mask_sensitive=self.github_webhook.mask_sensitive,
802+
)
803+
if not rc:
804+
self.logger.error(f"{self.log_prefix} cherry-pick --continue failed after AI resolution: {err}")
805+
return False
806+
else:
807+
self.logger.info(f"{self.log_prefix} Cherry-pick already completed after staging resolved files")
797808

798809
self.logger.info(f"{self.log_prefix} AI successfully resolved cherry-pick conflicts")
799810
return True

webhook_server/tests/test_runner_handler.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,6 +1528,78 @@ async def run_command_side_effect(command: str, **kwargs: Any) -> tuple[bool, st
15281528
assert "ai-resolved-conflicts" in labels_call_str
15291529
assert "CherryPicked-from-main" in labels_call_str
15301530

1531+
@pytest.mark.asyncio
1532+
async def test_cherry_pick_ai_resolves_modify_delete_conflict(
1533+
self, runner_handler: RunnerHandler, mock_pull_request: Mock
1534+
) -> None:
1535+
"""Cherry-pick modify/delete conflict — AI resolves, cherry-pick auto-completes without --continue."""
1536+
runner_handler.github_webhook.ai_features = {
1537+
"ai-provider": "claude",
1538+
"ai-model": "sonnet",
1539+
"resolve-cherry-pick-conflicts-with-ai": {"enabled": True, "timeout-minutes": 10},
1540+
}
1541+
1542+
async def run_command_side_effect(command: str, **kwargs: Any) -> tuple[bool, str, str]:
1543+
# Fail on cherry-pick (conflict)
1544+
if "cherry-pick" in command and "--continue" not in command and "rev-parse" not in command:
1545+
return (False, "", "CONFLICT (modify/delete): file.sh deleted in HEAD and modified in abc1234")
1546+
# CHERRY_PICK_HEAD does not exist — cherry-pick auto-completed
1547+
if "rev-parse --verify CHERRY_PICK_HEAD" in command:
1548+
return (False, "", "fatal: Needed a single revision")
1549+
if "gh pr create" in command:
1550+
return (True, "https://github.com/test-org/test-repo/pull/99", "")
1551+
return (True, "success", "")
1552+
1553+
with patch.object(runner_handler, "is_branch_exists", new=AsyncMock(return_value=Mock())):
1554+
with patch.object(runner_handler.check_run_handler, "set_check_in_progress"):
1555+
with patch.object(runner_handler.check_run_handler, "set_check_success") as mock_set_success:
1556+
with patch.object(runner_handler, "_checkout_worktree") as mock_checkout:
1557+
mock_checkout.return_value = AsyncMock()
1558+
mock_checkout.return_value.__aenter__ = AsyncMock(
1559+
return_value=(True, "/tmp/worktree-path", "", "")
1560+
)
1561+
mock_checkout.return_value.__aexit__ = AsyncMock(return_value=None)
1562+
with patch(
1563+
"webhook_server.libs.handlers.runner_handler.run_command",
1564+
new=AsyncMock(side_effect=run_command_side_effect),
1565+
) as mock_run_cmd:
1566+
with patch(
1567+
"webhook_server.libs.handlers.runner_handler.call_ai_cli",
1568+
new=AsyncMock(return_value=(True, "resolved")),
1569+
) as mock_ai_cli:
1570+
with patch(
1571+
"asyncio.to_thread",
1572+
new=AsyncMock(side_effect=lambda fn, *a, **kw: fn(*a, **kw) if a or kw else fn()),
1573+
):
1574+
with patch(
1575+
"webhook_server.libs.handlers.runner_handler.get_repository_github_app_token",
1576+
return_value=None,
1577+
):
1578+
await runner_handler.cherry_pick(mock_pull_request, "main")
1579+
mock_set_success.assert_called_once()
1580+
mock_ai_cli.assert_called_once()
1581+
# Verify cherry-pick --continue was NOT called
1582+
continue_calls = [
1583+
c for c in mock_run_cmd.call_args_list if "cherry-pick --continue" in str(c)
1584+
]
1585+
assert not continue_calls, (
1586+
"cherry-pick --continue should not be called"
1587+
" when cherry-pick auto-completed"
1588+
)
1589+
# Verify CHERRY_PICK_HEAD check was called
1590+
rev_parse_calls = [
1591+
c for c in mock_run_cmd.call_args_list if "rev-parse" in str(c)
1592+
]
1593+
assert rev_parse_calls, (
1594+
"CHERRY_PICK_HEAD check (rev-parse) should have been called"
1595+
)
1596+
# Verify AI comment was posted
1597+
comment_calls = mock_pull_request.create_issue_comment.call_args_list
1598+
ai_comment = any(
1599+
"Cherry-pick conflicts were resolved by AI" in str(c) for c in comment_calls
1600+
)
1601+
assert ai_comment, f"Expected AI comment, got: {comment_calls}"
1602+
15311603
@pytest.mark.asyncio
15321604
async def test_cherry_pick_ai_fails_fallback(self, runner_handler: RunnerHandler, mock_pull_request: Mock) -> None:
15331605
"""Cherry-pick conflicts + AI fails — falls back to manual instructions."""

0 commit comments

Comments
 (0)