diff --git a/webhook_server/libs/handlers/runner_handler.py b/webhook_server/libs/handlers/runner_handler.py index 2d3685f8..9e1f9112 100644 --- a/webhook_server/libs/handlers/runner_handler.py +++ b/webhook_server/libs/handlers/runner_handler.py @@ -75,6 +75,7 @@ async def _checkout_worktree( is_merged: bool = False, checkout: str = "", tag_name: str = "", + skip_merge: bool = False, ) -> AsyncGenerator[tuple[bool, str, str, str]]: """Create worktree from existing clone for handler operations. @@ -86,6 +87,8 @@ async def _checkout_worktree( is_merged: Whether PR is merged checkout: Specific branch/commit to checkout tag_name: Tag name to checkout + skip_merge: Skip merging base branch into worktree (used by cherry-pick + which manages its own branch setup) Yields: tuple: (success: bool, worktree_path: str, stdout: str, stderr: str) @@ -148,7 +151,7 @@ async def _checkout_worktree( result: tuple[bool, str, str, str] = (success, worktree_path, out, err) # Merge base branch if needed (for PR testing) - if success and pull_request and not is_merged and not tag_name: + if success and pull_request and not is_merged and not tag_name and not skip_merge: merge_ref = base_ref if merge_ref is None: merge_ref = await asyncio.to_thread(lambda: pull_request.base.ref) @@ -850,7 +853,12 @@ async def cherry_pick( pull_request_url = pull_request.html_url github_token = self.github_webhook.token - async with self._checkout_worktree(pull_request=pull_request) as (success, worktree_path, out, err): + async with self._checkout_worktree(pull_request=pull_request, skip_merge=True) as ( + success, + worktree_path, + out, + err, + ): git_cmd = f"git --work-tree={worktree_path} --git-dir={worktree_path}/.git" pr_title = f"{CHERRY_PICKED_LABEL}: [{target_branch}] {commit_msg_striped}" pr_body = ( diff --git a/webhook_server/tests/test_runner_handler.py b/webhook_server/tests/test_runner_handler.py index 1b6bc924..203ede24 100644 --- a/webhook_server/tests/test_runner_handler.py +++ b/webhook_server/tests/test_runner_handler.py @@ -1151,6 +1151,62 @@ async def test_checkout_worktree_merge_failure(self, runner_handler, mock_pull_r assert not success assert out == "fail" + @pytest.mark.asyncio + async def test_checkout_worktree_skip_merge(self, runner_handler: RunnerHandler, mock_pull_request: Mock) -> None: + """Test _checkout_worktree with skip_merge=True skips the merge step.""" + with patch("webhook_server.utils.helpers.git_worktree_checkout") as mock_git_worktree: + mock_git_worktree.return_value.__aenter__ = AsyncMock( + return_value=(True, "/tmp/worktree-path", "success", "") + ) + mock_git_worktree.return_value.__aexit__ = AsyncMock(return_value=None) + with patch( + "webhook_server.libs.handlers.runner_handler.run_command", + new=AsyncMock(return_value=(True, "some-branch", "")), + ) as mock_run_cmd: + async with runner_handler._checkout_worktree(pull_request=mock_pull_request, skip_merge=True) as result: + success, worktree_path, _, _ = result + assert success is True + assert worktree_path == "/tmp/worktree-path" + # Verify merge command was NOT called (only rev-parse for branch check) + for call in mock_run_cmd.call_args_list: + cmd = call.kwargs.get("command", call.args[0] if call.args else "") + assert "merge" not in cmd, ( + f"Merge command should not be called with skip_merge=True, got: {cmd}" + ) + + @pytest.mark.asyncio + async def test_cherry_pick_calls_checkout_worktree_with_skip_merge( + self, runner_handler: RunnerHandler, mock_pull_request: Mock + ) -> None: + """Test cherry_pick passes skip_merge=True to _checkout_worktree.""" + runner_handler.github_webhook.pypi = {"token": "dummy"} + with patch.object(runner_handler, "is_branch_exists", new=AsyncMock(return_value=Mock())): + with patch.object(runner_handler.check_run_handler, "set_check_in_progress"): + with patch.object(runner_handler.check_run_handler, "set_check_success"): + with patch.object(runner_handler, "_checkout_worktree") as mock_checkout: + mock_checkout.return_value = AsyncMock() + mock_checkout.return_value.__aenter__ = AsyncMock( + return_value=(True, "/tmp/worktree-path", "", "") + ) + mock_checkout.return_value.__aexit__ = AsyncMock(return_value=None) + with patch( + "webhook_server.libs.handlers.runner_handler.run_command", + new=AsyncMock(return_value=(True, "https://github.com/test/repo/pull/99", "")), + ): + with patch.object(mock_pull_request, "create_issue_comment", new=Mock()): + with patch( + "asyncio.to_thread", + new=AsyncMock(side_effect=lambda fn, *a, **kw: fn(*a, **kw) if a or kw else fn()), + ): + with patch( + "webhook_server.libs.handlers.runner_handler.get_repository_github_app_token", + return_value=None, + ): + await runner_handler.cherry_pick(mock_pull_request, "main") + mock_checkout.assert_called_once_with( + pull_request=mock_pull_request, skip_merge=True + ) + @pytest.mark.asyncio async def test_run_build_container_push_failure(self, runner_handler, mock_pull_request): runner_handler.github_webhook.pypi = {"token": "dummy"}