github2gerrit 1.3.1__tar.gz → 1.3.3__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.3.1 → github2gerrit-1.3.3}/PKG-INFO +1 -1
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/sitecustomize.py +6 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/cli.py +16 -4
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/core.py +0 -17
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/duplicate_detection.py +0 -9
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/external_api.py +2 -2
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gerrit_rest.py +0 -13
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/github_api.py +22 -12
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gitreview.py +0 -5
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gitutils.py +2 -1
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/ssh_discovery.py +0 -1
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_dependency_supersession.py +68 -41
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_external_api_framework.py +2 -2
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gitutils_helpers.py +79 -31
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.editorconfig +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.gitignore +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.gitlint +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.markdownlint.yaml +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.pre-commit-config.yaml +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.readthedocs.yml +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/.yamllint +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/LICENSE +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/LICENSES/Apache-2.0.txt +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/README.md +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/REUSE.toml +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/SECURITY.md +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/action.yaml +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/docs/COMMIT_RULES.md +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/docs/RELEASE-v0.2.0.md +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/docs/github2gerrit_token_permissions_classic.png +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/pyproject.toml +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/__init__.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/commit_normalization.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/commit_rules.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/config.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/constants.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/error_codes.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gerrit_pr_closer.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gerrit_query.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gerrit_ssh.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/gerrit_urls.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/mapping_comment.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/models.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/netrc.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/orchestrator/__init__.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/pr_commands.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/pr_content_filter.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/reconcile_matcher.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/rich_display.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/rich_logging.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/similarity.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/ssh_agent_setup.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/ssh_common.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/ssh_config_parser.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/trailers.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/utils.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/conftest.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/fixtures/__init__.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/fixtures/make_repo.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/fixtures/ssh_config_samples.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_action_environment_mapping.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_action_outputs.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_action_pr_number_handling.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_action_step_validation.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_automation_only.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_change_id_deduplication.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_clean_squash_title.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_cli.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_cli_helpers.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_cli_netrc_options.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_cli_outputs_file.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_cli_url_and_dryrun.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_commit_normalization.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_commit_rules.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_composite_action_coverage.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_config_and_reviewers.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_config_helpers.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_close_pr_policy.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_config_and_errors.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_gerrit_backref_comment.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_gerrit_push_errors.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_gerrit_rest_results.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_integration_fixture_repo.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_prepare_commits.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_shallow_clone.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_ssh_setup.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_core_ssrf_protection.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_dns_validation_and_no_gerrit.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_duplicate_detection.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_email_case_normalization.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_error_codes.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_force_flag_cli.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_change_id_footer.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_change_status_checks.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_pr_closer.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_rest_client.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_ssh_abandon.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_urls.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gerrit_urls_more.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ghe_and_gitreview_args.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_github_api_error_handling.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_github_api_helpers.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_github_api_retry_and_helpers.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_gitreview.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_issue_157_regressions.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_mapping_comment_additional.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_mapping_comment_digest_and_backref.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_metadata_and_reconciliation.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_metadata_trailer_separation_bug.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_misc_small_coverage.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_netrc.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_orphan_rest_side_effects.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_pr_commands.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_pr_content_filter.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_pr_content_filter_integration.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_pr_update_detection.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_reconciliation_extracted_module.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_reconciliation_plan_and_orphans.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_reconciliation_scenarios.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ssh_agent.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ssh_agent_ownership.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ssh_artifact_prevention.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ssh_common.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ssh_discovery.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_ssh_discovery_dry_run.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_trailers_additional.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_url_parser.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_utils.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/unit/test_config_integration.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/unit/test_ssh_config_parser.py +0 -0
- {github2gerrit-1.3.1 → github2gerrit-1.3.3}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github2gerrit
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.3
|
|
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
|
|
@@ -57,6 +57,8 @@ def _dbg(msg: str) -> None:
|
|
|
57
57
|
try:
|
|
58
58
|
print(f"[sitecustomize] {msg}", file=sys.stderr)
|
|
59
59
|
except Exception:
|
|
60
|
+
# Debug logging must never disrupt interpreter startup; if
|
|
61
|
+
# writing to stderr fails, silently drop the message.
|
|
60
62
|
pass
|
|
61
63
|
|
|
62
64
|
|
|
@@ -125,6 +127,8 @@ def _collect_base_dirs(repo_root: Path) -> set[Path]:
|
|
|
125
127
|
try:
|
|
126
128
|
bases.add(Path.cwd())
|
|
127
129
|
except Exception:
|
|
130
|
+
# CWD may be unavailable (e.g. deleted); skip it and continue
|
|
131
|
+
# collecting the remaining base directories.
|
|
128
132
|
pass
|
|
129
133
|
# Repository root (directory containing this file)
|
|
130
134
|
bases.add(repo_root)
|
|
@@ -134,6 +138,8 @@ def _collect_base_dirs(repo_root: Path) -> set[Path]:
|
|
|
134
138
|
try:
|
|
135
139
|
bases.add(Path(gw))
|
|
136
140
|
except Exception:
|
|
141
|
+
# A malformed GITHUB_WORKSPACE value should not abort cleanup;
|
|
142
|
+
# skip it and continue with the other base directories.
|
|
137
143
|
pass
|
|
138
144
|
return bases
|
|
139
145
|
|
|
@@ -385,12 +385,15 @@ else:
|
|
|
385
385
|
|
|
386
386
|
|
|
387
387
|
class _FormatterProto(Protocol):
|
|
388
|
-
def write_usage(self, prog: str, args: str, prefix: str = ...) -> None:
|
|
388
|
+
def write_usage(self, prog: str, args: str, prefix: str = ...) -> None:
|
|
389
|
+
"""Write the program usage line."""
|
|
389
390
|
|
|
390
391
|
|
|
391
392
|
class _ContextProto(Protocol):
|
|
392
393
|
@property
|
|
393
|
-
def command_path(self) -> str:
|
|
394
|
+
def command_path(self) -> str:
|
|
395
|
+
"""Return the full command path."""
|
|
396
|
+
raise NotImplementedError
|
|
394
397
|
|
|
395
398
|
|
|
396
399
|
class _SingleUsageGroup(BaseGroup):
|
|
@@ -471,14 +474,23 @@ def _resolve_issue_id_from_json(json_str: str, github_actor: str) -> str:
|
|
|
471
474
|
return ""
|
|
472
475
|
|
|
473
476
|
|
|
474
|
-
|
|
475
|
-
|
|
477
|
+
def _print_version_banner() -> None:
|
|
478
|
+
"""Print the resolved package version, or a fallback notice.
|
|
479
|
+
|
|
480
|
+
Defined as a function so the ``print`` calls do not execute at module
|
|
481
|
+
scope during import; the banner is only emitted when explicitly invoked.
|
|
482
|
+
"""
|
|
476
483
|
try:
|
|
477
484
|
app_version = get_version("github2gerrit")
|
|
478
485
|
print(f"🏷️ github2gerrit version {app_version}")
|
|
479
486
|
except Exception:
|
|
480
487
|
print("⚠️ github2gerrit version information not available")
|
|
481
488
|
|
|
489
|
+
|
|
490
|
+
# Show version information when --help is used or in GitHub Actions mode
|
|
491
|
+
if "--help" in sys.argv or _is_github_actions_context():
|
|
492
|
+
_print_version_banner()
|
|
493
|
+
|
|
482
494
|
app: typer.Typer = typer.Typer(
|
|
483
495
|
add_completion=False,
|
|
484
496
|
no_args_is_help=False,
|
|
@@ -80,19 +80,10 @@ from .utils import is_verbose_mode
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
try:
|
|
83
|
-
from pygerrit2 import GerritRestAPI
|
|
84
|
-
from pygerrit2 import HTTPBasicAuth
|
|
85
|
-
except ImportError:
|
|
86
|
-
GerritRestAPI = None
|
|
87
|
-
HTTPBasicAuth = None
|
|
88
|
-
|
|
89
|
-
try:
|
|
90
|
-
from .ssh_discovery import SSHDiscoveryError
|
|
91
83
|
from .ssh_discovery import auto_discover_gerrit_host_keys
|
|
92
84
|
except ImportError:
|
|
93
85
|
# Fallback if ssh_discovery module is not available
|
|
94
86
|
auto_discover_gerrit_host_keys = None # type: ignore[assignment]
|
|
95
|
-
SSHDiscoveryError = Exception # type: ignore[misc,assignment]
|
|
96
87
|
|
|
97
88
|
try:
|
|
98
89
|
from .ssh_agent_setup import SSHAgentManager
|
|
@@ -113,7 +104,6 @@ log = logging.getLogger("github2gerrit.core")
|
|
|
113
104
|
|
|
114
105
|
|
|
115
106
|
# Error message constants to comply with TRY003
|
|
116
|
-
_MSG_ISSUE_ID_MULTILINE = "Issue ID must be single line"
|
|
117
107
|
_MSG_MISSING_PR_CONTEXT = "missing PR context"
|
|
118
108
|
_MSG_BAD_REPOSITORY_CONTEXT = "bad repository context"
|
|
119
109
|
_MSG_MISSING_GERRIT_SERVER = (
|
|
@@ -121,10 +111,6 @@ _MSG_MISSING_GERRIT_SERVER = (
|
|
|
121
111
|
"input/environment variable, .gitreview file, or action configuration."
|
|
122
112
|
)
|
|
123
113
|
_MSG_MISSING_GERRIT_PROJECT = "missing GERRIT_PROJECT"
|
|
124
|
-
_MSG_PYGERRIT2_REQUIRED_REST = "pygerrit2 is required to query Gerrit REST API"
|
|
125
|
-
_MSG_PYGERRIT2_REQUIRED_AUTH = "pygerrit2 is required for HTTP authentication"
|
|
126
|
-
_MSG_PYGERRIT2_MISSING = "pygerrit2 missing"
|
|
127
|
-
_MSG_PYGERRIT2_AUTH_MISSING = "pygerrit2 auth missing"
|
|
128
114
|
_MSG_DNS_RESOLUTION_FAILED = "DNS resolution failed for '%s'"
|
|
129
115
|
|
|
130
116
|
|
|
@@ -4429,7 +4415,6 @@ class Orchestrator:
|
|
|
4429
4415
|
List of email addresses that were not found in Gerrit
|
|
4430
4416
|
"""
|
|
4431
4417
|
combined_output = f"{exc.stdout}\n{exc.stderr}"
|
|
4432
|
-
import re
|
|
4433
4418
|
|
|
4434
4419
|
# Pattern to match: Account 'email@domain.com' not found
|
|
4435
4420
|
pattern = r"Account\s+'([^']+)'\s+not\s+found"
|
|
@@ -4903,7 +4888,6 @@ class Orchestrator:
|
|
|
4903
4888
|
# Extract specific rejection reason from output
|
|
4904
4889
|
# Handle multiline rejection messages by looking in normalized
|
|
4905
4890
|
# output
|
|
4906
|
-
import re
|
|
4907
4891
|
|
|
4908
4892
|
# Look for the rejection pattern in the normalized output
|
|
4909
4893
|
rejection_match = re.search(
|
|
@@ -5299,7 +5283,6 @@ class Orchestrator:
|
|
|
5299
5283
|
self, workspace: Path, gh: GitHubContext, inputs: Inputs
|
|
5300
5284
|
) -> None:
|
|
5301
5285
|
"""Fallback to GitHub API archive download (ported from CLI)."""
|
|
5302
|
-
import json
|
|
5303
5286
|
import zipfile
|
|
5304
5287
|
|
|
5305
5288
|
repo_full = gh.repository.strip() if gh.repository else ""
|
|
@@ -32,15 +32,6 @@ from .rich_display import safe_console_print
|
|
|
32
32
|
from .trailers import extract_github_metadata
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
# Optional Gerrit REST API support
|
|
36
|
-
try:
|
|
37
|
-
from pygerrit2 import GerritRestAPI
|
|
38
|
-
from pygerrit2 import HTTPBasicAuth
|
|
39
|
-
except ImportError:
|
|
40
|
-
GerritRestAPI = None
|
|
41
|
-
HTTPBasicAuth = None
|
|
42
|
-
|
|
43
|
-
|
|
44
35
|
log = logging.getLogger(__name__)
|
|
45
36
|
|
|
46
37
|
__all__ = [
|
|
@@ -327,7 +327,7 @@ def external_api_call(
|
|
|
327
327
|
policy=policy,
|
|
328
328
|
)
|
|
329
329
|
|
|
330
|
-
last_exc:
|
|
330
|
+
last_exc: Exception | None = None
|
|
331
331
|
|
|
332
332
|
for attempt in range(1, policy.max_attempts + 1):
|
|
333
333
|
context.attempt = attempt
|
|
@@ -347,7 +347,7 @@ def external_api_call(
|
|
|
347
347
|
|
|
348
348
|
# Call the actual function
|
|
349
349
|
result = func(*args, **kwargs)
|
|
350
|
-
except
|
|
350
|
+
except Exception as exc:
|
|
351
351
|
last_exc = exc
|
|
352
352
|
duration = time.time() - context.start_time
|
|
353
353
|
|
|
@@ -68,19 +68,6 @@ _MSG_PYGERRIT2_REQUIRED_AUTH: Final[str] = (
|
|
|
68
68
|
"pygerrit2 is required for HTTP authentication"
|
|
69
69
|
)
|
|
70
70
|
|
|
71
|
-
_TRANSIENT_ERR_SUBSTRINGS: Final[tuple[str, ...]] = (
|
|
72
|
-
"timed out",
|
|
73
|
-
"temporarily unavailable",
|
|
74
|
-
"temporary failure",
|
|
75
|
-
"connection reset",
|
|
76
|
-
"connection aborted",
|
|
77
|
-
"broken pipe",
|
|
78
|
-
"connection refused",
|
|
79
|
-
"bad gateway",
|
|
80
|
-
"service unavailable",
|
|
81
|
-
"gateway timeout",
|
|
82
|
-
)
|
|
83
|
-
|
|
84
71
|
# HTTP status codes that indicate an authentication/authorization problem
|
|
85
72
|
# rather than a transient fault or a bug. These are expected when no
|
|
86
73
|
# Gerrit REST credentials are available (or they are insufficient for the
|
|
@@ -23,7 +23,6 @@ from collections.abc import Iterable
|
|
|
23
23
|
from importlib import import_module
|
|
24
24
|
from typing import Any
|
|
25
25
|
from typing import Protocol
|
|
26
|
-
from typing import TypeVar
|
|
27
26
|
from typing import cast
|
|
28
27
|
|
|
29
28
|
from .error_codes import ExitCode
|
|
@@ -35,8 +34,6 @@ from .external_api import external_api_call
|
|
|
35
34
|
|
|
36
35
|
# Error message constants to comply with TRY003
|
|
37
36
|
_MSG_PYGITHUB_REQUIRED = "PyGithub required"
|
|
38
|
-
_MSG_MISSING_GITHUB_TOKEN = "missing GITHUB_TOKEN" # noqa: S105
|
|
39
|
-
_MSG_BAD_GITHUB_REPOSITORY = "bad GITHUB_REPOSITORY"
|
|
40
37
|
|
|
41
38
|
|
|
42
39
|
class GithubExceptionType(Exception):
|
|
@@ -82,8 +79,12 @@ class GhIssueComment(Protocol):
|
|
|
82
79
|
|
|
83
80
|
|
|
84
81
|
class GhIssue(Protocol):
|
|
85
|
-
def get_comments(self) -> Iterable[GhIssueComment]:
|
|
86
|
-
|
|
82
|
+
def get_comments(self) -> Iterable[GhIssueComment]:
|
|
83
|
+
"""Return the comments on the issue."""
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
86
|
+
def create_comment(self, body: str) -> None:
|
|
87
|
+
"""Create a new comment on the issue."""
|
|
87
88
|
|
|
88
89
|
|
|
89
90
|
class GhPullRequest(Protocol):
|
|
@@ -91,17 +92,28 @@ class GhPullRequest(Protocol):
|
|
|
91
92
|
title: str | None
|
|
92
93
|
body: str | None
|
|
93
94
|
|
|
94
|
-
def as_issue(self) -> GhIssue:
|
|
95
|
-
|
|
95
|
+
def as_issue(self) -> GhIssue:
|
|
96
|
+
"""Return the issue view of this pull request."""
|
|
97
|
+
raise NotImplementedError
|
|
98
|
+
|
|
99
|
+
def edit(self, *, state: str) -> None:
|
|
100
|
+
"""Edit the pull request state."""
|
|
96
101
|
|
|
97
102
|
|
|
98
103
|
class GhRepository(Protocol):
|
|
99
|
-
def get_pull(self, number: int) -> GhPullRequest:
|
|
100
|
-
|
|
104
|
+
def get_pull(self, number: int) -> GhPullRequest:
|
|
105
|
+
"""Return the pull request with the given number."""
|
|
106
|
+
raise NotImplementedError
|
|
107
|
+
|
|
108
|
+
def get_pulls(self, state: str) -> Iterable[GhPullRequest]:
|
|
109
|
+
"""Return the pull requests with the given state."""
|
|
110
|
+
raise NotImplementedError
|
|
101
111
|
|
|
102
112
|
|
|
103
113
|
class GhClient(Protocol):
|
|
104
|
-
def get_repo(self, full: str) -> GhRepository:
|
|
114
|
+
def get_repo(self, full: str) -> GhRepository:
|
|
115
|
+
"""Return the repository for the given full name."""
|
|
116
|
+
raise NotImplementedError
|
|
105
117
|
|
|
106
118
|
|
|
107
119
|
__all__ = [
|
|
@@ -120,8 +132,6 @@ __all__ = [
|
|
|
120
132
|
|
|
121
133
|
log = logging.getLogger("github2gerrit.github_api")
|
|
122
134
|
|
|
123
|
-
_T = TypeVar("_T")
|
|
124
|
-
|
|
125
135
|
|
|
126
136
|
def _getenv_str(name: str) -> str:
|
|
127
137
|
val = os.getenv(name, "")
|
|
@@ -151,11 +151,6 @@ def derive_base_path(host: str) -> str | None:
|
|
|
151
151
|
return _KNOWN_BASE_PATHS.get(host.lower().strip())
|
|
152
152
|
|
|
153
153
|
|
|
154
|
-
# Keep a private alias so internal callers don't break if they were
|
|
155
|
-
# referencing the underscore-prefixed name during development.
|
|
156
|
-
_derive_base_path = derive_base_path
|
|
157
|
-
|
|
158
|
-
|
|
159
154
|
# ───────────────────────────────────────────────────────────────────────
|
|
160
155
|
# Pure parser
|
|
161
156
|
# ───────────────────────────────────────────────────────────────────────
|
|
@@ -555,6 +555,8 @@ def git_commit_amend(
|
|
|
555
555
|
if committer_email and _has_committer_signoff(body):
|
|
556
556
|
effective_signoff = False
|
|
557
557
|
except GitError:
|
|
558
|
+
# Unable to read the current commit body (e.g. no HEAD yet);
|
|
559
|
+
# fall back to honoring the requested signoff setting.
|
|
558
560
|
pass
|
|
559
561
|
except Exception:
|
|
560
562
|
# Best effort only; default to requested signoff
|
|
@@ -613,7 +615,6 @@ def git_commit_new(
|
|
|
613
615
|
_tf.flush()
|
|
614
616
|
tmp_path = Path(_tf.name)
|
|
615
617
|
message_file = tmp_path
|
|
616
|
-
message = None
|
|
617
618
|
|
|
618
619
|
# Determine whether to add -s; only suppress if message already has a
|
|
619
620
|
# sign-off for current committer
|
|
@@ -379,7 +379,6 @@ def save_host_keys_to_config(
|
|
|
379
379
|
new_lines.append(f"[{organization}]")
|
|
380
380
|
escaped_keys = host_keys.replace("\n", "\\n")
|
|
381
381
|
new_lines.append(f'GERRIT_KNOWN_HOSTS = "{escaped_keys}"')
|
|
382
|
-
gerrit_known_hosts_updated = True
|
|
383
382
|
|
|
384
383
|
# If section existed but didn't have GERRIT_KNOWN_HOSTS, add it
|
|
385
384
|
elif not gerrit_known_hosts_updated:
|
|
@@ -17,6 +17,7 @@ Covers two mechanisms:
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
import hashlib
|
|
20
21
|
from dataclasses import dataclass
|
|
21
22
|
from typing import Any
|
|
22
23
|
from unittest.mock import MagicMock
|
|
@@ -31,6 +32,7 @@ from github2gerrit.gerrit_query import query_open_changes_by_project
|
|
|
31
32
|
from github2gerrit.gitreview import GerritInfo
|
|
32
33
|
from github2gerrit.models import GitHubContext
|
|
33
34
|
from github2gerrit.similarity import extract_dependency_package_from_subject
|
|
35
|
+
from github2gerrit.utils import reset_warning_once
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
# -------------------------------------------------------------------
|
|
@@ -38,6 +40,46 @@ from github2gerrit.similarity import extract_dependency_package_from_subject
|
|
|
38
40
|
# -------------------------------------------------------------------
|
|
39
41
|
|
|
40
42
|
|
|
43
|
+
# Dependency fixture values reused across the supersession tests.
|
|
44
|
+
# Centralising them documents which data is significant to the logic under
|
|
45
|
+
# test (the *package name* must match for a change to be superseded) versus
|
|
46
|
+
# which is arbitrary (the exact version strings).
|
|
47
|
+
_DEP_PACKAGE = "requests"
|
|
48
|
+
_DEP_VERSION_OLD = "2.31.0"
|
|
49
|
+
_DEP_VERSION_NEW = "2.32.0"
|
|
50
|
+
_DEP_VERSION_INTERMEDIATE = "2.31.5"
|
|
51
|
+
_DEP_BUMP_TITLE = (
|
|
52
|
+
f"Bump {_DEP_PACKAGE} from {_DEP_VERSION_OLD} to {_DEP_VERSION_NEW}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.fixture(autouse=True)
|
|
57
|
+
def _reset_warning_once_state() -> None:
|
|
58
|
+
"""Reset the one-time warning cache before each test.
|
|
59
|
+
|
|
60
|
+
Several tests assert warn-once behaviour (for example the truncation
|
|
61
|
+
warning emitted by the anonymous fallback), which relies on process-wide
|
|
62
|
+
global state. Resetting it automatically keeps every test isolated so
|
|
63
|
+
individual tests no longer need to call ``reset_warning_once`` by hand.
|
|
64
|
+
"""
|
|
65
|
+
reset_warning_once()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _make_change_id(seed: str) -> str:
|
|
69
|
+
"""Return a valid-looking Gerrit Change-Id derived from ``seed``.
|
|
70
|
+
|
|
71
|
+
A real Gerrit Change-Id is the letter ``I`` followed by 40 hexadecimal
|
|
72
|
+
characters (a SHA-1 digest). Tests previously used mnemonic but invalid
|
|
73
|
+
values such as ``Iexist00...`` that contain non-hex characters. This
|
|
74
|
+
helper instead derives a truncated SHA-256 digest from a human-readable
|
|
75
|
+
seed: the exact hash algorithm is unimportant, it only needs to match
|
|
76
|
+
the ``I`` + 40-hex-character format while keeping failures easy to
|
|
77
|
+
trace.
|
|
78
|
+
"""
|
|
79
|
+
digest = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:40]
|
|
80
|
+
return f"I{digest}"
|
|
81
|
+
|
|
82
|
+
|
|
41
83
|
def _gerrit_change(
|
|
42
84
|
*,
|
|
43
85
|
change_id: str = "Iaaaa000000000000000000000000000000000000",
|
|
@@ -202,10 +244,6 @@ class TestQueryOpenChangesByProject:
|
|
|
202
244
|
to drive the anonymous fallback, the helper must short-circuit and
|
|
203
245
|
never issue the request, returning an empty list.
|
|
204
246
|
"""
|
|
205
|
-
from github2gerrit.utils import reset_warning_once
|
|
206
|
-
|
|
207
|
-
reset_warning_once()
|
|
208
|
-
|
|
209
247
|
fake_client = MagicMock()
|
|
210
248
|
fake_client.is_authenticated = False
|
|
211
249
|
|
|
@@ -232,10 +270,6 @@ class TestQueryOpenChangesByProject:
|
|
|
232
270
|
The query must NOT use owner:self, and only changes whose
|
|
233
271
|
GitHub-PR trailer targets the current repository are returned.
|
|
234
272
|
"""
|
|
235
|
-
from github2gerrit.utils import reset_warning_once
|
|
236
|
-
|
|
237
|
-
reset_warning_once()
|
|
238
|
-
|
|
239
273
|
ours = _raw_change(
|
|
240
274
|
100,
|
|
241
275
|
"chore: bump requests from 2.31.0 to 2.32.0",
|
|
@@ -262,10 +296,6 @@ class TestQueryOpenChangesByProject:
|
|
|
262
296
|
|
|
263
297
|
def test_anonymous_fallback_repo_match_is_case_insensitive(self) -> None:
|
|
264
298
|
"""Repo trailer matching ignores case differences."""
|
|
265
|
-
from github2gerrit.utils import reset_warning_once
|
|
266
|
-
|
|
267
|
-
reset_warning_once()
|
|
268
|
-
|
|
269
299
|
ours = _raw_change(
|
|
270
300
|
200,
|
|
271
301
|
"chore: bump requests from 2.31.0 to 2.32.0",
|
|
@@ -283,10 +313,6 @@ class TestQueryOpenChangesByProject:
|
|
|
283
313
|
|
|
284
314
|
def test_anonymous_fallback_matches_ghes_host(self) -> None:
|
|
285
315
|
"""The host is ignored, so GitHub Enterprise URLs still match."""
|
|
286
|
-
from github2gerrit.utils import reset_warning_once
|
|
287
|
-
|
|
288
|
-
reset_warning_once()
|
|
289
|
-
|
|
290
316
|
ours = _raw_change(
|
|
291
317
|
210,
|
|
292
318
|
"chore: bump requests from 2.31.0 to 2.32.0",
|
|
@@ -310,10 +336,6 @@ class TestQueryOpenChangesByProject:
|
|
|
310
336
|
repo in its query string, a non-URL value, and a relative path
|
|
311
337
|
(e.g. ``/org/repo/pull/1``) are all correctly excluded.
|
|
312
338
|
"""
|
|
313
|
-
from github2gerrit.utils import reset_warning_once
|
|
314
|
-
|
|
315
|
-
reset_warning_once()
|
|
316
|
-
|
|
317
339
|
other = _raw_change(
|
|
318
340
|
220,
|
|
319
341
|
"chore: bump requests from 2.31.0 to 2.32.0",
|
|
@@ -346,10 +368,6 @@ class TestQueryOpenChangesByProject:
|
|
|
346
368
|
GitHub2Gerrit changes (which carry the GitHub-PR trailer) so busy
|
|
347
369
|
projects do not truncate before the relevant changes are seen.
|
|
348
370
|
"""
|
|
349
|
-
from github2gerrit.utils import reset_warning_once
|
|
350
|
-
|
|
351
|
-
reset_warning_once()
|
|
352
|
-
|
|
353
371
|
fake_client = MagicMock()
|
|
354
372
|
fake_client.is_authenticated = False
|
|
355
373
|
fake_client.get.return_value = []
|
|
@@ -369,10 +387,6 @@ class TestQueryOpenChangesByProject:
|
|
|
369
387
|
"""Hitting the result cap emits a one-time truncation warning."""
|
|
370
388
|
import logging
|
|
371
389
|
|
|
372
|
-
from github2gerrit.utils import reset_warning_once
|
|
373
|
-
|
|
374
|
-
reset_warning_once()
|
|
375
|
-
|
|
376
390
|
c1 = _raw_change(
|
|
377
391
|
300,
|
|
378
392
|
"chore: bump x from 1 to 2",
|
|
@@ -406,9 +420,6 @@ class TestQueryOpenChangesByProject:
|
|
|
406
420
|
self, monkeypatch: pytest.MonkeyPatch
|
|
407
421
|
) -> None:
|
|
408
422
|
"""Disabling the fallback flag skips even when repo is known."""
|
|
409
|
-
from github2gerrit.utils import reset_warning_once
|
|
410
|
-
|
|
411
|
-
reset_warning_once()
|
|
412
423
|
monkeypatch.setenv("G2G_ANON_SUPERSEDE_FALLBACK", "false")
|
|
413
424
|
|
|
414
425
|
fake_client = MagicMock()
|
|
@@ -422,10 +433,6 @@ class TestQueryOpenChangesByProject:
|
|
|
422
433
|
|
|
423
434
|
def test_anonymous_fallback_whitespace_repo_skips(self) -> None:
|
|
424
435
|
"""A blank/whitespace-only repository must not trigger a query."""
|
|
425
|
-
from github2gerrit.utils import reset_warning_once
|
|
426
|
-
|
|
427
|
-
reset_warning_once()
|
|
428
|
-
|
|
429
436
|
fake_client = MagicMock()
|
|
430
437
|
fake_client.is_authenticated = False
|
|
431
438
|
|
|
@@ -446,7 +453,16 @@ class TestQueryOpenChangesByProject:
|
|
|
446
453
|
new=lambda *_a, **_kw: "",
|
|
447
454
|
)
|
|
448
455
|
class TestAbandonSupersededDependencyChanges:
|
|
449
|
-
"""Tests for the post-push abandon sweep.
|
|
456
|
+
"""Tests for the post-push abandon sweep.
|
|
457
|
+
|
|
458
|
+
The class-level ``@patch`` stubs
|
|
459
|
+
``gerrit_urls._discover_base_path_for_host`` to return an empty base
|
|
460
|
+
path for *every* test method below, so URL construction does not attempt
|
|
461
|
+
host discovery (a network call). The patch is intentionally applied at
|
|
462
|
+
the class level because all methods exercise URL building; it is not
|
|
463
|
+
injected as a method argument because the stubbed value is never
|
|
464
|
+
inspected by the tests.
|
|
465
|
+
"""
|
|
450
466
|
|
|
451
467
|
@patch("github2gerrit.gerrit_rest.build_client_for_host")
|
|
452
468
|
@patch("github2gerrit.gerrit_query.query_open_changes_by_project")
|
|
@@ -789,7 +805,7 @@ class TestStrategy5Integration:
|
|
|
789
805
|
# Set up GitHub mocks — strategies 1-4 should all fail
|
|
790
806
|
mock_get_pull.return_value = _FakePullRequest(
|
|
791
807
|
number=42,
|
|
792
|
-
title=
|
|
808
|
+
title=_DEP_BUMP_TITLE,
|
|
793
809
|
user=_FakeGitHubUser(),
|
|
794
810
|
)
|
|
795
811
|
mock_get_repo.return_value = MagicMock()
|
|
@@ -803,10 +819,14 @@ class TestStrategy5Integration:
|
|
|
803
819
|
mock_gerrit_client.return_value = fake_gerrit
|
|
804
820
|
|
|
805
821
|
# Strategy 5 — return an open change bumping the same package
|
|
822
|
+
existing_change_id = _make_change_id("strategy5-existing")
|
|
806
823
|
existing_change = _gerrit_change(
|
|
807
|
-
change_id=
|
|
824
|
+
change_id=existing_change_id,
|
|
808
825
|
number="999",
|
|
809
|
-
subject=
|
|
826
|
+
subject=(
|
|
827
|
+
f"chore: bump {_DEP_PACKAGE} from "
|
|
828
|
+
f"{_DEP_VERSION_OLD} to {_DEP_VERSION_INTERMEDIATE}"
|
|
829
|
+
),
|
|
810
830
|
)
|
|
811
831
|
mock_query_open.return_value = [existing_change]
|
|
812
832
|
|
|
@@ -832,7 +852,7 @@ class TestStrategy5Integration:
|
|
|
832
852
|
)
|
|
833
853
|
|
|
834
854
|
result = orch._find_existing_change_for_pr(gh, gerrit)
|
|
835
|
-
assert result == [
|
|
855
|
+
assert result == [existing_change_id]
|
|
836
856
|
|
|
837
857
|
@patch("github2gerrit.core.build_client")
|
|
838
858
|
@patch("github2gerrit.core.get_repo_from_env")
|
|
@@ -956,7 +976,14 @@ class TestStrategy5Integration:
|
|
|
956
976
|
new=lambda *_a, **_kw: "",
|
|
957
977
|
)
|
|
958
978
|
class TestRealWorldScenarios:
|
|
959
|
-
"""Scenarios taken directly from the issue report.
|
|
979
|
+
"""Scenarios taken directly from the issue report.
|
|
980
|
+
|
|
981
|
+
As with :class:`TestAbandonSupersededDependencyChanges`, the class-level
|
|
982
|
+
``@patch`` stubs ``gerrit_urls._discover_base_path_for_host`` for every
|
|
983
|
+
method so URL construction avoids host discovery. The stub uses ``new=``
|
|
984
|
+
rather than a mock argument because its return value is fixed and never
|
|
985
|
+
asserted on.
|
|
986
|
+
"""
|
|
960
987
|
|
|
961
988
|
@patch("github2gerrit.gerrit_rest.build_client_for_host")
|
|
962
989
|
@patch("github2gerrit.gerrit_query.query_open_changes_by_project")
|
|
@@ -222,6 +222,8 @@ def test_curl_download_success(
|
|
|
222
222
|
output_path = Path(cmd[o_index + 1])
|
|
223
223
|
output_path.write_text("downloaded content")
|
|
224
224
|
except (ValueError, IndexError):
|
|
225
|
+
# No "-o" flag (or no value after it) in the mocked curl
|
|
226
|
+
# command; nothing to write, so behave like a no-op download.
|
|
225
227
|
pass
|
|
226
228
|
return MockCompletedProcess()
|
|
227
229
|
|
|
@@ -278,8 +280,6 @@ def test_log_api_metrics_summary(
|
|
|
278
280
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
279
281
|
) -> None:
|
|
280
282
|
"""Test API metrics summary logging."""
|
|
281
|
-
import logging
|
|
282
|
-
|
|
283
283
|
# Set up some metrics
|
|
284
284
|
reset_api_metrics()
|
|
285
285
|
|
|
@@ -16,19 +16,40 @@ from github2gerrit.gitutils import run_cmd
|
|
|
16
16
|
from github2gerrit.gitutils import run_cmd_with_retries
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
# Environment variables that pytest, pytest-cov, and pytest-xdist inject so
|
|
20
|
+
# that coverage measurement and test metadata propagate into subprocesses.
|
|
21
|
+
# When a test spawns the current Python interpreter via ``run_cmd`` we clear
|
|
22
|
+
# these so the child does not start its own coverage run (which can emit
|
|
23
|
+
# warnings or write stray data files) and so its output stays deterministic
|
|
24
|
+
# regardless of how the outer pytest session was invoked.
|
|
25
|
+
#
|
|
26
|
+
# COV_CORE_SOURCE/CONFIG/DATAFILE pytest-cov subprocess coverage hooks
|
|
27
|
+
# COVERAGE_PROCESS_START triggers coverage startup in children
|
|
28
|
+
# COVERAGE_FILE / COVERAGE_RCFILE coverage data / config file locations
|
|
29
|
+
# PYTEST_ADDOPTS extra pytest args inherited by children
|
|
30
|
+
# PYTEST_CURRENT_TEST current test id (set per test by pytest)
|
|
31
|
+
# PYTEST_XDIST_WORKER xdist worker id when running in parallel
|
|
32
|
+
_COVERAGE_ENV_KEYS = (
|
|
33
|
+
"COV_CORE_SOURCE",
|
|
34
|
+
"COV_CORE_CONFIG",
|
|
35
|
+
"COV_CORE_DATAFILE",
|
|
36
|
+
"COVERAGE_PROCESS_START",
|
|
37
|
+
"COVERAGE_FILE",
|
|
38
|
+
"COVERAGE_RCFILE",
|
|
39
|
+
"PYTEST_ADDOPTS",
|
|
40
|
+
"PYTEST_CURRENT_TEST",
|
|
41
|
+
"PYTEST_XDIST_WORKER",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
19
45
|
def _no_cov_env() -> dict[str, str]:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"PYTEST_ADDOPTS",
|
|
28
|
-
"PYTEST_CURRENT_TEST",
|
|
29
|
-
"PYTEST_XDIST_WORKER",
|
|
30
|
-
]
|
|
31
|
-
return dict.fromkeys(keys, "")
|
|
46
|
+
"""Return an env mapping that disables coverage in subprocesses.
|
|
47
|
+
|
|
48
|
+
Every key in :data:`_COVERAGE_ENV_KEYS` is mapped to an empty string so
|
|
49
|
+
a spawned child process inherits a clean, coverage-free environment. See
|
|
50
|
+
that constant for the rationale behind each cleared variable.
|
|
51
|
+
"""
|
|
52
|
+
return dict.fromkeys(_COVERAGE_ENV_KEYS, "")
|
|
32
53
|
|
|
33
54
|
|
|
34
55
|
def test_mask_text_replaces_tokens_and_ignores_empty() -> None:
|
|
@@ -93,24 +114,31 @@ def test_run_cmd_with_retries_retries_on_transient_error_then_succeeds(
|
|
|
93
114
|
|
|
94
115
|
monkeypatch.setattr("time.sleep", _fake_sleep)
|
|
95
116
|
|
|
96
|
-
#
|
|
97
|
-
# error
|
|
98
|
-
#
|
|
117
|
+
# The helper script creates a marker file and fails with a transient
|
|
118
|
+
# error on its first run, then succeeds once the marker exists. Writing
|
|
119
|
+
# the behaviour to a real script file (taking the marker path as an
|
|
120
|
+
# argument) keeps the test readable and avoids embedding Python source
|
|
121
|
+
# inside an f-string.
|
|
99
122
|
marker = tmp_path / "attempted"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
import
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
""
|
|
123
|
+
script = tmp_path / "transient_then_succeed.py"
|
|
124
|
+
script.write_text(
|
|
125
|
+
"import pathlib\n"
|
|
126
|
+
"import sys\n"
|
|
127
|
+
"\n"
|
|
128
|
+
"marker = pathlib.Path(sys.argv[1])\n"
|
|
129
|
+
"if not marker.exists():\n"
|
|
130
|
+
' marker.write_text("1", encoding="utf-8")\n'
|
|
131
|
+
' print("could not resolve host: example.com", file=sys.stderr)\n'
|
|
132
|
+
" sys.exit(128)\n"
|
|
133
|
+
"else:\n"
|
|
134
|
+
' print("ok")\n',
|
|
135
|
+
encoding="utf-8",
|
|
136
|
+
)
|
|
111
137
|
|
|
112
138
|
res = run_cmd_with_retries(
|
|
113
|
-
[sys.executable,
|
|
139
|
+
[sys.executable, str(script), str(marker)],
|
|
140
|
+
cwd=tmp_path,
|
|
141
|
+
env=_no_cov_env(),
|
|
114
142
|
)
|
|
115
143
|
# Should succeed after retry, marker must exist, and we should have slept at
|
|
116
144
|
# least once
|
|
@@ -222,7 +250,7 @@ def test_git_last_commit_trailers_parsing_edge_cases(
|
|
|
222
250
|
) -> None:
|
|
223
251
|
# Patch git_show to return a crafted commit message body with various
|
|
224
252
|
# trailer forms.
|
|
225
|
-
|
|
253
|
+
from github2gerrit.gitutils import git_last_commit_trailers
|
|
226
254
|
|
|
227
255
|
body = (
|
|
228
256
|
"Subject line\n\n"
|
|
@@ -235,10 +263,12 @@ def test_git_last_commit_trailers_parsing_edge_cases(
|
|
|
235
263
|
"Change-Id: Iabc123abc123abc123abc123abc123abc123ab\n"
|
|
236
264
|
)
|
|
237
265
|
|
|
238
|
-
monkeypatch.setattr(
|
|
266
|
+
monkeypatch.setattr(
|
|
267
|
+
"github2gerrit.gitutils.git_show", lambda rev, **kw: body
|
|
268
|
+
)
|
|
239
269
|
|
|
240
270
|
# Without filter: expect only proper footer trailers collected.
|
|
241
|
-
trailers_all =
|
|
271
|
+
trailers_all = git_last_commit_trailers()
|
|
242
272
|
# Body "Key:" lines should not be extracted as trailers
|
|
243
273
|
assert "Key" not in trailers_all
|
|
244
274
|
# 'Empty' should be ignored due to empty value
|
|
@@ -254,7 +284,7 @@ def test_git_last_commit_trailers_parsing_edge_cases(
|
|
|
254
284
|
]
|
|
255
285
|
|
|
256
286
|
# With filter keys: only Change-Id should be returned.
|
|
257
|
-
trailers_change =
|
|
287
|
+
trailers_change = git_last_commit_trailers(keys=["Change-Id"])
|
|
258
288
|
assert list(trailers_change.keys()) == ["Change-Id"]
|
|
259
289
|
assert trailers_change["Change-Id"] == [
|
|
260
290
|
"Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
|
|
@@ -262,6 +292,24 @@ def test_git_last_commit_trailers_parsing_edge_cases(
|
|
|
262
292
|
]
|
|
263
293
|
|
|
264
294
|
|
|
295
|
+
def test_git_last_commit_trailers_returns_empty_on_git_error(
|
|
296
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
297
|
+
) -> None:
|
|
298
|
+
"""A failing ``git show`` (e.g. fresh repo) yields an empty mapping."""
|
|
299
|
+
from github2gerrit.gitutils import GitError
|
|
300
|
+
from github2gerrit.gitutils import git_last_commit_trailers
|
|
301
|
+
|
|
302
|
+
def _raise_git_error(rev: str, **_kw: object) -> str:
|
|
303
|
+
raise GitError("git show HEAD failed")
|
|
304
|
+
|
|
305
|
+
monkeypatch.setattr("github2gerrit.gitutils.git_show", _raise_git_error)
|
|
306
|
+
|
|
307
|
+
# The implementation catches GitError and returns an empty dict, both
|
|
308
|
+
# without and with a key filter.
|
|
309
|
+
assert git_last_commit_trailers() == {}
|
|
310
|
+
assert git_last_commit_trailers(keys=["Change-Id"]) == {}
|
|
311
|
+
|
|
312
|
+
|
|
265
313
|
def test_git_quiet_suppresses_failure_logging(
|
|
266
314
|
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
|
|
267
315
|
) -> None:
|
|
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
|
{github2gerrit-1.3.1 → github2gerrit-1.3.3}/docs/github2gerrit_token_permissions_classic.png
RENAMED
|
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
|
{github2gerrit-1.3.1 → github2gerrit-1.3.3}/src/github2gerrit/orchestrator/reconciliation.py
RENAMED
|
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
|
{github2gerrit-1.3.1 → github2gerrit-1.3.3}/tests/test_mapping_comment_digest_and_backref.py
RENAMED
|
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
|