github2gerrit 1.0.5__tar.gz → 1.0.6__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.6}/.pre-commit-config.yaml +3 -3
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/PKG-INFO +2 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/action.yaml +1 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/pyproject.toml +3 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/core.py +117 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/pr_content_filter.py +19 -21
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_change_id_deduplication.py +1 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_cli_outputs_file.py +1 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_cli_url_and_dryrun.py +2 -2
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_core_close_pr_policy.py +1 -3
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_core_prepare_commits.py +1 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_core_shallow_clone.py +339 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_gerrit_change_id_footer.py +1 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_gitutils_helpers.py +1 -1
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/uv.lock +93 -86
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/.editorconfig +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/.gitignore +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/.gitlint +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/.markdownlint.yaml +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/.readthedocs.yml +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/.yamllint +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/LICENSE +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/LICENSES/Apache-2.0.txt +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/README.md +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/REUSE.toml +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/docs/RELEASE-v0.2.0.md +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/docs/github2gerrit_token_permissions_classic.png +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/sitecustomize.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/__init__.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/cli.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/commit_normalization.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/config.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/constants.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/duplicate_detection.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/error_codes.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/external_api.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/gerrit_pr_closer.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/gerrit_query.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/gerrit_rest.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/gerrit_urls.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/github_api.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/gitutils.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/mapping_comment.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/models.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/netrc.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/orchestrator/__init__.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/reconcile_matcher.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/rich_display.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/rich_logging.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/similarity.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/ssh_agent_setup.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/ssh_common.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/ssh_config_parser.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/ssh_discovery.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/trailers.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/src/github2gerrit/utils.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/conftest.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/fixtures/__init__.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/fixtures/make_repo.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/fixtures/ssh_config_samples.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_action_environment_mapping.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_action_outputs.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_action_pr_number_handling.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_action_step_validation.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_automation_only.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_cli.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_cli_helpers.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_cli_netrc_options.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_commit_normalization.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_composite_action_coverage.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_config_and_reviewers.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_config_helpers.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_core_config_and_errors.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_core_gerrit_backref_comment.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_core_gerrit_push_errors.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_core_gerrit_rest_results.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_core_integration_fixture_repo.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_core_ssh_setup.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_core_ssrf_protection.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_duplicate_detection.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_email_case_normalization.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_error_codes.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_external_api_framework.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_force_flag_cli.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_gerrit_change_status_checks.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_gerrit_pr_closer.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_gerrit_rest_client.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_gerrit_urls.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_gerrit_urls_more.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_ghe_and_gitreview_args.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_github_api_error_handling.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_github_api_helpers.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_github_api_retry_and_helpers.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_mapping_comment_additional.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_mapping_comment_digest_and_backref.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_metadata_and_reconciliation.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_metadata_trailer_separation_bug.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_misc_small_coverage.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_netrc.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_orphan_rest_side_effects.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_pr_content_filter.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_pr_content_filter_integration.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_pr_update_detection.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_reconciliation_extracted_module.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_reconciliation_plan_and_orphans.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_reconciliation_scenarios.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_ssh_agent.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_ssh_agent_ownership.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_ssh_artifact_prevention.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_ssh_common.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_ssh_discovery.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_ssh_discovery_dry_run.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_trailers_additional.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_url_parser.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/test_utils.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/tests/unit/test_config_integration.py +0 -0
- {github2gerrit-1.0.5 → github2gerrit-1.0.6}/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: fa93bc3224c614a0e9786d3e2d3d48edcca246eb # frozen: v0.15.1
|
|
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.6
|
|
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(
|
|
@@ -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,
|
|
@@ -102,9 +102,7 @@ def test_close_pr_invoked_for_pull_request_target_event(
|
|
|
102
102
|
self.closed_state: str | None = None
|
|
103
103
|
|
|
104
104
|
# Patch the GitHub helper functions used by the close path
|
|
105
|
-
monkeypatch.setattr(
|
|
106
|
-
"github2gerrit.core.build_client", lambda: DummyClient()
|
|
107
|
-
)
|
|
105
|
+
monkeypatch.setattr("github2gerrit.core.build_client", DummyClient)
|
|
108
106
|
monkeypatch.setattr(
|
|
109
107
|
"github2gerrit.core.get_repo_from_env", lambda _c: DummyRepo()
|
|
110
108
|
)
|
|
@@ -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
|
)
|
|
@@ -485,3 +485,342 @@ class TestEnsureWorkspacePrepared:
|
|
|
485
485
|
# Should only have fetched once
|
|
486
486
|
fetch_calls = [c for c in call_log if c[:2] == ["git", "fetch"]]
|
|
487
487
|
assert len(fetch_calls) == 1
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class TestMergeSquashWithUnshallowFallback:
|
|
491
|
+
"""Tests for _merge_squash_with_unshallow_fallback method."""
|
|
492
|
+
|
|
493
|
+
def test_merge_success_first_attempt(self, tmp_path: Path) -> None:
|
|
494
|
+
"""Merge succeeds on first attempt without any deepening."""
|
|
495
|
+
git_dir = tmp_path / ".git"
|
|
496
|
+
git_dir.mkdir()
|
|
497
|
+
|
|
498
|
+
call_log: list[list[str]] = []
|
|
499
|
+
|
|
500
|
+
def fake_run_cmd(cmd: list[str], **kwargs: Any) -> CommandResult:
|
|
501
|
+
call_log.append(cmd)
|
|
502
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
503
|
+
|
|
504
|
+
with patch("github2gerrit.core.run_cmd", side_effect=fake_run_cmd):
|
|
505
|
+
orch = Orchestrator(workspace=tmp_path)
|
|
506
|
+
orch._merge_squash_with_unshallow_fallback(head_sha="abc123")
|
|
507
|
+
|
|
508
|
+
# Should have called merge --squash exactly once
|
|
509
|
+
merge_calls = [c for c in call_log if "merge" in c and "--squash" in c]
|
|
510
|
+
assert len(merge_calls) == 1
|
|
511
|
+
assert merge_calls[0] == ["git", "merge", "--squash", "abc123"]
|
|
512
|
+
|
|
513
|
+
# No deepen or unshallow calls
|
|
514
|
+
deepen_calls = [c for c in call_log if "--deepen=" in str(c)]
|
|
515
|
+
assert len(deepen_calls) == 0
|
|
516
|
+
unshallow_calls = [c for c in call_log if "--unshallow" in c]
|
|
517
|
+
assert len(unshallow_calls) == 0
|
|
518
|
+
|
|
519
|
+
def test_merge_fails_unrelated_histories_deepen_succeeds(
|
|
520
|
+
self, tmp_path: Path
|
|
521
|
+
) -> None:
|
|
522
|
+
"""Merge fails with unrelated histories, deepen fixes it."""
|
|
523
|
+
git_dir = tmp_path / ".git"
|
|
524
|
+
git_dir.mkdir()
|
|
525
|
+
(git_dir / "shallow").touch()
|
|
526
|
+
|
|
527
|
+
merge_attempts = [0]
|
|
528
|
+
|
|
529
|
+
def fake_run_cmd(cmd: list[str], **kwargs: Any) -> CommandResult:
|
|
530
|
+
if cmd[:2] == ["git", "merge"] and "--squash" in cmd:
|
|
531
|
+
merge_attempts[0] += 1
|
|
532
|
+
if merge_attempts[0] == 1:
|
|
533
|
+
raise CommandError(
|
|
534
|
+
"fatal: refusing to merge unrelated histories",
|
|
535
|
+
returncode=128,
|
|
536
|
+
stderr="fatal: refusing to merge unrelated histories",
|
|
537
|
+
)
|
|
538
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
539
|
+
if cmd[:2] == ["git", "merge"] and "--abort" in cmd:
|
|
540
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
541
|
+
if "--deepen=" in str(cmd):
|
|
542
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
543
|
+
if "is-shallow-repository" in cmd:
|
|
544
|
+
return CommandResult(returncode=0, stdout="true\n", stderr="")
|
|
545
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
546
|
+
|
|
547
|
+
with patch("github2gerrit.core.run_cmd", side_effect=fake_run_cmd):
|
|
548
|
+
orch = Orchestrator(workspace=tmp_path)
|
|
549
|
+
orch._merge_squash_with_unshallow_fallback(head_sha="abc123")
|
|
550
|
+
|
|
551
|
+
# Should have attempted merge twice (before and after deepen)
|
|
552
|
+
assert merge_attempts[0] == 2
|
|
553
|
+
|
|
554
|
+
def test_merge_fails_unrelated_histories_deepen_insufficient_unshallow_succeeds(
|
|
555
|
+
self, tmp_path: Path
|
|
556
|
+
) -> None:
|
|
557
|
+
"""Merge fails, deepen insufficient, full unshallow fixes it."""
|
|
558
|
+
git_dir = tmp_path / ".git"
|
|
559
|
+
git_dir.mkdir()
|
|
560
|
+
(git_dir / "shallow").touch()
|
|
561
|
+
|
|
562
|
+
merge_attempts = [0]
|
|
563
|
+
|
|
564
|
+
def fake_run_cmd(cmd: list[str], **kwargs: Any) -> CommandResult:
|
|
565
|
+
if cmd[:2] == ["git", "merge"] and "--squash" in cmd:
|
|
566
|
+
merge_attempts[0] += 1
|
|
567
|
+
if merge_attempts[0] <= 2:
|
|
568
|
+
raise CommandError(
|
|
569
|
+
"fatal: refusing to merge unrelated histories",
|
|
570
|
+
returncode=128,
|
|
571
|
+
stderr="fatal: refusing to merge unrelated histories",
|
|
572
|
+
)
|
|
573
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
574
|
+
if cmd[:2] == ["git", "merge"] and "--abort" in cmd:
|
|
575
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
576
|
+
if "--deepen=" in str(cmd):
|
|
577
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
578
|
+
if cmd[:3] == ["git", "fetch", "--unshallow"]:
|
|
579
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
580
|
+
if "is-shallow-repository" in cmd:
|
|
581
|
+
return CommandResult(returncode=0, stdout="true\n", stderr="")
|
|
582
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
583
|
+
|
|
584
|
+
with patch("github2gerrit.core.run_cmd", side_effect=fake_run_cmd):
|
|
585
|
+
orch = Orchestrator(workspace=tmp_path)
|
|
586
|
+
orch._merge_squash_with_unshallow_fallback(head_sha="abc123")
|
|
587
|
+
|
|
588
|
+
# Should have attempted merge 3 times:
|
|
589
|
+
# 1. Initial (fail), 2. After deepen (fail), 3. After unshallow (success)
|
|
590
|
+
assert merge_attempts[0] == 3
|
|
591
|
+
|
|
592
|
+
def test_merge_fails_non_shallow_error_raises_immediately(
|
|
593
|
+
self, tmp_path: Path
|
|
594
|
+
) -> None:
|
|
595
|
+
"""Merge fails with non-shallow error, raises immediately."""
|
|
596
|
+
git_dir = tmp_path / ".git"
|
|
597
|
+
git_dir.mkdir()
|
|
598
|
+
|
|
599
|
+
def fake_run_cmd(cmd: list[str], **kwargs: Any) -> CommandResult:
|
|
600
|
+
if cmd[:2] == ["git", "merge"] and "--squash" in cmd:
|
|
601
|
+
raise CommandError(
|
|
602
|
+
"Merge conflict in file.txt",
|
|
603
|
+
returncode=1,
|
|
604
|
+
stderr="CONFLICT (content): Merge conflict in file.txt",
|
|
605
|
+
)
|
|
606
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
607
|
+
|
|
608
|
+
with patch("github2gerrit.core.run_cmd", side_effect=fake_run_cmd):
|
|
609
|
+
orch = Orchestrator(workspace=tmp_path)
|
|
610
|
+
with pytest.raises(CommandError) as exc_info:
|
|
611
|
+
orch._merge_squash_with_unshallow_fallback(head_sha="abc123")
|
|
612
|
+
|
|
613
|
+
assert "conflict" in str(exc_info.value).lower()
|
|
614
|
+
|
|
615
|
+
def test_merge_fails_unrelated_histories_not_shallow_raises(
|
|
616
|
+
self, tmp_path: Path
|
|
617
|
+
) -> None:
|
|
618
|
+
"""Unrelated histories in a non-shallow clone raises immediately."""
|
|
619
|
+
git_dir = tmp_path / ".git"
|
|
620
|
+
git_dir.mkdir()
|
|
621
|
+
# No shallow file — not a shallow clone
|
|
622
|
+
|
|
623
|
+
def fake_run_cmd(cmd: list[str], **kwargs: Any) -> CommandResult:
|
|
624
|
+
if cmd[:2] == ["git", "merge"] and "--squash" in cmd:
|
|
625
|
+
raise CommandError(
|
|
626
|
+
"fatal: refusing to merge unrelated histories",
|
|
627
|
+
returncode=128,
|
|
628
|
+
stderr="fatal: refusing to merge unrelated histories",
|
|
629
|
+
)
|
|
630
|
+
if "is-shallow-repository" in cmd:
|
|
631
|
+
return CommandResult(returncode=0, stdout="false\n", stderr="")
|
|
632
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
633
|
+
|
|
634
|
+
with patch("github2gerrit.core.run_cmd", side_effect=fake_run_cmd):
|
|
635
|
+
orch = Orchestrator(workspace=tmp_path)
|
|
636
|
+
with pytest.raises(CommandError) as exc_info:
|
|
637
|
+
orch._merge_squash_with_unshallow_fallback(head_sha="abc123")
|
|
638
|
+
|
|
639
|
+
assert "unrelated histories" in str(exc_info.value).lower()
|
|
640
|
+
|
|
641
|
+
def test_merge_fails_all_recovery_fails_raises(
|
|
642
|
+
self, tmp_path: Path
|
|
643
|
+
) -> None:
|
|
644
|
+
"""Merge fails, deepen and unshallow both fail, raises original error."""
|
|
645
|
+
git_dir = tmp_path / ".git"
|
|
646
|
+
git_dir.mkdir()
|
|
647
|
+
(git_dir / "shallow").touch()
|
|
648
|
+
|
|
649
|
+
def fake_run_cmd(cmd: list[str], **kwargs: Any) -> CommandResult:
|
|
650
|
+
if cmd[:2] == ["git", "merge"] and "--squash" in cmd:
|
|
651
|
+
raise CommandError(
|
|
652
|
+
"fatal: refusing to merge unrelated histories",
|
|
653
|
+
returncode=128,
|
|
654
|
+
stderr="fatal: refusing to merge unrelated histories",
|
|
655
|
+
)
|
|
656
|
+
if cmd[:2] == ["git", "merge"] and "--abort" in cmd:
|
|
657
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
658
|
+
if "--deepen=" in str(cmd):
|
|
659
|
+
raise CommandError("deepen failed", returncode=1)
|
|
660
|
+
if cmd[:3] == ["git", "fetch", "--unshallow"]:
|
|
661
|
+
raise CommandError("unshallow failed", returncode=1)
|
|
662
|
+
if "is-shallow-repository" in cmd:
|
|
663
|
+
return CommandResult(returncode=0, stdout="true\n", stderr="")
|
|
664
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
665
|
+
|
|
666
|
+
with patch("github2gerrit.core.run_cmd", side_effect=fake_run_cmd):
|
|
667
|
+
orch = Orchestrator(workspace=tmp_path)
|
|
668
|
+
with pytest.raises(CommandError) as exc_info:
|
|
669
|
+
orch._merge_squash_with_unshallow_fallback(head_sha="abc123")
|
|
670
|
+
|
|
671
|
+
assert "unrelated histories" in str(exc_info.value).lower()
|
|
672
|
+
|
|
673
|
+
def test_merge_abort_called_before_retry(self, tmp_path: Path) -> None:
|
|
674
|
+
"""Ensure git merge --abort is called before retrying merge."""
|
|
675
|
+
git_dir = tmp_path / ".git"
|
|
676
|
+
git_dir.mkdir()
|
|
677
|
+
(git_dir / "shallow").touch()
|
|
678
|
+
|
|
679
|
+
call_log: list[list[str]] = []
|
|
680
|
+
merge_attempts = [0]
|
|
681
|
+
|
|
682
|
+
def fake_run_cmd(cmd: list[str], **kwargs: Any) -> CommandResult:
|
|
683
|
+
call_log.append(list(cmd))
|
|
684
|
+
if cmd[:2] == ["git", "merge"] and "--squash" in cmd:
|
|
685
|
+
merge_attempts[0] += 1
|
|
686
|
+
if merge_attempts[0] == 1:
|
|
687
|
+
raise CommandError(
|
|
688
|
+
"fatal: refusing to merge unrelated histories",
|
|
689
|
+
returncode=128,
|
|
690
|
+
stderr="fatal: refusing to merge unrelated histories",
|
|
691
|
+
)
|
|
692
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
693
|
+
if cmd[:2] == ["git", "merge"] and "--abort" in cmd:
|
|
694
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
695
|
+
if "--deepen=" in str(cmd):
|
|
696
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
697
|
+
if "is-shallow-repository" in cmd:
|
|
698
|
+
return CommandResult(returncode=0, stdout="true\n", stderr="")
|
|
699
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
700
|
+
|
|
701
|
+
with patch("github2gerrit.core.run_cmd", side_effect=fake_run_cmd):
|
|
702
|
+
orch = Orchestrator(workspace=tmp_path)
|
|
703
|
+
orch._merge_squash_with_unshallow_fallback(head_sha="abc123")
|
|
704
|
+
|
|
705
|
+
# Find the position of the abort call and the second merge call
|
|
706
|
+
abort_indices = [
|
|
707
|
+
i
|
|
708
|
+
for i, c in enumerate(call_log)
|
|
709
|
+
if c[:2] == ["git", "merge"] and "--abort" in c
|
|
710
|
+
]
|
|
711
|
+
second_merge_indices = [
|
|
712
|
+
i
|
|
713
|
+
for i, c in enumerate(call_log)
|
|
714
|
+
if c == ["git", "merge", "--squash", "abc123"]
|
|
715
|
+
]
|
|
716
|
+
# abort should come before the second merge attempt
|
|
717
|
+
assert len(abort_indices) >= 1
|
|
718
|
+
assert len(second_merge_indices) == 2
|
|
719
|
+
assert abort_indices[0] < second_merge_indices[1]
|
|
720
|
+
|
|
721
|
+
@pytest.mark.parametrize(
|
|
722
|
+
"error_message,stderr_message",
|
|
723
|
+
[
|
|
724
|
+
(
|
|
725
|
+
"fatal: refusing to merge unrelated histories",
|
|
726
|
+
"fatal: refusing to merge unrelated histories",
|
|
727
|
+
),
|
|
728
|
+
(
|
|
729
|
+
"merge failed: unrelated histories",
|
|
730
|
+
"unrelated histories detected",
|
|
731
|
+
),
|
|
732
|
+
(
|
|
733
|
+
"no common ancestor found",
|
|
734
|
+
"fatal: no common ancestor",
|
|
735
|
+
),
|
|
736
|
+
],
|
|
737
|
+
)
|
|
738
|
+
def test_recognizes_unrelated_history_errors(
|
|
739
|
+
self,
|
|
740
|
+
tmp_path: Path,
|
|
741
|
+
error_message: str,
|
|
742
|
+
stderr_message: str,
|
|
743
|
+
) -> None:
|
|
744
|
+
"""All variations of unrelated history errors trigger deepening."""
|
|
745
|
+
git_dir = tmp_path / ".git"
|
|
746
|
+
git_dir.mkdir()
|
|
747
|
+
(git_dir / "shallow").touch()
|
|
748
|
+
|
|
749
|
+
merge_attempts = [0]
|
|
750
|
+
|
|
751
|
+
def fake_run_cmd(cmd: list[str], **kwargs: Any) -> CommandResult:
|
|
752
|
+
if cmd[:2] == ["git", "merge"] and "--squash" in cmd:
|
|
753
|
+
merge_attempts[0] += 1
|
|
754
|
+
if merge_attempts[0] == 1:
|
|
755
|
+
raise CommandError(
|
|
756
|
+
error_message,
|
|
757
|
+
returncode=128,
|
|
758
|
+
stderr=stderr_message,
|
|
759
|
+
)
|
|
760
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
761
|
+
if cmd[:2] == ["git", "merge"] and "--abort" in cmd:
|
|
762
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
763
|
+
if "--deepen=" in str(cmd):
|
|
764
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
765
|
+
if "is-shallow-repository" in cmd:
|
|
766
|
+
return CommandResult(returncode=0, stdout="true\n", stderr="")
|
|
767
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
768
|
+
|
|
769
|
+
with patch("github2gerrit.core.run_cmd", side_effect=fake_run_cmd):
|
|
770
|
+
orch = Orchestrator(workspace=tmp_path)
|
|
771
|
+
orch._merge_squash_with_unshallow_fallback(head_sha="abc123")
|
|
772
|
+
|
|
773
|
+
# Should have attempted merge twice (before and after deepen)
|
|
774
|
+
assert merge_attempts[0] == 2
|
|
775
|
+
|
|
776
|
+
def test_deepen_changes_error_to_conflict_raises_without_unshallow(
|
|
777
|
+
self, tmp_path: Path
|
|
778
|
+
) -> None:
|
|
779
|
+
"""After deepen, if error changes from unrelated histories to a merge
|
|
780
|
+
conflict, raise immediately instead of attempting expensive unshallow."""
|
|
781
|
+
git_dir = tmp_path / ".git"
|
|
782
|
+
git_dir.mkdir()
|
|
783
|
+
(git_dir / "shallow").touch()
|
|
784
|
+
|
|
785
|
+
merge_attempts = [0]
|
|
786
|
+
call_log: list[list[str]] = []
|
|
787
|
+
|
|
788
|
+
def fake_run_cmd(cmd: list[str], **kwargs: Any) -> CommandResult:
|
|
789
|
+
call_log.append(list(cmd))
|
|
790
|
+
if cmd[:2] == ["git", "merge"] and "--squash" in cmd:
|
|
791
|
+
merge_attempts[0] += 1
|
|
792
|
+
if merge_attempts[0] == 1:
|
|
793
|
+
# First attempt: unrelated histories (shallow clone issue)
|
|
794
|
+
raise CommandError(
|
|
795
|
+
"fatal: refusing to merge unrelated histories",
|
|
796
|
+
returncode=128,
|
|
797
|
+
stderr="fatal: refusing to merge unrelated histories",
|
|
798
|
+
)
|
|
799
|
+
# Second attempt after deepen: now a real merge conflict
|
|
800
|
+
raise CommandError(
|
|
801
|
+
"Merge conflict in file.txt",
|
|
802
|
+
returncode=1,
|
|
803
|
+
stderr="CONFLICT (content): Merge conflict in file.txt",
|
|
804
|
+
)
|
|
805
|
+
if cmd[:2] == ["git", "merge"] and "--abort" in cmd:
|
|
806
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
807
|
+
if "--deepen=" in str(cmd):
|
|
808
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
809
|
+
if "is-shallow-repository" in cmd:
|
|
810
|
+
return CommandResult(returncode=0, stdout="true\n", stderr="")
|
|
811
|
+
return CommandResult(returncode=0, stdout="", stderr="")
|
|
812
|
+
|
|
813
|
+
with patch("github2gerrit.core.run_cmd", side_effect=fake_run_cmd):
|
|
814
|
+
orch = Orchestrator(workspace=tmp_path)
|
|
815
|
+
with pytest.raises(CommandError) as exc_info:
|
|
816
|
+
orch._merge_squash_with_unshallow_fallback(head_sha="abc123")
|
|
817
|
+
|
|
818
|
+
# Should have raised the conflict error, not the original
|
|
819
|
+
assert "conflict" in str(exc_info.value).lower()
|
|
820
|
+
|
|
821
|
+
# Should have attempted merge twice only (no third attempt after unshallow)
|
|
822
|
+
assert merge_attempts[0] == 2
|
|
823
|
+
|
|
824
|
+
# Should NOT have attempted unshallow
|
|
825
|
+
unshallow_calls = [c for c in call_log if "--unshallow" in c]
|
|
826
|
+
assert len(unshallow_calls) == 0
|
|
@@ -99,7 +99,7 @@ def test_apply_pr_title_body_preserves_change_id_footer(
|
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
# Stub GitHub API helpers used by _apply_pr_title_body_if_requested
|
|
102
|
-
monkeypatch.setattr("github2gerrit.core.build_client",
|
|
102
|
+
monkeypatch.setattr("github2gerrit.core.build_client", object)
|
|
103
103
|
monkeypatch.setattr(
|
|
104
104
|
"github2gerrit.core.get_repo_from_env", lambda client: object()
|
|
105
105
|
)
|