github2gerrit 1.0.5__tar.gz → 1.0.7__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.
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.pre-commit-config.yaml +3 -3
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/PKG-INFO +2 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/action.yaml +1 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/pyproject.toml +3 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/core.py +123 -6
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/models.py +6 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/pr_content_filter.py +19 -21
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_change_id_deduplication.py +1 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_cli_outputs_file.py +1 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_cli_url_and_dryrun.py +2 -2
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_close_pr_policy.py +59 -7
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_prepare_commits.py +1 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_shallow_clone.py +339 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_duplicate_detection.py +10 -2
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gerrit_change_id_footer.py +1 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gitutils_helpers.py +1 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/uv.lock +96 -89
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.editorconfig +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.gitignore +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.gitlint +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.markdownlint.yaml +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.readthedocs.yml +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.yamllint +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/LICENSE +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/LICENSES/Apache-2.0.txt +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/README.md +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/REUSE.toml +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/docs/RELEASE-v0.2.0.md +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/docs/github2gerrit_token_permissions_classic.png +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/sitecustomize.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/__init__.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/cli.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/commit_normalization.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/config.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/constants.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/duplicate_detection.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/error_codes.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/external_api.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/gerrit_pr_closer.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/gerrit_query.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/gerrit_rest.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/gerrit_urls.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/github_api.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/gitutils.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/mapping_comment.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/netrc.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/orchestrator/__init__.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/reconcile_matcher.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/rich_display.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/rich_logging.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/similarity.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/ssh_agent_setup.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/ssh_common.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/ssh_config_parser.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/ssh_discovery.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/trailers.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/utils.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/conftest.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/fixtures/__init__.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/fixtures/make_repo.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/fixtures/ssh_config_samples.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_action_environment_mapping.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_action_outputs.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_action_pr_number_handling.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_action_step_validation.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_automation_only.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_cli.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_cli_helpers.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_cli_netrc_options.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_commit_normalization.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_composite_action_coverage.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_config_and_reviewers.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_config_helpers.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_config_and_errors.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_gerrit_backref_comment.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_gerrit_push_errors.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_gerrit_rest_results.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_integration_fixture_repo.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_ssh_setup.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_ssrf_protection.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_email_case_normalization.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_error_codes.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_external_api_framework.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_force_flag_cli.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gerrit_change_status_checks.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gerrit_pr_closer.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gerrit_rest_client.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gerrit_urls.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gerrit_urls_more.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ghe_and_gitreview_args.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_github_api_error_handling.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_github_api_helpers.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_github_api_retry_and_helpers.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_mapping_comment_additional.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_mapping_comment_digest_and_backref.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_metadata_and_reconciliation.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_metadata_trailer_separation_bug.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_misc_small_coverage.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_netrc.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_orphan_rest_side_effects.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_pr_content_filter.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_pr_content_filter_integration.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_pr_update_detection.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_reconciliation_extracted_module.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_reconciliation_plan_and_orphans.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_reconciliation_scenarios.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ssh_agent.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ssh_agent_ownership.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ssh_artifact_prevention.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ssh_common.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ssh_discovery.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ssh_discovery_dry_run.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_trailers_additional.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_url_parser.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_utils.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/unit/test_config_integration.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/unit/test_ssh_config_parser.py +0 -0
|
@@ -59,7 +59,7 @@ repos:
|
|
|
59
59
|
types: [yaml]
|
|
60
60
|
|
|
61
61
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
62
|
-
rev:
|
|
62
|
+
rev: 0839f92796ae388643a08a21640a029b322be5c2 # frozen: v0.15.2
|
|
63
63
|
hooks:
|
|
64
64
|
- id: ruff
|
|
65
65
|
files: ^(src|scripts|tests)/.+\.py$
|
|
@@ -110,7 +110,7 @@ repos:
|
|
|
110
110
|
# Replaces: https://github.com/rhysd/actionlint
|
|
111
111
|
# Permits actionlint to run both locally and with precommit.ci/GitHub
|
|
112
112
|
- repo: https://github.com/Mateusz-Grzelinski/actionlint-py
|
|
113
|
-
rev:
|
|
113
|
+
rev: 694e2c0dfb4253d51f3c6c54b8f9fec0a16764dc # frozen: v1.7.11.24
|
|
114
114
|
hooks:
|
|
115
115
|
- id: actionlint
|
|
116
116
|
|
|
@@ -121,7 +121,7 @@ repos:
|
|
|
121
121
|
- id: codespell
|
|
122
122
|
|
|
123
123
|
- repo: https://github.com/python-jsonschema/check-jsonschema
|
|
124
|
-
rev:
|
|
124
|
+
rev: ec368acd16deee9c560c105ab6d27db4ee19a5ec # frozen: 0.36.2
|
|
125
125
|
hooks:
|
|
126
126
|
- id: check-github-actions
|
|
127
127
|
- id: check-github-workflows
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github2gerrit
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
4
4
|
Summary: Submit a GitHub pull request to a Gerrit repository.
|
|
5
5
|
Project-URL: Homepage, https://github.com/lfreleng-actions/github2gerrit-action
|
|
6
6
|
Project-URL: Repository, https://github.com/lfreleng-actions/github2gerrit-action
|
|
@@ -22,6 +22,7 @@ Classifier: Topic :: Software Development :: Build Tools
|
|
|
22
22
|
Classifier: Topic :: Software Development :: Version Control
|
|
23
23
|
Classifier: Typing :: Typed
|
|
24
24
|
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: cryptography>=46.0.5
|
|
25
26
|
Requires-Dist: git-review>=2.5.0
|
|
26
27
|
Requires-Dist: pygerrit2>=2.0.15
|
|
27
28
|
Requires-Dist: pygithub>=2.8.1
|
|
@@ -164,7 +164,7 @@ runs:
|
|
|
164
164
|
|
|
165
165
|
- name: "Setup uv"
|
|
166
166
|
# yamllint disable-line rule:line-length
|
|
167
|
-
uses: astral-sh/setup-uv@
|
|
167
|
+
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
|
168
168
|
with:
|
|
169
169
|
enable-cache: false
|
|
170
170
|
|
|
@@ -2739,6 +2739,113 @@ class Orchestrator:
|
|
|
2739
2739
|
run_cmd(cmd, cwd=self.workspace)
|
|
2740
2740
|
log.info("Checkout succeeded after full unshallow")
|
|
2741
2741
|
|
|
2742
|
+
def _merge_squash_with_unshallow_fallback(self, head_sha: str) -> None:
|
|
2743
|
+
"""Perform git merge --squash with graduated deepening fallback.
|
|
2744
|
+
|
|
2745
|
+
If the initial merge fails because the shallow clone lacks a common
|
|
2746
|
+
ancestor between the base and head (causing "refusing to merge
|
|
2747
|
+
unrelated histories"), this method will:
|
|
2748
|
+
1. First try to deepen the repository (fetch 100 more commits)
|
|
2749
|
+
2. If that fails, fully unshallow the repository
|
|
2750
|
+
3. Retry the merge after each attempt
|
|
2751
|
+
|
|
2752
|
+
This mirrors the graduated approach used by
|
|
2753
|
+
``_checkout_with_unshallow_fallback`` but for merge operations,
|
|
2754
|
+
which are equally susceptible to shallow clone limitations.
|
|
2755
|
+
|
|
2756
|
+
Args:
|
|
2757
|
+
head_sha: The SHA to merge (PR head commit)
|
|
2758
|
+
|
|
2759
|
+
Raises:
|
|
2760
|
+
CommandError: If merge fails even after unshallowing, or if
|
|
2761
|
+
the failure is not related to shallow clone history
|
|
2762
|
+
"""
|
|
2763
|
+
merge_cmd = ["git", "merge", "--squash", head_sha]
|
|
2764
|
+
|
|
2765
|
+
merge_exc: CommandError | None = None
|
|
2766
|
+
try:
|
|
2767
|
+
run_cmd(merge_cmd, cwd=self.workspace)
|
|
2768
|
+
except CommandError as exc:
|
|
2769
|
+
merge_exc = exc
|
|
2770
|
+
|
|
2771
|
+
if merge_exc is None:
|
|
2772
|
+
return # Success on first attempt
|
|
2773
|
+
|
|
2774
|
+
# Check if the failure is due to unrelated histories in a shallow clone
|
|
2775
|
+
error_str = str(merge_exc).lower()
|
|
2776
|
+
stderr_str = (merge_exc.stderr or "").lower()
|
|
2777
|
+
combined = f"{error_str} {stderr_str}"
|
|
2778
|
+
|
|
2779
|
+
is_unrelated_histories = (
|
|
2780
|
+
"refusing to merge unrelated histories" in combined
|
|
2781
|
+
or "unrelated histories" in combined
|
|
2782
|
+
or "no common ancestor" in combined
|
|
2783
|
+
)
|
|
2784
|
+
|
|
2785
|
+
if not is_unrelated_histories or not self._is_shallow_clone():
|
|
2786
|
+
# Not a shallow clone issue — let the caller handle the error
|
|
2787
|
+
raise merge_exc
|
|
2788
|
+
|
|
2789
|
+
log.warning(
|
|
2790
|
+
"Merge --squash failed due to unrelated histories in shallow "
|
|
2791
|
+
"clone. Attempting graduated deepening to recover..."
|
|
2792
|
+
)
|
|
2793
|
+
|
|
2794
|
+
# Step 1: Try deepening first (cheaper than full unshallow)
|
|
2795
|
+
if self._deepen_repository(depth=100):
|
|
2796
|
+
log.debug("Retrying merge --squash after deepening...")
|
|
2797
|
+
# Reset the failed merge state before retrying
|
|
2798
|
+
run_cmd(
|
|
2799
|
+
["git", "merge", "--abort"],
|
|
2800
|
+
cwd=self.workspace,
|
|
2801
|
+
check=False,
|
|
2802
|
+
)
|
|
2803
|
+
try:
|
|
2804
|
+
run_cmd(merge_cmd, cwd=self.workspace)
|
|
2805
|
+
except CommandError as deepen_exc:
|
|
2806
|
+
log.debug(
|
|
2807
|
+
"Merge --squash still failed after deepening: %s",
|
|
2808
|
+
deepen_exc,
|
|
2809
|
+
)
|
|
2810
|
+
# Re-check whether this is still a shallow-history problem;
|
|
2811
|
+
# if the error has changed (e.g. real merge conflict), an
|
|
2812
|
+
# expensive full unshallow cannot help — propagate immediately.
|
|
2813
|
+
deepen_combined = (
|
|
2814
|
+
f"{deepen_exc} {deepen_exc.stderr or ''}".lower()
|
|
2815
|
+
)
|
|
2816
|
+
deepen_is_unrelated = (
|
|
2817
|
+
"refusing to merge unrelated histories" in deepen_combined
|
|
2818
|
+
or "unrelated histories" in deepen_combined
|
|
2819
|
+
or "no common ancestor" in deepen_combined
|
|
2820
|
+
)
|
|
2821
|
+
if not deepen_is_unrelated:
|
|
2822
|
+
raise
|
|
2823
|
+
else:
|
|
2824
|
+
log.info("Merge --squash succeeded after deepening repository")
|
|
2825
|
+
return
|
|
2826
|
+
|
|
2827
|
+
# Step 2: Full unshallow as last resort
|
|
2828
|
+
log.info(
|
|
2829
|
+
"Deepening insufficient, performing full unshallow for merge..."
|
|
2830
|
+
)
|
|
2831
|
+
# Reset the failed merge state before retrying
|
|
2832
|
+
run_cmd(
|
|
2833
|
+
["git", "merge", "--abort"],
|
|
2834
|
+
cwd=self.workspace,
|
|
2835
|
+
check=False,
|
|
2836
|
+
)
|
|
2837
|
+
if not self._unshallow_repository():
|
|
2838
|
+
log.error(
|
|
2839
|
+
"Failed to unshallow repository. Cannot merge SHA: %s",
|
|
2840
|
+
head_sha,
|
|
2841
|
+
)
|
|
2842
|
+
raise merge_exc
|
|
2843
|
+
|
|
2844
|
+
# Retry the merge after full unshallow
|
|
2845
|
+
log.debug("Retrying merge --squash after full unshallow...")
|
|
2846
|
+
run_cmd(merge_cmd, cwd=self.workspace)
|
|
2847
|
+
log.info("Merge --squash succeeded after full unshallow")
|
|
2848
|
+
|
|
2742
2849
|
def _cleanup_ssh(self) -> None:
|
|
2743
2850
|
"""Clean up temporary SSH files created by this tool.
|
|
2744
2851
|
|
|
@@ -3103,6 +3210,15 @@ class Orchestrator:
|
|
|
3103
3210
|
|
|
3104
3211
|
except Exception as debug_exc:
|
|
3105
3212
|
log.warning("Failed to analyze merge situation: %s", debug_exc)
|
|
3213
|
+
# Proactively deepen if merge-base fails in a shallow clone,
|
|
3214
|
+
# as this strongly indicates the shallow history is insufficient
|
|
3215
|
+
# for the upcoming merge --squash operation.
|
|
3216
|
+
if self._is_shallow_clone():
|
|
3217
|
+
log.info(
|
|
3218
|
+
"merge-base failed in shallow clone — proactively "
|
|
3219
|
+
"deepening repository to improve merge success chances"
|
|
3220
|
+
)
|
|
3221
|
+
self._deepen_repository(depth=100)
|
|
3106
3222
|
|
|
3107
3223
|
self._checkout_with_unshallow_fallback(
|
|
3108
3224
|
branch_name=tmp_branch,
|
|
@@ -3134,7 +3250,7 @@ class Orchestrator:
|
|
|
3134
3250
|
|
|
3135
3251
|
log.debug("About to run: git merge --squash %s", head_sha)
|
|
3136
3252
|
try:
|
|
3137
|
-
|
|
3253
|
+
self._merge_squash_with_unshallow_fallback(head_sha)
|
|
3138
3254
|
except CommandError as merge_exc:
|
|
3139
3255
|
# Enhanced error handling for git merge failures
|
|
3140
3256
|
error_details = self._analyze_merge_failure(
|
|
@@ -3308,7 +3424,7 @@ class Orchestrator:
|
|
|
3308
3424
|
)
|
|
3309
3425
|
if (
|
|
3310
3426
|
not sync_all_prs
|
|
3311
|
-
and gh.event_name
|
|
3427
|
+
and gh.event_name in ("pull_request", "pull_request_target")
|
|
3312
3428
|
and gh.event_action in ("reopened", "synchronize")
|
|
3313
3429
|
):
|
|
3314
3430
|
try:
|
|
@@ -5402,8 +5518,9 @@ class Orchestrator:
|
|
|
5402
5518
|
self,
|
|
5403
5519
|
gh: GitHubContext,
|
|
5404
5520
|
) -> None:
|
|
5405
|
-
"""Close the PR if policy requires
|
|
5521
|
+
"""Close the PR if policy requires.
|
|
5406
5522
|
|
|
5523
|
+
Supports both ``pull_request`` and ``pull_request_target`` triggers.
|
|
5407
5524
|
When PRESERVE_GITHUB_PRS is true, skip closing PRs (useful for testing).
|
|
5408
5525
|
"""
|
|
5409
5526
|
# Respect PRESERVE_GITHUB_PRS to avoid closing PRs during tests
|
|
@@ -5414,9 +5531,9 @@ class Orchestrator:
|
|
|
5414
5531
|
gh.pr_number,
|
|
5415
5532
|
)
|
|
5416
5533
|
return
|
|
5417
|
-
#
|
|
5418
|
-
if gh.event_name
|
|
5419
|
-
log.debug("Event is not
|
|
5534
|
+
# Close PRs on pull_request or pull_request_target events.
|
|
5535
|
+
if gh.event_name not in ("pull_request", "pull_request_target"):
|
|
5536
|
+
log.debug("Event is not a pull_request event; not closing PR")
|
|
5420
5537
|
return
|
|
5421
5538
|
log.info("Closing PR #%s", gh.pr_number)
|
|
5422
5539
|
try:
|
|
@@ -117,10 +117,15 @@ class GitHubContext:
|
|
|
117
117
|
def get_operation_mode(self) -> PROperationMode:
|
|
118
118
|
"""Determine the operation mode based on event type and action.
|
|
119
119
|
|
|
120
|
+
Supports both ``pull_request`` and ``pull_request_target`` triggers.
|
|
121
|
+
Using ``pull_request`` is preferred for security (avoids granting
|
|
122
|
+
secrets to untrusted fork code), while ``pull_request_target`` is
|
|
123
|
+
accepted for backward compatibility.
|
|
124
|
+
|
|
120
125
|
Returns:
|
|
121
126
|
PROperationMode enum indicating the type of operation
|
|
122
127
|
"""
|
|
123
|
-
if self.event_name
|
|
128
|
+
if self.event_name not in ("pull_request", "pull_request_target"):
|
|
124
129
|
return PROperationMode.UNKNOWN
|
|
125
130
|
|
|
126
131
|
action = self.event_action.lower() if self.event_action else ""
|
|
@@ -44,27 +44,6 @@ _MULTIPLE_NEWLINES_PATTERN = re.compile(r"\n{3,}")
|
|
|
44
44
|
_EMOJI_PATTERN = re.compile(r":[a-z_]+:") # GitHub emoji codes like :sparkles:
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
@dataclass
|
|
48
|
-
class FilterConfig:
|
|
49
|
-
"""Configuration for PR content filtering."""
|
|
50
|
-
|
|
51
|
-
# Global options
|
|
52
|
-
enabled: bool = True
|
|
53
|
-
remove_emoji_codes: bool = True
|
|
54
|
-
deduplicate_title_in_body: bool = True
|
|
55
|
-
|
|
56
|
-
# Author-specific filtering
|
|
57
|
-
author_rules: dict[str, str] = field(default_factory=dict)
|
|
58
|
-
|
|
59
|
-
# Rule-specific configurations
|
|
60
|
-
dependabot_config: DependabotConfig = field(
|
|
61
|
-
default_factory=lambda: DependabotConfig()
|
|
62
|
-
)
|
|
63
|
-
precommit_config: PrecommitConfig = field(
|
|
64
|
-
default_factory=lambda: PrecommitConfig()
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
|
|
68
47
|
@dataclass
|
|
69
48
|
class DependabotConfig:
|
|
70
49
|
"""Configuration for Dependabot PR filtering."""
|
|
@@ -85,6 +64,25 @@ class PrecommitConfig:
|
|
|
85
64
|
# Future: add pre-commit.ci specific options
|
|
86
65
|
|
|
87
66
|
|
|
67
|
+
@dataclass
|
|
68
|
+
class FilterConfig:
|
|
69
|
+
"""Configuration for PR content filtering."""
|
|
70
|
+
|
|
71
|
+
# Global options
|
|
72
|
+
enabled: bool = True
|
|
73
|
+
remove_emoji_codes: bool = True
|
|
74
|
+
deduplicate_title_in_body: bool = True
|
|
75
|
+
|
|
76
|
+
# Author-specific filtering
|
|
77
|
+
author_rules: dict[str, str] = field(default_factory=dict)
|
|
78
|
+
|
|
79
|
+
# Rule-specific configurations
|
|
80
|
+
dependabot_config: DependabotConfig = field(
|
|
81
|
+
default_factory=DependabotConfig
|
|
82
|
+
)
|
|
83
|
+
precommit_config: PrecommitConfig = field(default_factory=PrecommitConfig)
|
|
84
|
+
|
|
85
|
+
|
|
88
86
|
class FilterRule(ABC):
|
|
89
87
|
"""Abstract base class for PR content filtering rules."""
|
|
90
88
|
|
|
@@ -275,7 +275,7 @@ Change-Id: {reused_change_id}
|
|
|
275
275
|
monkeypatch.setattr("github2gerrit.core.run_cmd", mock_run_cmd)
|
|
276
276
|
|
|
277
277
|
# Mock GitHub API for Change-ID reuse
|
|
278
|
-
monkeypatch.setattr("github2gerrit.core.build_client",
|
|
278
|
+
monkeypatch.setattr("github2gerrit.core.build_client", object)
|
|
279
279
|
monkeypatch.setattr(
|
|
280
280
|
"github2gerrit.core.get_repo_from_env", lambda _: object()
|
|
281
281
|
)
|
|
@@ -232,7 +232,7 @@ def test_multi_pr_url_mode_writes_aggregated_outputs(
|
|
|
232
232
|
def __init__(self, number: int) -> None:
|
|
233
233
|
self.number = number
|
|
234
234
|
|
|
235
|
-
monkeypatch.setattr(cli_mod, "build_client",
|
|
235
|
+
monkeypatch.setattr(cli_mod, "build_client", object)
|
|
236
236
|
monkeypatch.setattr(cli_mod, "get_repo_from_env", lambda _client: object())
|
|
237
237
|
monkeypatch.setattr(
|
|
238
238
|
cli_mod,
|
|
@@ -168,7 +168,7 @@ def test_repo_url_dry_run_invokes_for_each_open_pr(
|
|
|
168
168
|
monkeypatch.setattr(cli_mod, "Orchestrator", _DummyOrchestrator)
|
|
169
169
|
|
|
170
170
|
# Patch PyGithub wrapper functions used by CLI bulk path
|
|
171
|
-
monkeypatch.setattr(cli_mod, "build_client",
|
|
171
|
+
monkeypatch.setattr(cli_mod, "build_client", object)
|
|
172
172
|
monkeypatch.setattr(cli_mod, "get_repo_from_env", lambda _client: object())
|
|
173
173
|
monkeypatch.setattr(
|
|
174
174
|
cli_mod, "iter_open_pulls", lambda _repo: iter(dummy_prs)
|
|
@@ -212,7 +212,7 @@ def test_url_mode_sets_environment_for_config_resolution(
|
|
|
212
212
|
monkeypatch.setattr(cli_mod, "Orchestrator", _DummyOrchestrator)
|
|
213
213
|
|
|
214
214
|
# Minimal patches for bulk flow
|
|
215
|
-
monkeypatch.setattr(cli_mod, "build_client",
|
|
215
|
+
monkeypatch.setattr(cli_mod, "build_client", object)
|
|
216
216
|
monkeypatch.setattr(cli_mod, "get_repo_from_env", lambda _client: object())
|
|
217
217
|
monkeypatch.setattr(
|
|
218
218
|
cli_mod,
|
|
@@ -58,16 +58,16 @@ def test_close_pr_skipped_when_preserve_github_prs_true(
|
|
|
58
58
|
orch._close_pull_request_if_required(gh)
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def
|
|
61
|
+
def test_close_pr_not_invoked_for_non_pr_event(
|
|
62
62
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
63
63
|
) -> None:
|
|
64
64
|
# Ensure preservation is disabled so only event controls behavior
|
|
65
65
|
monkeypatch.delenv("PRESERVE_GITHUB_PRS", raising=False)
|
|
66
66
|
|
|
67
|
-
# Non-
|
|
67
|
+
# Non-PR events (e.g. workflow_dispatch, push) should not attempt to close
|
|
68
68
|
def _fail(*args: Any, **kwargs: Any) -> Any:
|
|
69
69
|
raise AssertionError(
|
|
70
|
-
"GitHub API should not be called for non-
|
|
70
|
+
"GitHub API should not be called for non-PR events"
|
|
71
71
|
)
|
|
72
72
|
|
|
73
73
|
monkeypatch.setattr("github2gerrit.core.build_client", _fail)
|
|
@@ -76,15 +76,20 @@ def test_close_pr_not_invoked_for_non_target_event(
|
|
|
76
76
|
monkeypatch.setattr("github2gerrit.core.close_pr", _fail)
|
|
77
77
|
|
|
78
78
|
orch = Orchestrator(workspace=tmp_path)
|
|
79
|
-
gh = _gh_ctx(event_name="
|
|
79
|
+
gh = _gh_ctx(event_name="workflow_dispatch", pr_number=42)
|
|
80
80
|
|
|
81
|
-
# Act: should no-op
|
|
81
|
+
# Act: should no-op for non-PR events
|
|
82
82
|
orch._close_pull_request_if_required(gh)
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
def
|
|
85
|
+
def test_close_pr_invoked_for_pull_request_event(
|
|
86
86
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
87
87
|
) -> None:
|
|
88
|
+
"""Verify that pull_request events (not just pull_request_target) close PRs.
|
|
89
|
+
|
|
90
|
+
The code now supports both ``pull_request`` and ``pull_request_target``
|
|
91
|
+
triggers. Using ``pull_request`` is preferred for security.
|
|
92
|
+
"""
|
|
88
93
|
# Ensure preservation is disabled so closing is allowed
|
|
89
94
|
monkeypatch.delenv("PRESERVE_GITHUB_PRS", raising=False)
|
|
90
95
|
|
|
@@ -102,9 +107,56 @@ def test_close_pr_invoked_for_pull_request_target_event(
|
|
|
102
107
|
self.closed_state: str | None = None
|
|
103
108
|
|
|
104
109
|
# Patch the GitHub helper functions used by the close path
|
|
110
|
+
monkeypatch.setattr("github2gerrit.core.build_client", DummyClient)
|
|
105
111
|
monkeypatch.setattr(
|
|
106
|
-
"github2gerrit.core.
|
|
112
|
+
"github2gerrit.core.get_repo_from_env", lambda _c: DummyRepo()
|
|
107
113
|
)
|
|
114
|
+
|
|
115
|
+
def _get_pull(_repo: DummyRepo, number: int) -> DummyPR:
|
|
116
|
+
calls["get_pull_number"] = number
|
|
117
|
+
return DummyPR(number)
|
|
118
|
+
|
|
119
|
+
def _close_pr(pr: DummyPR, *, comment: str | None = None) -> None:
|
|
120
|
+
calls["closed_pr_number"] = pr.number
|
|
121
|
+
calls["comment"] = comment
|
|
122
|
+
pr.closed_state = "closed"
|
|
123
|
+
|
|
124
|
+
monkeypatch.setattr("github2gerrit.core.get_pull", _get_pull)
|
|
125
|
+
monkeypatch.setattr("github2gerrit.core.close_pr", _close_pr)
|
|
126
|
+
|
|
127
|
+
orch = Orchestrator(workspace=tmp_path)
|
|
128
|
+
gh = _gh_ctx(event_name="pull_request", pr_number=42)
|
|
129
|
+
|
|
130
|
+
# Act: pull_request events should now close the PR
|
|
131
|
+
orch._close_pull_request_if_required(gh)
|
|
132
|
+
|
|
133
|
+
# Assert
|
|
134
|
+
assert calls.get("get_pull_number") == 42
|
|
135
|
+
assert calls.get("closed_pr_number") == 42
|
|
136
|
+
assert calls.get("comment") == "Auto-closing pull request"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_close_pr_invoked_for_pull_request_target_event(
|
|
140
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
141
|
+
) -> None:
|
|
142
|
+
# Ensure preservation is disabled so closing is allowed
|
|
143
|
+
monkeypatch.delenv("PRESERVE_GITHUB_PRS", raising=False)
|
|
144
|
+
|
|
145
|
+
calls: dict[str, Any] = {}
|
|
146
|
+
|
|
147
|
+
class DummyClient:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
class DummyRepo:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
class DummyPR:
|
|
154
|
+
def __init__(self, number: int) -> None:
|
|
155
|
+
self.number = number
|
|
156
|
+
self.closed_state: str | None = None
|
|
157
|
+
|
|
158
|
+
# Patch the GitHub helper functions used by the close path
|
|
159
|
+
monkeypatch.setattr("github2gerrit.core.build_client", DummyClient)
|
|
108
160
|
monkeypatch.setattr(
|
|
109
161
|
"github2gerrit.core.get_repo_from_env", lambda _c: DummyRepo()
|
|
110
162
|
)
|
|
@@ -237,7 +237,7 @@ def test_prepare_squashed_commit_reuses_change_id_from_comments(
|
|
|
237
237
|
monkeypatch.setattr("github2gerrit.core.run_cmd", fake_run_cmd)
|
|
238
238
|
|
|
239
239
|
# GitHub API helpers used to fetch PR comments
|
|
240
|
-
monkeypatch.setattr("github2gerrit.core.build_client",
|
|
240
|
+
monkeypatch.setattr("github2gerrit.core.build_client", object)
|
|
241
241
|
monkeypatch.setattr(
|
|
242
242
|
"github2gerrit.core.get_repo_from_env", lambda _c: object()
|
|
243
243
|
)
|