orbi-cli 0.7.8__tar.gz → 0.7.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/PKG-INFO +1 -1
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/pyproject.toml +1 -1
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/__init__.py +1 -1
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/journal.py +3 -1
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/review_merge.py +49 -27
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/run_state.py +19 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/PKG-INFO +1 -1
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_git_merge_smoke.py +138 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_review_merge.py +103 -15
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/LICENSE +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/README.md +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/docs/licenses/sustainable-use-license.md +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/setup.cfg +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/blocked_comment.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/branch_reclaim.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/checks.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/claim.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/clarify.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/cli.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/cli_source.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/comment_kind.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/config.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/delivery_comment.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/delivery_labels.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/delivery_scene.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/engine_source.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/example_config.toml +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/failure.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/failure_report.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/gh_response.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/git_identity.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/git_transport.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/github.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/gitops.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/human_review.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/language.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/language_terms.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/launchd_deploy.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/merge_gate.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/merge_handoff.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/milestone.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/milestone_command.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/notify.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pi_activity.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pi_command.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pi_process.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pi_recovery.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pi_session.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pilot_setup.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/pilot_slots.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/progress.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/progress_display.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/progress_outcome.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/progress_summary.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/release.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/release_comments.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/release_git.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/release_notes.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/repo_config.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/resume_scan.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/review_rounds.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/runner.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/runner_health.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/scene.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/scheduler.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/scheduler_units.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/session_command.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/source_pool.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/status_observe.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/suggest.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/systemd_deploy.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi/ticket_command.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/SOURCES.txt +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/dependency_links.txt +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/entry_points.txt +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/src/orbi_cli.egg-info/top_level.txt +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_agents_md.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_apply_label_patch.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_base_branch_failure.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_blocked_comment.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_bootstrap_adapters.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_bootstrap_delivery.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_bootstrap_pi.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_bootstrap_preflight.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_bootstrap_release.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_bootstrap_runner.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_branch_reclaim.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_ci_failure_issue_workflow.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_ci_failure_triage.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_ci_macos_workflow.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_ci_workflow.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_clarify.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_claude_plugin.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_cli_install.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_cli_milestone.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_cli_packaging.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_cli_source.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_comment_identity.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_comment_readers_contract.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_concurrency_e2e.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_constitution.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_coverage_gate.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_deliver_pr_fakes.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_delivery_branch_delete.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_delivery_labels.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_delivery_scene.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docker_docs.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docker_entrypoint.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docker_image_workflow.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_i18n.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_issue_shapes.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_labels.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_mermaid.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_releases.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_single_source.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_docs_site.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_engine_source.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_failure.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_failure_report.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_fix_needed_loop.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_gh_read_retry.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_gh_response.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_git_base_smoke.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_git_network_retry.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_git_transport.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_github.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_github_fakes.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_gitops.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_gitops_fakes.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_human_review.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_import_graph.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_install_sh.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_issue_1367.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_issue_461.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_issue_template_config.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_journal.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_journal_events.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_launchd_deploy.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_license.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_message_language.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_milestone_command.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_notify.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_ops_e2e.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_orbi.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_orbi_dashboard.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_orbi_exporter.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_patch_ratchet.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pi_activity.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pi_command.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pi_extensions.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pi_provider_templates.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pi_recovery.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pi_session.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pilot_setup.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pilot_slots.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_pr_issue_link_workflow.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_prerequisite_check.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_progress.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_progress_wiring.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_prompt_contract.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_public_contracts.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_publish_workflow.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_release_docs_workflow.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_release_ticket_templates.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_repo_config.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_resume_continue_e2e.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_resume_e2e.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_resume_pr.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_run_artifacts.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_run_id_e2e.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_runner_config.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_runner_health.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_runner_source_freshness.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_scene.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_scheduler.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_size_ratchet.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_src_layout.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_status_observe.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_steering_review.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_suggest.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_suite_hygiene.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_systemd_deploy.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_systemd_timer.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_ticket_command.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_user_acceptance_contract.py +0 -0
- {orbi_cli-0.7.8 → orbi_cli-0.7.9}/tests/test_worktree_reclaim.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: orbi-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.9
|
|
4
4
|
Summary: Orbi — local AI development worker: claims GitHub Issues, runs Pi in isolated worktrees, and delivers PRs (GitHub Issues + labels are the only state store).
|
|
5
5
|
Author: xqliu
|
|
6
6
|
License-Expression: AGPL-3.0-only OR LicenseRef-Sustainable-Use-1.0
|
|
@@ -44,7 +44,7 @@ build-backend = "setuptools.build_meta"
|
|
|
44
44
|
|
|
45
45
|
[project]
|
|
46
46
|
name = "orbi-cli"
|
|
47
|
-
version = "0.7.
|
|
47
|
+
version = "0.7.9"
|
|
48
48
|
description = "Orbi — local AI development worker: claims GitHub Issues, runs Pi in isolated worktrees, and delivers PRs (GitHub Issues + labels are the only state store)."
|
|
49
49
|
readme = "README.md"
|
|
50
50
|
requires-python = ">=3.14"
|
|
@@ -364,9 +364,11 @@ JOURNAL_EVENTS: dict[str, str] = {
|
|
|
364
364
|
"merge_gate_issue_blocked": "the source Issue is ai-blocked; the merge was skipped (Issue #1504)",
|
|
365
365
|
"merge_gate_rulesets_unavailable": "the rulesets API is unavailable on the repository plan (Issue #1361)",
|
|
366
366
|
"merged": "the PR merge landed on the reviewed head",
|
|
367
|
+
"merge_landed_after_error": "the merge call failed but the reviewed head landed on the base branch (Issue #1798)",
|
|
367
368
|
"merge_method_unknown": "the repository merge settings could not be read; --merge is used",
|
|
368
369
|
"confirm_merged_missing_on_base": "the merged commit is missing from the base branch",
|
|
369
|
-
"confirm_merged_not_merged": "the PR
|
|
370
|
+
"confirm_merged_not_merged": "the PR is not merged and its reviewed head is not on the base branch",
|
|
371
|
+
"confirm_merged_landed_on_base": "the reviewed head landed on the base branch while the PR still reads unmerged (Issue #1798)",
|
|
370
372
|
# Human review gate.
|
|
371
373
|
"human_review_waiting": "the delivery waits for the human review label",
|
|
372
374
|
"human_review_checklist_failed": "publishing the human review checklist failed (bypass)",
|
|
@@ -673,20 +673,32 @@ def merge_gate(worktree: Path, pr: dict, base_branch: str,
|
|
|
673
673
|
"--match-head-commit", pr["head_oid"], merge_method,
|
|
674
674
|
], cwd=worktree)
|
|
675
675
|
except subprocess.CalledProcessError as exc:
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
676
|
+
# Issue #1798: GitHub can create the merge commit and this call still
|
|
677
|
+
# fail (`502 Bad Gateway`), leaving the PR OPEN with `mergedAt: null`.
|
|
678
|
+
# `confirm_merged` decides: landed continues like a success (never a
|
|
679
|
+
# second merge call), "not landed" keeps today's path.
|
|
680
|
+
try:
|
|
681
|
+
confirm_merged(worktree, pr, base_branch, repo_dir=repo_dir)
|
|
682
|
+
landed = True
|
|
683
|
+
except RuntimeError:
|
|
684
|
+
landed = False
|
|
685
|
+
if not landed:
|
|
686
|
+
preflight = github.merge_gate_preflight(merge_repo, base_branch)
|
|
687
|
+
stderr = str(exc.stderr or "")
|
|
688
|
+
if is_maintainer_actionable(stderr, preflight):
|
|
689
|
+
failed_preflight = [
|
|
690
|
+
line for line in preflight
|
|
691
|
+
if line.startswith("merge_gate: FAILED")
|
|
692
|
+
]
|
|
693
|
+
raise MergeHandoffRequired(
|
|
694
|
+
f"PR #{pr['number']} is ready for the named maintainer "
|
|
695
|
+
"action",
|
|
696
|
+
preflight=failed_preflight,
|
|
697
|
+
) from None
|
|
698
|
+
raise
|
|
699
|
+
event("merge_landed_after_error", level=logging.WARNING,
|
|
700
|
+
pr=pr["number"], head=pr["head_oid"], method=merge_method,
|
|
701
|
+
returncode=exc.returncode, stderr=str(exc.stderr or "").strip())
|
|
690
702
|
event("merged", pr=pr["number"], head=pr["head_oid"],
|
|
691
703
|
method=merge_method)
|
|
692
704
|
delete_merged_delivery_branch(merge_repo, pr["number"], pr["head_ref"])
|
|
@@ -695,27 +707,37 @@ def merge_gate(worktree: Path, pr: dict, base_branch: str,
|
|
|
695
707
|
|
|
696
708
|
def confirm_merged(worktree: Path, pr: dict, base_branch: str,
|
|
697
709
|
*, repo_dir: Path) -> dict:
|
|
698
|
-
"""Confirm the PR
|
|
710
|
+
"""Confirm the reviewed PR's merge landed on origin/<base>.
|
|
711
|
+
|
|
712
|
+
A MERGED PR answers with its merge commit, which must be on the base
|
|
713
|
+
branch. Issue #1798: the merge can also land while the merge call
|
|
714
|
+
itself errors, leaving the PR OPEN/CLOSED with `mergedAt: null` — the
|
|
715
|
+
reviewed head being on origin/<base> after the fresh fetch is that
|
|
716
|
+
merge. Anything else is a rejection; a failed read propagates.
|
|
699
717
|
|
|
700
|
-
The base fetch updates the shared remote-tracking ref, so it runs
|
|
701
|
-
|
|
702
|
-
as the lock location.
|
|
718
|
+
The base fetch updates the shared remote-tracking ref, so it runs under
|
|
719
|
+
the base-sync lock with the deployment checkout as the lock location.
|
|
703
720
|
"""
|
|
704
721
|
state = pr_view(pr["number"], "state,mergedAt,mergeCommit", cwd=worktree)
|
|
705
|
-
|
|
706
|
-
event(
|
|
707
|
-
"confirm_merged_not_merged", level=logging.ERROR,
|
|
708
|
-
pr=pr["number"], state=state.get("state"),
|
|
709
|
-
)
|
|
710
|
-
raise RuntimeError(
|
|
711
|
-
f"PR #{pr['number']} is not merged (state={state.get('state')})"
|
|
712
|
-
)
|
|
722
|
+
merged = state.get("state") == "MERGED" and bool(state.get("mergedAt"))
|
|
713
723
|
merge_commit = (state.get("mergeCommit") or {}).get("oid")
|
|
714
|
-
if not merge_commit:
|
|
724
|
+
if merged and not merge_commit:
|
|
715
725
|
raise RuntimeError(
|
|
716
726
|
f"PR #{pr['number']} is merged but has no merge commit oid"
|
|
717
727
|
)
|
|
718
728
|
fetch_base_ref(repo_dir, base_branch, cwd=worktree)
|
|
729
|
+
if not merged:
|
|
730
|
+
# Issue #1798: it can have landed while the merge call errored.
|
|
731
|
+
merge_commit = run_state.landed_merge_commit(
|
|
732
|
+
pr["head_oid"], f"origin/{base_branch}", cwd=worktree)
|
|
733
|
+
if merge_commit is None:
|
|
734
|
+
event("confirm_merged_not_merged", level=logging.ERROR,
|
|
735
|
+
pr=pr["number"], state=state.get("state"))
|
|
736
|
+
raise RuntimeError(f"PR #{pr['number']} is not merged "
|
|
737
|
+
f"(state={state.get('state')})")
|
|
738
|
+
event("confirm_merged_landed_on_base", pr=pr["number"],
|
|
739
|
+
head=pr["head_oid"], merge_commit=merge_commit)
|
|
740
|
+
return {"state": "MERGED", "merge_commit": merge_commit}
|
|
719
741
|
if not _is_ancestor(merge_commit, f"origin/{base_branch}", cwd=worktree):
|
|
720
742
|
event(
|
|
721
743
|
"confirm_merged_missing_on_base", level=logging.ERROR,
|
|
@@ -58,6 +58,25 @@ def assess_base_freshness(worktree: Path, base_branch: str, *,
|
|
|
58
58
|
return BaseFreshness.ABSORBABLE
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def landed_merge_commit(head: str, base_ref: str, *,
|
|
62
|
+
cwd: Path) -> str | None:
|
|
63
|
+
"""The commit that landed *head* on *base_ref*, or None (Issue #1798).
|
|
64
|
+
|
|
65
|
+
GitHub can create the merge commit while the merge call itself fails
|
|
66
|
+
(`502 Bad Gateway`), so the PR state alone cannot show that merge: the
|
|
67
|
+
head being reachable from the base branch is it. `--topo-order
|
|
68
|
+
--reverse` lists parents before children on that ancestry path, so the
|
|
69
|
+
first commit is the one that landed the head.
|
|
70
|
+
"""
|
|
71
|
+
if not _is_ancestor(head, base_ref, cwd=cwd):
|
|
72
|
+
return None
|
|
73
|
+
commits = run_command([
|
|
74
|
+
"git", "rev-list", "--ancestry-path", "--topo-order", "--reverse",
|
|
75
|
+
f"{head}..{base_ref}",
|
|
76
|
+
], cwd=cwd).split()
|
|
77
|
+
return commits[0] if commits else None
|
|
78
|
+
|
|
79
|
+
|
|
61
80
|
def delivery_commit_guard(worktree: Path, *, local_head: str,
|
|
62
81
|
base_sha: str, branch: str) -> None:
|
|
63
82
|
"""Fail the delivery when the task branch carries no real change.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: orbi-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.9
|
|
4
4
|
Summary: Orbi — local AI development worker: claims GitHub Issues, runs Pi in isolated worktrees, and delivers PRs (GitHub Issues + labels are the only state store).
|
|
5
5
|
Author: xqliu
|
|
6
6
|
License-Expression: AGPL-3.0-only OR LicenseRef-Sustainable-Use-1.0
|
|
@@ -307,3 +307,141 @@ def test_sync_base_checkout_fails_fast_on_drifted_checkout(
|
|
|
307
307
|
|
|
308
308
|
with pytest.raises(RuntimeError, match="cannot fast-forward"):
|
|
309
309
|
review_merge.sync_base_checkout(checkout, "main")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
# Issue #1798: a merge that landed despite an error from the merge call
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
def install_failing_merge_gh(monkeypatch, clone: Path, head_oid: str, *,
|
|
317
|
+
landed: bool = True, pr_after=None) -> list:
|
|
318
|
+
"""Run real git; `gh pr merge` lands a real merge and THEN fails.
|
|
319
|
+
|
|
320
|
+
This is the Issue #1798 scene (orbi-build/orbi-cloud PR #2652): GitHub
|
|
321
|
+
creates the merge commit on the base branch and the merge call itself
|
|
322
|
+
returns `502 Bad Gateway`. `landed=False` models the same error with
|
|
323
|
+
nothing on the base branch. `pr_after` answers `gh pr view` once the
|
|
324
|
+
merge call has failed; before it the reviewed head reads OPEN and
|
|
325
|
+
mergeable.
|
|
326
|
+
"""
|
|
327
|
+
real_run = runner.run_command
|
|
328
|
+
calls: list = []
|
|
329
|
+
failed = False
|
|
330
|
+
|
|
331
|
+
def fake_run(command, **kwargs):
|
|
332
|
+
nonlocal failed
|
|
333
|
+
if command[0] == "gh" and command[1:3] == ["pr", "view"]:
|
|
334
|
+
calls.append(command)
|
|
335
|
+
if failed and pr_after is not None:
|
|
336
|
+
return pr_after()
|
|
337
|
+
return make_pr(head_oid)
|
|
338
|
+
if command[0] == "gh" and command[1] == "pr" and "merge" in command:
|
|
339
|
+
calls.append(command)
|
|
340
|
+
if landed:
|
|
341
|
+
git(clone, "checkout", "main")
|
|
342
|
+
git(clone, "merge", "--no-ff", command[command.index(
|
|
343
|
+
"--match-head-commit") + 1])
|
|
344
|
+
git(clone, "push", "origin", "main")
|
|
345
|
+
git(clone, "checkout", "-")
|
|
346
|
+
failed = True
|
|
347
|
+
raise subprocess.CalledProcessError(
|
|
348
|
+
1, command, stderr="HTTP 502: Bad Gateway",
|
|
349
|
+
)
|
|
350
|
+
if command[0] == "gh":
|
|
351
|
+
calls.append(command)
|
|
352
|
+
return ""
|
|
353
|
+
return real_run(command, **kwargs)
|
|
354
|
+
|
|
355
|
+
monkeypatch.setattr(seam, "run_command", fake_run)
|
|
356
|
+
return calls
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _delivery_pr(clone: Path, head_oid: str) -> dict:
|
|
360
|
+
return {"number": 4, "url": "u", "base_ref": "main",
|
|
361
|
+
"base_oid": git(clone, "rev-parse", "origin/main"),
|
|
362
|
+
"head_ref": "orbi/owner-repo-issue-4", "head_oid": head_oid}
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def test_merge_gate_accepts_a_merge_that_landed_despite_a_502(
|
|
366
|
+
clone, monkeypatch, caplog):
|
|
367
|
+
"""Issue #1798 acceptance 1: `gh pr merge` fails 5xx and the PR then
|
|
368
|
+
reads MERGED with its merge commit on origin/<base> — the delivery
|
|
369
|
+
continues like a successful merge instead of a fix-needed round."""
|
|
370
|
+
git(clone, "checkout", "-b", "orbi/owner-repo-issue-4")
|
|
371
|
+
head_oid = commit_file(clone, "delivery.txt", "delivery")
|
|
372
|
+
calls = install_failing_merge_gh(
|
|
373
|
+
monkeypatch, clone, head_oid,
|
|
374
|
+
pr_after=lambda: make_pr(
|
|
375
|
+
head_oid, state="MERGED", merged_at="2026-10-19T13:38:31Z",
|
|
376
|
+
merge_commit=git(clone, "rev-parse", "origin/main"),
|
|
377
|
+
),
|
|
378
|
+
)
|
|
379
|
+
with caplog.at_level("WARNING"):
|
|
380
|
+
merged = review_merge.merge_gate(
|
|
381
|
+
clone, _delivery_pr(clone, head_oid), "main",
|
|
382
|
+
repo_dir=clone, language_code="en",
|
|
383
|
+
)
|
|
384
|
+
assert merged["merged"] is True
|
|
385
|
+
assert "merge_landed_after_error" in caplog.text
|
|
386
|
+
# Still exactly one merge call: the error is confirmed, never retried.
|
|
387
|
+
assert len([c for c in calls
|
|
388
|
+
if c[0] == "gh" and c[1] == "pr" and "merge" in c]) == 1
|
|
389
|
+
merge_commit = git(clone, "rev-parse", "origin/main")
|
|
390
|
+
confirmed = review_merge.confirm_merged(clone, merged, "main",
|
|
391
|
+
repo_dir=clone)
|
|
392
|
+
assert confirmed == {"state": "MERGED", "merge_commit": merge_commit}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def test_merge_gate_accepts_a_landed_head_while_the_pr_reads_open(
|
|
396
|
+
clone, monkeypatch, caplog):
|
|
397
|
+
"""Issue #1798 acceptance 2 (PR #2652's shape): GitHub created the merge
|
|
398
|
+
commit on the base branch and never marked the PR merged, so re-reading
|
|
399
|
+
the PR state cannot recognise the merge. The reviewed head being
|
|
400
|
+
reachable from origin/<base> does, and the landed merge commit is the
|
|
401
|
+
one the delivery records."""
|
|
402
|
+
git(clone, "checkout", "-b", "orbi/owner-repo-issue-4")
|
|
403
|
+
head_oid = commit_file(clone, "delivery.txt", "delivery")
|
|
404
|
+
calls = install_failing_merge_gh(monkeypatch, clone, head_oid)
|
|
405
|
+
with caplog.at_level("INFO"):
|
|
406
|
+
merged = review_merge.merge_gate(
|
|
407
|
+
clone, _delivery_pr(clone, head_oid), "main",
|
|
408
|
+
repo_dir=clone, language_code="en",
|
|
409
|
+
)
|
|
410
|
+
assert merged["merged"] is True
|
|
411
|
+
assert len([c for c in calls
|
|
412
|
+
if c[0] == "gh" and c[1] == "pr" and "merge" in c]) == 1
|
|
413
|
+
merge_commit = git(clone, "rev-parse", "origin/main")
|
|
414
|
+
# The commit that landed the reviewed head is a real merge commit whose
|
|
415
|
+
# second parent is that head.
|
|
416
|
+
assert git(clone, "rev-parse", f"{merge_commit}^2") == head_oid
|
|
417
|
+
with caplog.at_level("INFO"):
|
|
418
|
+
confirmed = review_merge.confirm_merged(clone, merged, "main",
|
|
419
|
+
repo_dir=clone)
|
|
420
|
+
assert confirmed == {"state": "MERGED", "merge_commit": merge_commit}
|
|
421
|
+
assert "confirm_merged_landed_on_base" in caplog.text
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def test_merge_gate_still_fails_when_the_merge_did_not_land(
|
|
425
|
+
clone, monkeypatch):
|
|
426
|
+
"""Issue #1798 acceptance 3: the same 5xx with the reviewed head NOT on
|
|
427
|
+
origin/<base> keeps today's outcome — the error surfaces."""
|
|
428
|
+
git(clone, "checkout", "-b", "orbi/owner-repo-issue-4")
|
|
429
|
+
head_oid = commit_file(clone, "delivery.txt", "delivery")
|
|
430
|
+
pr = _delivery_pr(clone, head_oid)
|
|
431
|
+
calls = install_failing_merge_gh(monkeypatch, clone, head_oid,
|
|
432
|
+
landed=False)
|
|
433
|
+
monkeypatch.setattr(
|
|
434
|
+
seam, "merge_gate_preflight",
|
|
435
|
+
lambda *args: ["merge_gate: PASS protection readable"],
|
|
436
|
+
)
|
|
437
|
+
with pytest.raises(subprocess.CalledProcessError) as raised:
|
|
438
|
+
review_merge.merge_gate(
|
|
439
|
+
clone, pr, "main", repo_dir=clone, language_code="en",
|
|
440
|
+
)
|
|
441
|
+
assert raised.value.returncode == 1
|
|
442
|
+
assert "502" in (raised.value.stderr or "")
|
|
443
|
+
assert len([c for c in calls
|
|
444
|
+
if c[0] == "gh" and c[1] == "pr" and "merge" in c]) == 1
|
|
445
|
+
# Nothing landed: the base branch is untouched.
|
|
446
|
+
assert git(clone, "rev-parse", "origin/main") == pr["base_oid"]
|
|
447
|
+
|
|
@@ -1676,6 +1676,13 @@ def test_merge_gate_hands_off_each_actionable_policy_rejection(
|
|
|
1676
1676
|
raise subprocess.CalledProcessError(
|
|
1677
1677
|
1, command, stderr="the base branch policy prohibits the merge",
|
|
1678
1678
|
)
|
|
1679
|
+
if command[0] == "git" and command[1] == "merge-base":
|
|
1680
|
+
# The head contains the base, but the base does NOT contain
|
|
1681
|
+
# the reviewed head: this merge did not land (Issue #1798 asks
|
|
1682
|
+
# the ancestry question before treating the error as final).
|
|
1683
|
+
if command[3] == "origin/main":
|
|
1684
|
+
return ""
|
|
1685
|
+
raise subprocess.CalledProcessError(1, command, stderr="no")
|
|
1679
1686
|
return ""
|
|
1680
1687
|
monkeypatch.setattr(seam, "run_command", fake_run)
|
|
1681
1688
|
monkeypatch.setattr(runner.github, "merge_gate_preflight", lambda *a: [
|
|
@@ -1697,15 +1704,25 @@ def test_merge_gate_hands_off_each_actionable_policy_rejection(
|
|
|
1697
1704
|
])
|
|
1698
1705
|
def test_merge_gate_does_not_hand_off_without_failed_preflight(
|
|
1699
1706
|
monkeypatch, tmp_path, preflight):
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1707
|
+
def fake_run(command, **kwargs):
|
|
1708
|
+
if command[0] == "gh" and command[1] == "pr" and "merge" in command:
|
|
1709
|
+
raise subprocess.CalledProcessError(
|
|
1710
|
+
1, command, stderr="the base branch policy prohibits the merge",
|
|
1711
|
+
)
|
|
1712
|
+
if command[0] == "gh" and command[1] == "pr" and "view" in command:
|
|
1713
|
+
return json.dumps({
|
|
1714
|
+
"state": "OPEN", "mergeable": "MERGEABLE", "headRefOid": "h1",
|
|
1715
|
+
"statusCheckRollup": [],
|
|
1716
|
+
})
|
|
1717
|
+
if command[0] == "git" and command[1] == "merge-base":
|
|
1718
|
+
# The head contains the base, but the base does NOT contain the
|
|
1719
|
+
# reviewed head: the failed merge did not land (Issue #1798).
|
|
1720
|
+
if command[3] == "origin/main":
|
|
1721
|
+
return ""
|
|
1722
|
+
raise subprocess.CalledProcessError(1, command, stderr="no")
|
|
1723
|
+
return ""
|
|
1724
|
+
|
|
1725
|
+
monkeypatch.setattr(seam, "run_command", fake_run)
|
|
1709
1726
|
monkeypatch.setattr(
|
|
1710
1727
|
runner.github, "merge_gate_preflight", lambda *a: preflight,
|
|
1711
1728
|
)
|
|
@@ -2009,12 +2026,21 @@ def test_confirm_merged_fetches_under_the_base_sync_lock(
|
|
|
2009
2026
|
|
|
2010
2027
|
|
|
2011
2028
|
def test_confirm_merged_rejects_unmerged_pr(monkeypatch, tmp_path):
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2029
|
+
"""Issue #1798: an unmerged PR whose reviewed head is NOT on
|
|
2030
|
+
origin/<base> is still a failed confirm — the head's ancestry is the
|
|
2031
|
+
fact that decides it, so this fixture answers it (the reviewed head is
|
|
2032
|
+
not reachable from origin/main)."""
|
|
2033
|
+
def fake_run(command, **kwargs):
|
|
2034
|
+
if command[0] == "gh" and command[1] == "pr" and "view" in command:
|
|
2035
|
+
return json.dumps({
|
|
2036
|
+
"number": 4, "url": "u", "state": "OPEN",
|
|
2037
|
+
"mergedAt": None, "mergeCommit": None,
|
|
2038
|
+
})
|
|
2039
|
+
if command[0] == "git" and command[1] == "merge-base":
|
|
2040
|
+
raise subprocess.CalledProcessError(1, command, stderr="not ancestor")
|
|
2041
|
+
return ""
|
|
2042
|
+
|
|
2043
|
+
monkeypatch.setattr(seam, "run_command", fake_run)
|
|
2018
2044
|
with pytest.raises(RuntimeError, match="not merged"):
|
|
2019
2045
|
review_merge.confirm_merged(
|
|
2020
2046
|
tmp_path, {"number": 4, "url": "u", "base_ref": "main",
|
|
@@ -2023,6 +2049,68 @@ def test_confirm_merged_rejects_unmerged_pr(monkeypatch, tmp_path):
|
|
|
2023
2049
|
)
|
|
2024
2050
|
|
|
2025
2051
|
|
|
2052
|
+
def test_confirm_merged_accepts_a_reviewed_head_that_landed_on_base(
|
|
2053
|
+
monkeypatch, tmp_path, caplog):
|
|
2054
|
+
"""Issue #1798: GitHub creates the merge commit and the merge call
|
|
2055
|
+
still fails, so the PR reads OPEN with `mergedAt: null` and no merge
|
|
2056
|
+
commit. The reviewed head being reachable from origin/<base> is that
|
|
2057
|
+
landed merge; the commit that landed it is the answer.
|
|
2058
|
+
|
|
2059
|
+
`--topo-order --reverse` lists parents before children on the
|
|
2060
|
+
ancestry path, so the first commit is the one that landed the head —
|
|
2061
|
+
verified against a real repository in test_git_merge_smoke.py."""
|
|
2062
|
+
commands = []
|
|
2063
|
+
|
|
2064
|
+
def fake_run(command, **kwargs):
|
|
2065
|
+
commands.append(command)
|
|
2066
|
+
if command[0] == "gh" and command[1] == "pr" and "view" in command:
|
|
2067
|
+
return json.dumps({
|
|
2068
|
+
"number": 4, "url": "u", "state": "OPEN",
|
|
2069
|
+
"mergedAt": None, "mergeCommit": None,
|
|
2070
|
+
})
|
|
2071
|
+
if command[0] == "git" and command[1] == "rev-list":
|
|
2072
|
+
return "m1\nlater\n"
|
|
2073
|
+
return ""
|
|
2074
|
+
|
|
2075
|
+
monkeypatch.setattr(seam, "run_command", fake_run)
|
|
2076
|
+
with caplog.at_level("INFO"):
|
|
2077
|
+
result = review_merge.confirm_merged(
|
|
2078
|
+
tmp_path, {"number": 4, "head_oid": "h1"}, "main",
|
|
2079
|
+
repo_dir=tmp_path,
|
|
2080
|
+
)
|
|
2081
|
+
assert result == {"state": "MERGED", "merge_commit": "m1"}
|
|
2082
|
+
rev_list = [c for c in commands if c[1:2] == ["rev-list"]][0]
|
|
2083
|
+
assert rev_list[2:] == [
|
|
2084
|
+
"--ancestry-path", "--topo-order", "--reverse", "h1..origin/main",
|
|
2085
|
+
]
|
|
2086
|
+
assert "confirm_merged_landed_on_base" in caplog.text
|
|
2087
|
+
|
|
2088
|
+
|
|
2089
|
+
def test_confirm_merged_needs_a_landing_commit_for_an_open_pr(
|
|
2090
|
+
monkeypatch, tmp_path, caplog):
|
|
2091
|
+
"""Issue #1798: a head that is an ancestor of origin/<base> with no
|
|
2092
|
+
commit between them names no merge commit, so the confirm stays a
|
|
2093
|
+
rejection instead of inventing one."""
|
|
2094
|
+
def fake_run(command, **kwargs):
|
|
2095
|
+
if command[0] == "gh" and command[1] == "pr" and "view" in command:
|
|
2096
|
+
return json.dumps({
|
|
2097
|
+
"number": 4, "url": "u", "state": "OPEN",
|
|
2098
|
+
"mergedAt": None, "mergeCommit": None,
|
|
2099
|
+
})
|
|
2100
|
+
return ""
|
|
2101
|
+
|
|
2102
|
+
monkeypatch.setattr(seam, "run_command", fake_run)
|
|
2103
|
+
with caplog.at_level("ERROR"), pytest.raises(
|
|
2104
|
+
RuntimeError, match="not merged",
|
|
2105
|
+
):
|
|
2106
|
+
review_merge.confirm_merged(
|
|
2107
|
+
tmp_path, {"number": 4, "head_oid": "h1"}, "main",
|
|
2108
|
+
repo_dir=tmp_path,
|
|
2109
|
+
)
|
|
2110
|
+
assert "confirm_merged_not_merged" in caplog.text
|
|
2111
|
+
assert "confirm_merged_landed_on_base" not in caplog.text
|
|
2112
|
+
|
|
2113
|
+
|
|
2026
2114
|
def test_confirm_merged_rejects_merge_commit_missing_from_origin_main(
|
|
2027
2115
|
monkeypatch, tmp_path, caplog):
|
|
2028
2116
|
def fake_run(command, **kwargs):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|