github2gerrit 1.2.4__tar.gz → 1.3.1__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.2.4 → github2gerrit-1.3.1}/PKG-INFO +24 -1
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/README.md +23 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/cli.py +7 -4
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/core.py +2 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/gerrit_pr_closer.py +10 -1
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/gerrit_query.py +139 -22
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/gerrit_rest.py +0 -1
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/ssh_config_parser.py +3 -2
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_config_helpers.py +6 -1
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_core_config_and_errors.py +5 -1
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_core_ssh_setup.py +8 -2
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_dependency_supersession.py +258 -3
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_url_parser.py +5 -1
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/.editorconfig +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/.gitignore +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/.gitlint +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/.markdownlint.yaml +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/.pre-commit-config.yaml +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/.readthedocs.yml +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/.yamllint +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/LICENSE +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/LICENSES/Apache-2.0.txt +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/REUSE.toml +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/SECURITY.md +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/action.yaml +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/docs/COMMIT_RULES.md +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/docs/RELEASE-v0.2.0.md +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/docs/github2gerrit_token_permissions_classic.png +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/pyproject.toml +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/sitecustomize.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/__init__.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/commit_normalization.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/commit_rules.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/config.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/constants.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/duplicate_detection.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/error_codes.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/external_api.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/gerrit_ssh.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/gerrit_urls.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/github_api.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/gitreview.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/gitutils.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/mapping_comment.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/models.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/netrc.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/orchestrator/__init__.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/pr_commands.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/pr_content_filter.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/reconcile_matcher.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/rich_display.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/rich_logging.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/similarity.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/ssh_agent_setup.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/ssh_common.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/ssh_discovery.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/trailers.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/src/github2gerrit/utils.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/conftest.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/fixtures/__init__.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/fixtures/make_repo.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/fixtures/ssh_config_samples.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_action_environment_mapping.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_action_outputs.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_action_pr_number_handling.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_action_step_validation.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_automation_only.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_change_id_deduplication.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_clean_squash_title.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_cli.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_cli_helpers.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_cli_netrc_options.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_cli_outputs_file.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_cli_url_and_dryrun.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_commit_normalization.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_commit_rules.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_composite_action_coverage.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_config_and_reviewers.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_core_close_pr_policy.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_core_gerrit_backref_comment.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_core_gerrit_push_errors.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_core_gerrit_rest_results.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_core_integration_fixture_repo.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_core_prepare_commits.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_core_shallow_clone.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_core_ssrf_protection.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_dns_validation_and_no_gerrit.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_duplicate_detection.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_email_case_normalization.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_error_codes.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_external_api_framework.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_force_flag_cli.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_gerrit_change_id_footer.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_gerrit_change_status_checks.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_gerrit_pr_closer.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_gerrit_rest_client.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_gerrit_ssh_abandon.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_gerrit_urls.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_gerrit_urls_more.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_ghe_and_gitreview_args.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_github_api_error_handling.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_github_api_helpers.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_github_api_retry_and_helpers.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_gitreview.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_gitutils_helpers.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_issue_157_regressions.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_mapping_comment_additional.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_mapping_comment_digest_and_backref.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_metadata_and_reconciliation.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_metadata_trailer_separation_bug.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_misc_small_coverage.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_netrc.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_orphan_rest_side_effects.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_pr_commands.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_pr_content_filter.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_pr_content_filter_integration.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_pr_update_detection.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_reconciliation_extracted_module.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_reconciliation_plan_and_orphans.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_reconciliation_scenarios.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_ssh_agent.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_ssh_agent_ownership.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_ssh_artifact_prevention.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_ssh_common.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_ssh_discovery.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_ssh_discovery_dry_run.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_trailers_additional.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/test_utils.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/unit/test_config_integration.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/tests/unit/test_ssh_config_parser.py +0 -0
- {github2gerrit-1.2.4 → github2gerrit-1.3.1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github2gerrit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
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
|
|
@@ -1430,6 +1430,29 @@ variable.
|
|
|
1430
1430
|
**Note**: Unknown configuration keys will generate warnings to help catch typos
|
|
1431
1431
|
and missing functionality.
|
|
1432
1432
|
|
|
1433
|
+
### Supersession Sweep Without REST Credentials
|
|
1434
|
+
|
|
1435
|
+
The dependency **supersession sweep** (which reuses or abandons older open
|
|
1436
|
+
Gerrit changes when GitHub2Gerrit pushes a newer update for the same
|
|
1437
|
+
dependency) needs to list GitHub2Gerrit's own open changes. With Gerrit REST
|
|
1438
|
+
credentials it does this precisely via the `owner:self` query operator.
|
|
1439
|
+
|
|
1440
|
+
`owner:self` requires an authenticated session, so without Gerrit REST
|
|
1441
|
+
credentials the tool falls back to an **anonymous** query: it lists the
|
|
1442
|
+
project/branch's open GitHub2Gerrit changes (narrowed server-side via the
|
|
1443
|
+
`GitHub-PR` trailer, a public read-only operation) and then keeps only those
|
|
1444
|
+
whose trailer points at the current repository. This scopes the sweep to
|
|
1445
|
+
GitHub2Gerrit changes for this repository without needing a Gerrit HTTP
|
|
1446
|
+
password.
|
|
1447
|
+
|
|
1448
|
+
- Use `G2G_ANON_SUPERSEDE_FALLBACK` (default `true`) to control the fallback.
|
|
1449
|
+
- Set it to `false` to disable the fallback; without credentials the tool then
|
|
1450
|
+
skips the sweep and logs a warning, as before.
|
|
1451
|
+
- The fallback depends on the Gerrit server allowing anonymous change queries.
|
|
1452
|
+
If the server rejects that query, the tool skips the sweep gracefully.
|
|
1453
|
+
- On large projects the query has a result cap; if it reaches that cap the
|
|
1454
|
+
tool warns that the sweep may be incomplete.
|
|
1455
|
+
|
|
1433
1456
|
### Credential Derivation
|
|
1434
1457
|
|
|
1435
1458
|
When `GERRIT_SSH_USER_G2G` and `GERRIT_SSH_USER_G2G_EMAIL` are not explicitly provided,
|
|
@@ -1384,6 +1384,29 @@ variable.
|
|
|
1384
1384
|
**Note**: Unknown configuration keys will generate warnings to help catch typos
|
|
1385
1385
|
and missing functionality.
|
|
1386
1386
|
|
|
1387
|
+
### Supersession Sweep Without REST Credentials
|
|
1388
|
+
|
|
1389
|
+
The dependency **supersession sweep** (which reuses or abandons older open
|
|
1390
|
+
Gerrit changes when GitHub2Gerrit pushes a newer update for the same
|
|
1391
|
+
dependency) needs to list GitHub2Gerrit's own open changes. With Gerrit REST
|
|
1392
|
+
credentials it does this precisely via the `owner:self` query operator.
|
|
1393
|
+
|
|
1394
|
+
`owner:self` requires an authenticated session, so without Gerrit REST
|
|
1395
|
+
credentials the tool falls back to an **anonymous** query: it lists the
|
|
1396
|
+
project/branch's open GitHub2Gerrit changes (narrowed server-side via the
|
|
1397
|
+
`GitHub-PR` trailer, a public read-only operation) and then keeps only those
|
|
1398
|
+
whose trailer points at the current repository. This scopes the sweep to
|
|
1399
|
+
GitHub2Gerrit changes for this repository without needing a Gerrit HTTP
|
|
1400
|
+
password.
|
|
1401
|
+
|
|
1402
|
+
- Use `G2G_ANON_SUPERSEDE_FALLBACK` (default `true`) to control the fallback.
|
|
1403
|
+
- Set it to `false` to disable the fallback; without credentials the tool then
|
|
1404
|
+
skips the sweep and logs a warning, as before.
|
|
1405
|
+
- The fallback depends on the Gerrit server allowing anonymous change queries.
|
|
1406
|
+
If the server rejects that query, the tool skips the sweep gracefully.
|
|
1407
|
+
- On large projects the query has a result cap; if it reaches that cap the
|
|
1408
|
+
tool warns that the sweep may be incomplete.
|
|
1409
|
+
|
|
1387
1410
|
### Credential Derivation
|
|
1388
1411
|
|
|
1389
1412
|
When `GERRIT_SSH_USER_G2G` and `GERRIT_SSH_USER_G2G_EMAIL` are not explicitly provided,
|
|
@@ -504,9 +504,12 @@ def _resolve_org(default_org: str | None) -> str:
|
|
|
504
504
|
if TYPE_CHECKING:
|
|
505
505
|
F = TypeVar("F", bound=Callable[..., object])
|
|
506
506
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
507
|
+
# Typed stub so basedpyright treats @typed_app_command as a typed
|
|
508
|
+
# decorator. The body raises rather than using an ellipsis so static
|
|
509
|
+
# analysers do not model it as a procedure that returns None; this
|
|
510
|
+
# branch is never executed at runtime.
|
|
511
|
+
def typed_app_command(*args: object, **kwargs: object) -> Callable[[F], F]:
|
|
512
|
+
raise NotImplementedError
|
|
510
513
|
else:
|
|
511
514
|
typed_app_command = app.command
|
|
512
515
|
|
|
@@ -865,7 +868,7 @@ def main(
|
|
|
865
868
|
) -> bool:
|
|
866
869
|
"""Return *current* if the CLI flag was explicit, else parse env."""
|
|
867
870
|
source = ctx.get_parameter_source(param_name) # pyright: ignore[reportAttributeAccessIssue]
|
|
868
|
-
if source == click.core.ParameterSource.COMMANDLINE: # pyright: ignore[reportAttributeAccessIssue]
|
|
871
|
+
if source == click.core.ParameterSource.COMMANDLINE: # pyright: ignore[reportAttributeAccessIssue, reportUnnecessaryComparison]
|
|
869
872
|
return current
|
|
870
873
|
env_val = os.getenv(env_var)
|
|
871
874
|
if env_val is not None:
|
|
@@ -969,6 +969,7 @@ class Orchestrator:
|
|
|
969
969
|
gerrit.project,
|
|
970
970
|
branch=gh.base_ref,
|
|
971
971
|
max_results=200,
|
|
972
|
+
github_repository=gh.repository,
|
|
972
973
|
)
|
|
973
974
|
|
|
974
975
|
# Collect all matching changes, then select the
|
|
@@ -2178,6 +2179,7 @@ class Orchestrator:
|
|
|
2178
2179
|
exclude_change_ids=prep.change_ids,
|
|
2179
2180
|
dry_run=False,
|
|
2180
2181
|
target_branch=self._resolve_target_branch(),
|
|
2182
|
+
github_repository=gh.repository,
|
|
2181
2183
|
)
|
|
2182
2184
|
except Exception as exc:
|
|
2183
2185
|
log.debug("Post-push supersession sweep skipped: %s", exc)
|
|
@@ -1780,6 +1780,7 @@ def abandon_superseded_dependency_changes(
|
|
|
1780
1780
|
*,
|
|
1781
1781
|
dry_run: bool = False,
|
|
1782
1782
|
target_branch: str | None = None,
|
|
1783
|
+
github_repository: str | None = None,
|
|
1783
1784
|
) -> list[str]:
|
|
1784
1785
|
"""Abandon open Gerrit changes superseded by a newer dependency update.
|
|
1785
1786
|
|
|
@@ -1801,6 +1802,10 @@ def abandon_superseded_dependency_changes(
|
|
|
1801
1802
|
dry_run: If True, log but do not actually abandon.
|
|
1802
1803
|
target_branch: Optional Gerrit branch name. When provided,
|
|
1803
1804
|
only changes targeting this branch are considered.
|
|
1805
|
+
github_repository: Current GitHub repository in ``owner/repo``
|
|
1806
|
+
form. Enables the anonymous supersession fallback when no
|
|
1807
|
+
Gerrit REST credentials are available (the discovery query is
|
|
1808
|
+
then scoped to this repository via the ``GitHub-PR`` trailer).
|
|
1804
1809
|
|
|
1805
1810
|
Returns:
|
|
1806
1811
|
List of Gerrit change numbers that were abandoned
|
|
@@ -1830,7 +1835,11 @@ def abandon_superseded_dependency_changes(
|
|
|
1830
1835
|
try:
|
|
1831
1836
|
client = build_client_for_host(gerrit_server)
|
|
1832
1837
|
open_changes = query_open_changes_by_project(
|
|
1833
|
-
client,
|
|
1838
|
+
client,
|
|
1839
|
+
gerrit_project,
|
|
1840
|
+
branch=target_branch,
|
|
1841
|
+
max_results=200,
|
|
1842
|
+
github_repository=github_repository,
|
|
1834
1843
|
)
|
|
1835
1844
|
|
|
1836
1845
|
url_builder = create_gerrit_url_builder(gerrit_server)
|
|
@@ -11,9 +11,14 @@ import logging
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from typing import Any
|
|
13
13
|
from urllib.parse import quote
|
|
14
|
+
from urllib.parse import urlparse
|
|
14
15
|
|
|
15
16
|
from .gerrit_rest import GerritRestClient
|
|
16
17
|
from .gerrit_rest import warn_gerrit_credentials_unavailable
|
|
18
|
+
from .trailers import GITHUB_PR_TRAILER
|
|
19
|
+
from .trailers import parse_trailers
|
|
20
|
+
from .utils import env_bool
|
|
21
|
+
from .utils import log_warning_once
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
log = logging.getLogger(__name__)
|
|
@@ -121,19 +126,71 @@ def query_changes_by_topic(
|
|
|
121
126
|
return changes
|
|
122
127
|
|
|
123
128
|
|
|
129
|
+
def _change_belongs_to_repository(
|
|
130
|
+
change: GerritChange, github_repository: str
|
|
131
|
+
) -> bool:
|
|
132
|
+
"""Return True if a change's GitHub-PR trailer targets the given repo.
|
|
133
|
+
|
|
134
|
+
The ``GitHub-PR`` trailer is a pull request URL of the form
|
|
135
|
+
``https://github.com/{owner}/{repo}/pull/{n}``. Matching on it scopes
|
|
136
|
+
anonymous supersession queries to changes created by GitHub2Gerrit for
|
|
137
|
+
the current repository -- a precise replacement for ``owner:self`` that
|
|
138
|
+
does not require an authenticated session.
|
|
139
|
+
|
|
140
|
+
The trailer is parsed as a URL and matched on its **path** prefix
|
|
141
|
+
(``/{owner}/{repo}/pull/``) rather than a bare substring. The value must
|
|
142
|
+
be an absolute ``http(s)`` URL (scheme + host), so malformed or
|
|
143
|
+
relative values (e.g. ``/org/repo/pull/1``) and unrelated text that
|
|
144
|
+
merely contains the substring (for example inside a query string) do
|
|
145
|
+
not produce false positives. The host itself is not compared, so
|
|
146
|
+
GitHub Enterprise URLs still match.
|
|
147
|
+
"""
|
|
148
|
+
target = github_repository.strip().strip("/").lower()
|
|
149
|
+
if not target:
|
|
150
|
+
return False
|
|
151
|
+
prefix = f"/{target}/pull/"
|
|
152
|
+
trailers = parse_trailers(change.commit_message or "")
|
|
153
|
+
for value in trailers.get(GITHUB_PR_TRAILER, []):
|
|
154
|
+
if not value:
|
|
155
|
+
continue
|
|
156
|
+
try:
|
|
157
|
+
parsed = urlparse(value.strip())
|
|
158
|
+
except ValueError:
|
|
159
|
+
continue
|
|
160
|
+
# Require an absolute http(s) URL; reject relative/malformed values.
|
|
161
|
+
if parsed.scheme not in ("http", "https") or not parsed.netloc:
|
|
162
|
+
continue
|
|
163
|
+
if parsed.path.lower().startswith(prefix):
|
|
164
|
+
return True
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
|
|
124
168
|
def query_open_changes_by_project(
|
|
125
169
|
client: GerritRestClient,
|
|
126
170
|
project: str,
|
|
127
171
|
*,
|
|
128
172
|
branch: str | None = None,
|
|
129
173
|
max_results: int = 100,
|
|
174
|
+
github_repository: str | None = None,
|
|
130
175
|
) -> list[GerritChange]:
|
|
131
|
-
"""Query open changes
|
|
176
|
+
"""Query open GitHub2Gerrit changes in a Gerrit project.
|
|
132
177
|
|
|
133
|
-
Used by the supersession sweep to discover open changes that
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
178
|
+
Used by the supersession sweep to discover open changes that may be
|
|
179
|
+
superseded by a newer dependency update.
|
|
180
|
+
|
|
181
|
+
When the REST client is authenticated, the query is restricted to
|
|
182
|
+
changes owned by the authenticated user via the ``owner:self``
|
|
183
|
+
predicate (the original, most precise behaviour).
|
|
184
|
+
|
|
185
|
+
When the client is unauthenticated, ``owner:self`` cannot be used (the
|
|
186
|
+
server rejects it with HTTP 403). If the anonymous fallback is enabled
|
|
187
|
+
(``G2G_ANON_SUPERSEDE_FALLBACK``, default on) and ``github_repository``
|
|
188
|
+
is provided, the query instead lists all open changes in the
|
|
189
|
+
project/branch and keeps only those whose ``GitHub-PR`` trailer targets
|
|
190
|
+
``github_repository``. This scopes results to GitHub2Gerrit changes for
|
|
191
|
+
the current repository without requiring credentials. If the fallback
|
|
192
|
+
is disabled or no repository is available, the sweep is skipped (with a
|
|
193
|
+
warning) as before.
|
|
137
194
|
|
|
138
195
|
Args:
|
|
139
196
|
client: Gerrit REST client.
|
|
@@ -142,26 +199,55 @@ def query_open_changes_by_project(
|
|
|
142
199
|
When provided, only changes targeting this branch are
|
|
143
200
|
returned.
|
|
144
201
|
max_results: Maximum number of results to return.
|
|
202
|
+
github_repository: Current GitHub repository in ``owner/repo``
|
|
203
|
+
form. Required to enable the anonymous fallback.
|
|
145
204
|
|
|
146
205
|
Returns:
|
|
147
206
|
List of open ``GerritChange`` objects.
|
|
148
207
|
"""
|
|
149
|
-
|
|
208
|
+
base_query = f'project:"{_gerrit_quote(project)}" status:open'
|
|
150
209
|
if branch:
|
|
151
|
-
|
|
210
|
+
base_query += f' branch:"{_gerrit_quote(branch)}"'
|
|
152
211
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
212
|
+
repo_filter: str | None = None
|
|
213
|
+
if client.is_authenticated:
|
|
214
|
+
query = f"{base_query} owner:self"
|
|
215
|
+
else:
|
|
216
|
+
# owner:self requires an authenticated session. Fall back to an
|
|
217
|
+
# anonymous project/branch query scoped to the current repository
|
|
218
|
+
# via the GitHub-PR trailer, when permitted and possible.
|
|
219
|
+
normalized_repo = (github_repository or "").strip().strip("/")
|
|
220
|
+
fallback_enabled = env_bool("G2G_ANON_SUPERSEDE_FALLBACK", True)
|
|
221
|
+
if not fallback_enabled or not normalized_repo:
|
|
222
|
+
warn_gerrit_credentials_unavailable()
|
|
223
|
+
log.debug(
|
|
224
|
+
"Skipping owner:self query for project '%s': no Gerrit "
|
|
225
|
+
"REST credentials and anonymous fallback unavailable "
|
|
226
|
+
"(enabled=%s, repository=%s)",
|
|
227
|
+
project,
|
|
228
|
+
fallback_enabled,
|
|
229
|
+
normalized_repo,
|
|
230
|
+
)
|
|
231
|
+
return []
|
|
232
|
+
# owner:self is unavailable without authentication. Narrow the
|
|
233
|
+
# anonymous query server-side to GitHub2Gerrit changes (which all
|
|
234
|
+
# carry the GitHub-PR trailer in their commit message) so the
|
|
235
|
+
# result set stays small even on busy projects; the precise
|
|
236
|
+
# repository scoping is then applied client-side below.
|
|
237
|
+
query = f'{base_query} message:"{GITHUB_PR_TRAILER}"'
|
|
238
|
+
repo_filter = normalized_repo
|
|
239
|
+
# Surface the missing-credentials condition once at default level
|
|
240
|
+
# via the shared warn-once helper (its message already notes that
|
|
241
|
+
# fallback behavior may apply); keep the per-call detail at debug,
|
|
242
|
+
# since this can run multiple times per invocation (e.g. Strategy 5
|
|
243
|
+
# and the post-push sweep).
|
|
158
244
|
warn_gerrit_credentials_unavailable()
|
|
159
245
|
log.debug(
|
|
160
|
-
"
|
|
161
|
-
"
|
|
246
|
+
"No Gerrit REST credentials; using anonymous supersession "
|
|
247
|
+
"fallback for project '%s' scoped to repository '%s'",
|
|
162
248
|
project,
|
|
249
|
+
normalized_repo,
|
|
163
250
|
)
|
|
164
|
-
return []
|
|
165
251
|
|
|
166
252
|
log.debug("Querying Gerrit for open changes: %s", query)
|
|
167
253
|
|
|
@@ -169,11 +255,6 @@ def query_open_changes_by_project(
|
|
|
169
255
|
changes = _execute_query_with_pagination(
|
|
170
256
|
client, query, max_results=max_results
|
|
171
257
|
)
|
|
172
|
-
log.debug(
|
|
173
|
-
"Found %d open changes in project '%s'",
|
|
174
|
-
len(changes),
|
|
175
|
-
project,
|
|
176
|
-
)
|
|
177
258
|
except Exception as exc:
|
|
178
259
|
log.warning(
|
|
179
260
|
"Failed to query open Gerrit changes for project '%s': %s",
|
|
@@ -181,8 +262,44 @@ def query_open_changes_by_project(
|
|
|
181
262
|
exc,
|
|
182
263
|
)
|
|
183
264
|
return []
|
|
184
|
-
|
|
185
|
-
|
|
265
|
+
|
|
266
|
+
if repo_filter is not None:
|
|
267
|
+
# The cap may simply have been met exactly, but it can also mean
|
|
268
|
+
# the result set was truncated; surface the possibility once rather
|
|
269
|
+
# than silently missing changes (which could leave a duplicate
|
|
270
|
+
# un-reused/un-abandoned).
|
|
271
|
+
if len(changes) >= max_results:
|
|
272
|
+
log_warning_once(
|
|
273
|
+
log,
|
|
274
|
+
"gerrit_anon_supersede_truncated",
|
|
275
|
+
"Anonymous supersession query for project '%s' returned the "
|
|
276
|
+
"maximum of %d result(s); results may be truncated and some "
|
|
277
|
+
"GitHub2Gerrit changes may not have been examined. Provide "
|
|
278
|
+
"Gerrit REST credentials for a precise owner-scoped query.",
|
|
279
|
+
project,
|
|
280
|
+
max_results,
|
|
281
|
+
)
|
|
282
|
+
scoped = [
|
|
283
|
+
change
|
|
284
|
+
for change in changes
|
|
285
|
+
if _change_belongs_to_repository(change, repo_filter)
|
|
286
|
+
]
|
|
287
|
+
log.debug(
|
|
288
|
+
"Anonymous fallback: %d of %d open change(s) in project "
|
|
289
|
+
"'%s' belong to repository '%s'",
|
|
290
|
+
len(scoped),
|
|
291
|
+
len(changes),
|
|
292
|
+
project,
|
|
293
|
+
repo_filter,
|
|
294
|
+
)
|
|
295
|
+
return scoped
|
|
296
|
+
|
|
297
|
+
log.debug(
|
|
298
|
+
"Found %d open changes in project '%s'",
|
|
299
|
+
len(changes),
|
|
300
|
+
project,
|
|
301
|
+
)
|
|
302
|
+
return changes
|
|
186
303
|
|
|
187
304
|
|
|
188
305
|
def _execute_query_with_pagination(
|
|
@@ -321,7 +321,6 @@ class GerritRestClient:
|
|
|
321
321
|
)
|
|
322
322
|
|
|
323
323
|
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
324
|
-
status = getattr(resp, "status", None)
|
|
325
324
|
content = resp.read()
|
|
326
325
|
# Gerrit prepends ")]}'" in JSON responses to prevent JSON
|
|
327
326
|
# hijacking; strip if present
|
|
@@ -272,8 +272,9 @@ class SSHConfig:
|
|
|
272
272
|
try:
|
|
273
273
|
return bool(re.match(regex_pattern, hostname))
|
|
274
274
|
except re.error:
|
|
275
|
-
#
|
|
276
|
-
|
|
275
|
+
# An invalid regex cannot match. The exact-match case is
|
|
276
|
+
# already handled above, so there is nothing left to match.
|
|
277
|
+
return False
|
|
277
278
|
|
|
278
279
|
|
|
279
280
|
# Global cache for SSH config instances to avoid re-parsing
|
|
@@ -28,7 +28,12 @@ from github2gerrit.config import overlay_missing
|
|
|
28
28
|
if TYPE_CHECKING:
|
|
29
29
|
F = TypeVar("F", bound=Callable[..., object])
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
# Typed stub so basedpyright treats @parametrize as a typed decorator
|
|
32
|
+
# (reportUntypedFunctionDecorator). The body raises rather than using
|
|
33
|
+
# an ellipsis so static analysers do not model it as a procedure that
|
|
34
|
+
# returns None; this code is never executed at runtime.
|
|
35
|
+
def parametrize(*args: object, **kwargs: object) -> Callable[[F], F]:
|
|
36
|
+
raise NotImplementedError
|
|
32
37
|
else:
|
|
33
38
|
from pytest import mark
|
|
34
39
|
|
|
@@ -287,7 +287,11 @@ def test_configure_git_raises_on_git_review_init_failure(
|
|
|
287
287
|
if TYPE_CHECKING:
|
|
288
288
|
F = TypeVar("F", bound=Callable[..., object])
|
|
289
289
|
|
|
290
|
-
|
|
290
|
+
# Typed stub (raising body, never executed) so basedpyright treats
|
|
291
|
+
# @fixture as a typed decorator and static analysers do not model it
|
|
292
|
+
# as a procedure returning None.
|
|
293
|
+
def fixture(*args: object, **kwargs: object) -> Callable[[F], F]:
|
|
294
|
+
raise NotImplementedError
|
|
291
295
|
else:
|
|
292
296
|
fixture = pytest.fixture
|
|
293
297
|
|
|
@@ -52,8 +52,14 @@ def snapshot_dir_state(directory: Path) -> dict[str, str]:
|
|
|
52
52
|
if TYPE_CHECKING:
|
|
53
53
|
F = TypeVar("F", bound=Callable[..., object])
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
# Typed stubs (raising bodies, never executed) so basedpyright treats
|
|
56
|
+
# @fixture/@parametrize as typed decorators and static analysers do not
|
|
57
|
+
# model them as procedures returning None.
|
|
58
|
+
def fixture(*args: object, **kwargs: object) -> Callable[[F], F]:
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
|
|
61
|
+
def parametrize(*args: object, **kwargs: object) -> Callable[[F], F]:
|
|
62
|
+
raise NotImplementedError
|
|
57
63
|
else:
|
|
58
64
|
from pytest import fixture as _fixture
|
|
59
65
|
from pytest import mark as _mark
|
|
@@ -22,6 +22,8 @@ from typing import Any
|
|
|
22
22
|
from unittest.mock import MagicMock
|
|
23
23
|
from unittest.mock import patch
|
|
24
24
|
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
25
27
|
from github2gerrit.core import Orchestrator
|
|
26
28
|
from github2gerrit.gerrit_pr_closer import abandon_superseded_dependency_changes
|
|
27
29
|
from github2gerrit.gerrit_query import GerritChange
|
|
@@ -63,6 +65,35 @@ def _gerrit_change(
|
|
|
63
65
|
)
|
|
64
66
|
|
|
65
67
|
|
|
68
|
+
def _raw_change(
|
|
69
|
+
number: int,
|
|
70
|
+
subject: str,
|
|
71
|
+
pr_url: str | None = None,
|
|
72
|
+
) -> dict[str, Any]:
|
|
73
|
+
"""Build a raw Gerrit REST change dict (as returned by /changes/).
|
|
74
|
+
|
|
75
|
+
When ``pr_url`` is provided, a ``GitHub-PR`` trailer is added to the
|
|
76
|
+
current revision's commit message so the anonymous repo-scoping filter
|
|
77
|
+
can be exercised.
|
|
78
|
+
"""
|
|
79
|
+
message = subject
|
|
80
|
+
if pr_url:
|
|
81
|
+
message = f"{subject}\n\nGitHub-PR: {pr_url}\n"
|
|
82
|
+
return {
|
|
83
|
+
"change_id": f"I{number}",
|
|
84
|
+
"_number": number,
|
|
85
|
+
"subject": subject,
|
|
86
|
+
"status": "NEW",
|
|
87
|
+
"current_revision": "rev1",
|
|
88
|
+
"revisions": {
|
|
89
|
+
"rev1": {
|
|
90
|
+
"files": {},
|
|
91
|
+
"commit": {"message": message},
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
66
97
|
# -------------------------------------------------------------------
|
|
67
98
|
# extract_dependency_package_from_subject — baseline
|
|
68
99
|
# -------------------------------------------------------------------
|
|
@@ -164,11 +195,12 @@ class TestQueryOpenChangesByProject:
|
|
|
164
195
|
assert result == []
|
|
165
196
|
|
|
166
197
|
def test_skips_when_unauthenticated(self) -> None:
|
|
167
|
-
"""
|
|
198
|
+
"""Unauthenticated with no repository: skip (no request issued).
|
|
168
199
|
|
|
169
200
|
Without Gerrit REST credentials the ``owner:self`` query is
|
|
170
|
-
guaranteed to fail with HTTP 403
|
|
171
|
-
|
|
201
|
+
guaranteed to fail with HTTP 403. When no repository is available
|
|
202
|
+
to drive the anonymous fallback, the helper must short-circuit and
|
|
203
|
+
never issue the request, returning an empty list.
|
|
172
204
|
"""
|
|
173
205
|
from github2gerrit.utils import reset_warning_once
|
|
174
206
|
|
|
@@ -182,6 +214,227 @@ class TestQueryOpenChangesByProject:
|
|
|
182
214
|
assert result == []
|
|
183
215
|
fake_client.get.assert_not_called()
|
|
184
216
|
|
|
217
|
+
def test_authenticated_uses_owner_self(self) -> None:
|
|
218
|
+
"""Authenticated client must use the owner:self predicate."""
|
|
219
|
+
fake_client = MagicMock()
|
|
220
|
+
fake_client.is_authenticated = True
|
|
221
|
+
fake_client.get.return_value = []
|
|
222
|
+
|
|
223
|
+
query_open_changes_by_project(fake_client, "org/repo", branch="main")
|
|
224
|
+
|
|
225
|
+
path = fake_client.get.call_args[0][0]
|
|
226
|
+
# owner:self is URL-encoded as owner%3Aself in the query string
|
|
227
|
+
assert "owner%3Aself" in path
|
|
228
|
+
|
|
229
|
+
def test_anonymous_fallback_scopes_to_repository(self) -> None:
|
|
230
|
+
"""Unauthenticated + repo: query anonymously, scope by trailer.
|
|
231
|
+
|
|
232
|
+
The query must NOT use owner:self, and only changes whose
|
|
233
|
+
GitHub-PR trailer targets the current repository are returned.
|
|
234
|
+
"""
|
|
235
|
+
from github2gerrit.utils import reset_warning_once
|
|
236
|
+
|
|
237
|
+
reset_warning_once()
|
|
238
|
+
|
|
239
|
+
ours = _raw_change(
|
|
240
|
+
100,
|
|
241
|
+
"chore: bump requests from 2.31.0 to 2.32.0",
|
|
242
|
+
"https://github.com/org/repo/pull/42",
|
|
243
|
+
)
|
|
244
|
+
other_repo = _raw_change(
|
|
245
|
+
101,
|
|
246
|
+
"chore: bump requests from 2.31.0 to 2.32.0",
|
|
247
|
+
"https://github.com/other/project/pull/7",
|
|
248
|
+
)
|
|
249
|
+
human = _raw_change(102, "Fix: unrelated human change")
|
|
250
|
+
|
|
251
|
+
fake_client = MagicMock()
|
|
252
|
+
fake_client.is_authenticated = False
|
|
253
|
+
fake_client.get.return_value = [ours, other_repo, human]
|
|
254
|
+
|
|
255
|
+
result = query_open_changes_by_project(
|
|
256
|
+
fake_client, "org/repo", github_repository="org/repo"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
path = fake_client.get.call_args[0][0]
|
|
260
|
+
assert "owner%3Aself" not in path
|
|
261
|
+
assert [change.number for change in result] == ["100"]
|
|
262
|
+
|
|
263
|
+
def test_anonymous_fallback_repo_match_is_case_insensitive(self) -> None:
|
|
264
|
+
"""Repo trailer matching ignores case differences."""
|
|
265
|
+
from github2gerrit.utils import reset_warning_once
|
|
266
|
+
|
|
267
|
+
reset_warning_once()
|
|
268
|
+
|
|
269
|
+
ours = _raw_change(
|
|
270
|
+
200,
|
|
271
|
+
"chore: bump requests from 2.31.0 to 2.32.0",
|
|
272
|
+
"https://github.com/Org/Repo/pull/9",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
fake_client = MagicMock()
|
|
276
|
+
fake_client.is_authenticated = False
|
|
277
|
+
fake_client.get.return_value = [ours]
|
|
278
|
+
|
|
279
|
+
result = query_open_changes_by_project(
|
|
280
|
+
fake_client, "org/repo", github_repository="org/repo"
|
|
281
|
+
)
|
|
282
|
+
assert [change.number for change in result] == ["200"]
|
|
283
|
+
|
|
284
|
+
def test_anonymous_fallback_matches_ghes_host(self) -> None:
|
|
285
|
+
"""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
|
+
ours = _raw_change(
|
|
291
|
+
210,
|
|
292
|
+
"chore: bump requests from 2.31.0 to 2.32.0",
|
|
293
|
+
"https://ghe.example.com/org/repo/pull/9",
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
fake_client = MagicMock()
|
|
297
|
+
fake_client.is_authenticated = False
|
|
298
|
+
fake_client.get.return_value = [ours]
|
|
299
|
+
|
|
300
|
+
result = query_open_changes_by_project(
|
|
301
|
+
fake_client, "org/repo", github_repository="org/repo"
|
|
302
|
+
)
|
|
303
|
+
assert [change.number for change in result] == ["210"]
|
|
304
|
+
|
|
305
|
+
def test_anonymous_fallback_rejects_substring_in_query(self) -> None:
|
|
306
|
+
"""Non-absolute or substring-only repo references must not match.
|
|
307
|
+
|
|
308
|
+
The matcher requires an absolute http(s) URL and checks its path
|
|
309
|
+
prefix, so a different repo's URL that merely mentions the target
|
|
310
|
+
repo in its query string, a non-URL value, and a relative path
|
|
311
|
+
(e.g. ``/org/repo/pull/1``) are all correctly excluded.
|
|
312
|
+
"""
|
|
313
|
+
from github2gerrit.utils import reset_warning_once
|
|
314
|
+
|
|
315
|
+
reset_warning_once()
|
|
316
|
+
|
|
317
|
+
other = _raw_change(
|
|
318
|
+
220,
|
|
319
|
+
"chore: bump requests from 2.31.0 to 2.32.0",
|
|
320
|
+
"https://github.com/other/project/pull/7?ref=/org/repo/pull/1",
|
|
321
|
+
)
|
|
322
|
+
malformed = _raw_change(
|
|
323
|
+
221,
|
|
324
|
+
"chore: bump requests from 2.31.0 to 2.32.0",
|
|
325
|
+
"not-a-url-/org/repo/pull/1",
|
|
326
|
+
)
|
|
327
|
+
relative = _raw_change(
|
|
328
|
+
222,
|
|
329
|
+
"chore: bump requests from 2.31.0 to 2.32.0",
|
|
330
|
+
"/org/repo/pull/1",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
fake_client = MagicMock()
|
|
334
|
+
fake_client.is_authenticated = False
|
|
335
|
+
fake_client.get.return_value = [other, malformed, relative]
|
|
336
|
+
|
|
337
|
+
result = query_open_changes_by_project(
|
|
338
|
+
fake_client, "org/repo", github_repository="org/repo"
|
|
339
|
+
)
|
|
340
|
+
assert result == []
|
|
341
|
+
|
|
342
|
+
def test_anonymous_fallback_narrows_query_by_message(self) -> None:
|
|
343
|
+
"""The anonymous query narrows server-side via message:GitHub-PR.
|
|
344
|
+
|
|
345
|
+
Without owner:self, the result set is reduced server-side to
|
|
346
|
+
GitHub2Gerrit changes (which carry the GitHub-PR trailer) so busy
|
|
347
|
+
projects do not truncate before the relevant changes are seen.
|
|
348
|
+
"""
|
|
349
|
+
from github2gerrit.utils import reset_warning_once
|
|
350
|
+
|
|
351
|
+
reset_warning_once()
|
|
352
|
+
|
|
353
|
+
fake_client = MagicMock()
|
|
354
|
+
fake_client.is_authenticated = False
|
|
355
|
+
fake_client.get.return_value = []
|
|
356
|
+
|
|
357
|
+
query_open_changes_by_project(
|
|
358
|
+
fake_client, "org/repo", github_repository="org/repo"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
path = fake_client.get.call_args[0][0]
|
|
362
|
+
assert "owner%3Aself" not in path
|
|
363
|
+
# message:"GitHub-PR" is URL-encoded in the query string
|
|
364
|
+
assert "message%3A%22GitHub-PR%22" in path
|
|
365
|
+
|
|
366
|
+
def test_anonymous_fallback_warns_on_truncation(
|
|
367
|
+
self, caplog: pytest.LogCaptureFixture
|
|
368
|
+
) -> None:
|
|
369
|
+
"""Hitting the result cap emits a one-time truncation warning."""
|
|
370
|
+
import logging
|
|
371
|
+
|
|
372
|
+
from github2gerrit.utils import reset_warning_once
|
|
373
|
+
|
|
374
|
+
reset_warning_once()
|
|
375
|
+
|
|
376
|
+
c1 = _raw_change(
|
|
377
|
+
300,
|
|
378
|
+
"chore: bump x from 1 to 2",
|
|
379
|
+
"https://github.com/org/repo/pull/1",
|
|
380
|
+
)
|
|
381
|
+
c2 = _raw_change(
|
|
382
|
+
301,
|
|
383
|
+
"chore: bump y from 1 to 2",
|
|
384
|
+
"https://github.com/org/repo/pull/2",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
fake_client = MagicMock()
|
|
388
|
+
fake_client.is_authenticated = False
|
|
389
|
+
fake_client.get.return_value = [c1, c2]
|
|
390
|
+
|
|
391
|
+
with caplog.at_level(logging.WARNING):
|
|
392
|
+
result = query_open_changes_by_project(
|
|
393
|
+
fake_client,
|
|
394
|
+
"org/repo",
|
|
395
|
+
github_repository="org/repo",
|
|
396
|
+
max_results=2,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
assert {change.number for change in result} == {"300", "301"}
|
|
400
|
+
assert any(
|
|
401
|
+
"results may be truncated" in rec.getMessage()
|
|
402
|
+
for rec in caplog.records
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def test_anonymous_fallback_disabled_returns_empty(
|
|
406
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Disabling the fallback flag skips even when repo is known."""
|
|
409
|
+
from github2gerrit.utils import reset_warning_once
|
|
410
|
+
|
|
411
|
+
reset_warning_once()
|
|
412
|
+
monkeypatch.setenv("G2G_ANON_SUPERSEDE_FALLBACK", "false")
|
|
413
|
+
|
|
414
|
+
fake_client = MagicMock()
|
|
415
|
+
fake_client.is_authenticated = False
|
|
416
|
+
|
|
417
|
+
result = query_open_changes_by_project(
|
|
418
|
+
fake_client, "org/repo", github_repository="org/repo"
|
|
419
|
+
)
|
|
420
|
+
assert result == []
|
|
421
|
+
fake_client.get.assert_not_called()
|
|
422
|
+
|
|
423
|
+
def test_anonymous_fallback_whitespace_repo_skips(self) -> None:
|
|
424
|
+
"""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
|
+
fake_client = MagicMock()
|
|
430
|
+
fake_client.is_authenticated = False
|
|
431
|
+
|
|
432
|
+
result = query_open_changes_by_project(
|
|
433
|
+
fake_client, "org/repo", github_repository=" "
|
|
434
|
+
)
|
|
435
|
+
assert result == []
|
|
436
|
+
fake_client.get.assert_not_called()
|
|
437
|
+
|
|
185
438
|
|
|
186
439
|
# -------------------------------------------------------------------
|
|
187
440
|
# abandon_superseded_dependency_changes
|
|
@@ -467,6 +720,7 @@ class TestAbandonSupersededDependencyChanges:
|
|
|
467
720
|
current_subject="chore: bump requests from 2.31.0 to 2.32.0",
|
|
468
721
|
exclude_change_ids=[],
|
|
469
722
|
target_branch="main",
|
|
723
|
+
github_repository="org/repo-gh",
|
|
470
724
|
)
|
|
471
725
|
|
|
472
726
|
mock_query.assert_called_once_with(
|
|
@@ -474,6 +728,7 @@ class TestAbandonSupersededDependencyChanges:
|
|
|
474
728
|
"org/repo",
|
|
475
729
|
branch="main",
|
|
476
730
|
max_results=200,
|
|
731
|
+
github_repository="org/repo-gh",
|
|
477
732
|
)
|
|
478
733
|
|
|
479
734
|
|
|
@@ -11,7 +11,11 @@ from typing import TypeVar
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
F = TypeVar("F", bound=Callable[..., object])
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# Typed stub (raising body, never executed) so basedpyright treats
|
|
15
|
+
# @parametrize as a typed decorator and static analysers do not model
|
|
16
|
+
# it as a procedure returning None.
|
|
17
|
+
def parametrize(*args: object, **kwargs: object) -> Callable[[F], F]:
|
|
18
|
+
raise NotImplementedError
|
|
15
19
|
else:
|
|
16
20
|
from pytest import mark
|
|
17
21
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.2.4 → github2gerrit-1.3.1}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github2gerrit-1.2.4 → github2gerrit-1.3.1}/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
|
{github2gerrit-1.2.4 → github2gerrit-1.3.1}/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
|