github2gerrit 2.4.1__tar.gz → 2.4.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.
Files changed (155) hide show
  1. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/.pre-commit-config.yaml +5 -5
  2. github2gerrit-2.4.3/AGENTS.md +43 -0
  3. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/PKG-INFO +1 -1
  4. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/action.yaml +1 -1
  5. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/gitreview.py +69 -17
  6. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_gitreview.py +205 -0
  7. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/uv.lock +240 -176
  8. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/.aislop/config.yml +0 -0
  9. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/.editorconfig +0 -0
  10. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/.gitignore +0 -0
  11. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/.gitlint +0 -0
  12. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/.markdownlint.yaml +0 -0
  13. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/.yamllint +0 -0
  14. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/LICENSE +0 -0
  15. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/LICENSES/Apache-2.0.txt +0 -0
  16. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/README.md +0 -0
  17. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/REUSE.toml +0 -0
  18. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/SECURITY.md +0 -0
  19. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/docs/COMMIT_RULES.md +0 -0
  20. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/docs/cli.md +0 -0
  21. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/docs/development.md +0 -0
  22. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/docs/features.md +0 -0
  23. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/docs/github2gerrit_token_permissions_classic.png +0 -0
  24. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/pyproject.toml +0 -0
  25. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/sitecustomize.py +0 -0
  26. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/__init__.py +0 -0
  27. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/approvers.py +0 -0
  28. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/cli.py +0 -0
  29. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/commit_normalization.py +0 -0
  30. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/commit_rules.py +0 -0
  31. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/config.py +0 -0
  32. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/config_ini.py +0 -0
  33. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/constants.py +0 -0
  34. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/core.py +0 -0
  35. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/duplicate_detection.py +0 -0
  36. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/error_codes.py +0 -0
  37. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/external_api.py +0 -0
  38. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/gerrit_pr_closer.py +0 -0
  39. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/gerrit_query.py +0 -0
  40. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/gerrit_rest.py +0 -0
  41. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/gerrit_ssh.py +0 -0
  42. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/gerrit_urls.py +0 -0
  43. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/github_api.py +0 -0
  44. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/gitutils.py +0 -0
  45. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/mapping_comment.py +0 -0
  46. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/models.py +0 -0
  47. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/netrc.py +0 -0
  48. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/orchestrator/__init__.py +0 -0
  49. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
  50. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/pr_approval.py +0 -0
  51. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/pr_commands.py +0 -0
  52. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/pr_content_filter.py +0 -0
  53. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/pr_directives.py +0 -0
  54. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/project_names.py +0 -0
  55. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/reconcile_matcher.py +0 -0
  56. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/rich_display.py +0 -0
  57. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/rich_logging.py +0 -0
  58. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/similarity.py +0 -0
  59. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/ssh_agent_setup.py +0 -0
  60. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/ssh_common.py +0 -0
  61. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/ssh_config_parser.py +0 -0
  62. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/ssh_discovery.py +0 -0
  63. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/trailers.py +0 -0
  64. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/trust.py +0 -0
  65. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/src/github2gerrit/utils.py +0 -0
  66. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/conftest.py +0 -0
  67. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/fixtures/__init__.py +0 -0
  68. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/fixtures/make_repo.py +0 -0
  69. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/fixtures/ssh_config_samples.py +0 -0
  70. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_action_environment_mapping.py +0 -0
  71. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_action_outputs.py +0 -0
  72. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_action_pr_number_handling.py +0 -0
  73. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_action_step_validation.py +0 -0
  74. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_approver_sources.py +0 -0
  75. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_automation_only.py +0 -0
  76. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_bulk_fan_out.py +0 -0
  77. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_cache_isolation.py +0 -0
  78. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_change_id_deduplication.py +0 -0
  79. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_clean_squash_title.py +0 -0
  80. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_cli.py +0 -0
  81. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_cli_helpers.py +0 -0
  82. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_cli_netrc_options.py +0 -0
  83. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_cli_outputs_file.py +0 -0
  84. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_cli_url_and_dryrun.py +0 -0
  85. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_commit_normalization.py +0 -0
  86. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_commit_rules.py +0 -0
  87. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_composite_action_coverage.py +0 -0
  88. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_config_and_reviewers.py +0 -0
  89. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_config_helpers.py +0 -0
  90. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_core_close_pr_policy.py +0 -0
  91. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_core_config_and_errors.py +0 -0
  92. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_core_gerrit_backref_comment.py +0 -0
  93. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_core_gerrit_push_errors.py +0 -0
  94. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_core_gerrit_rest_results.py +0 -0
  95. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_core_integration_fixture_repo.py +0 -0
  96. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_core_prepare_commits.py +0 -0
  97. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_core_shallow_clone.py +0 -0
  98. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_core_ssh_setup.py +0 -0
  99. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_core_ssrf_protection.py +0 -0
  100. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_dependency_supersession.py +0 -0
  101. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_dns_validation_and_no_gerrit.py +0 -0
  102. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_duplicate_detection.py +0 -0
  103. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_email_case_normalization.py +0 -0
  104. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_error_codes.py +0 -0
  105. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_external_api_framework.py +0 -0
  106. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_final_state_reconciliation.py +0 -0
  107. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_force_flag_cli.py +0 -0
  108. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_fork_approval_gate.py +0 -0
  109. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_fork_pr_hardening.py +0 -0
  110. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_fork_transfer_record.py +0 -0
  111. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_gerrit_change_id_footer.py +0 -0
  112. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_gerrit_change_status_checks.py +0 -0
  113. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_gerrit_pr_closer.py +0 -0
  114. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_gerrit_rest_client.py +0 -0
  115. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_gerrit_ssh_abandon.py +0 -0
  116. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_gerrit_urls.py +0 -0
  117. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_gerrit_urls_more.py +0 -0
  118. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_ghe_and_gitreview_args.py +0 -0
  119. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_github_api_error_handling.py +0 -0
  120. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_github_api_helpers.py +0 -0
  121. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_github_api_retry_and_helpers.py +0 -0
  122. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_gitutils_helpers.py +0 -0
  123. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_issue_157_regressions.py +0 -0
  124. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_mapping_comment_additional.py +0 -0
  125. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  126. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_metadata_and_reconciliation.py +0 -0
  127. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_metadata_trailer_separation_bug.py +0 -0
  128. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_misc_small_coverage.py +0 -0
  129. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_netrc.py +0 -0
  130. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_orphan_rest_side_effects.py +0 -0
  131. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_pr_closed_handling.py +0 -0
  132. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_pr_command_authorisation.py +0 -0
  133. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_pr_commands.py +0 -0
  134. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_pr_content_filter.py +0 -0
  135. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_pr_content_filter_integration.py +0 -0
  136. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_pr_update_detection.py +0 -0
  137. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_precommit_mypy_pins.py +0 -0
  138. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_project_names.py +0 -0
  139. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_reconciliation_extracted_module.py +0 -0
  140. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_reconciliation_plan_and_orphans.py +0 -0
  141. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_reconciliation_scenarios.py +0 -0
  142. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_scheduled_sweep.py +0 -0
  143. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_ssh_agent.py +0 -0
  144. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_ssh_agent_ownership.py +0 -0
  145. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_ssh_artifact_prevention.py +0 -0
  146. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_ssh_common.py +0 -0
  147. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_ssh_discovery.py +0 -0
  148. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_ssh_discovery_dry_run.py +0 -0
  149. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_sync_mypy_pins.py +0 -0
  150. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_trailers_additional.py +0 -0
  151. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_url_parser.py +0 -0
  152. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/test_utils.py +0 -0
  153. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/unit/test_config_integration.py +0 -0
  154. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/unit/test_ssh_config_parser.py +0 -0
  155. {github2gerrit-2.4.1 → github2gerrit-2.4.3}/tests/workflow_harness.py +0 -0
@@ -86,7 +86,7 @@ repos:
86
86
  types: [yaml]
87
87
 
88
88
  - repo: https://github.com/astral-sh/ruff-pre-commit
89
- rev: a56c0b927e6465d37cae3e97d35d4d18ab2b96cd # frozen: v0.16.9
89
+ rev: f12be1ebaa5351c1fc76472de98db2c3446c8253 # frozen: v0.16.10
90
90
  hooks:
91
91
  - id: ruff
92
92
  files: ^(src|scripts|tests)/.+\.py$|^sitecustomize\.py$
@@ -104,12 +104,12 @@ repos:
104
104
  pass_filenames: false
105
105
 
106
106
  - repo: https://github.com/pre-commit/mirrors-mypy
107
- rev: 7ff8d35ae36a7d2b968f2f90b4c723e292e594ee # frozen: v2.3.1
107
+ rev: 2834ec6639549dd6796205c8f011dedcd587288b # frozen: v2.4.0
108
108
  hooks:
109
109
  - id: mypy
110
110
  files: ^(src|scripts|tests)/.+\.py$|^sitecustomize\.py$
111
111
  additional_dependencies:
112
- - mypy==2.3.1
112
+ - mypy==2.4.0
113
113
  - click==8.5.0
114
114
  - pytest==9.1.1
115
115
  - responses==0.26.3
@@ -153,13 +153,13 @@ repos:
153
153
  - id: codespell
154
154
 
155
155
  - repo: https://github.com/DetachHead/basedpyright-prek-mirror
156
- rev: 706e6acebe9e88c2c28d1527d3085a14b11794c4 # frozen: 1.40.1
156
+ rev: a11caebebdbf797e1e96611123db279b334a7675 # frozen: 1.40.2
157
157
  hooks:
158
158
  - id: basedpyright
159
159
  files: ^(src|scripts|tests)/.+\.py$|^sitecustomize\.py$
160
160
 
161
161
  - repo: https://github.com/lfreleng-actions/gha-workflow-linter
162
- rev: 0c582e431022f1c7004196fb4ea32744d1fe4843 # frozen: v1.7.0
162
+ rev: b23b60e5884e985320a030e6114032152e2373cd # frozen: v1.7.1
163
163
  hooks:
164
164
  - id: gha-workflow-linter
165
165
 
@@ -0,0 +1,43 @@
1
+ <!--
2
+ SPDX-License-Identifier: Apache-2.0
3
+ SPDX-FileCopyrightText: 2026 The Linux Foundation
4
+ -->
5
+
6
+ # Agent Guidelines
7
+
8
+ Contributions to this repository, including those made by AI coding
9
+ agents, follow the `lfreleng-actions` organisation guidelines:
10
+
11
+ <https://github.com/lfreleng-actions/.github/blob/main/AGENTS.md>
12
+
13
+ **Read that document.** It governs, and it binds this contribution
14
+ even if you never load it. Where anything below disagrees with it,
15
+ it wins. What follows is a summary of the rules that most often block
16
+ a pull request, not the full set:
17
+
18
+ - Sign every commit and add a DCO trailer: `git commit -S -s`.
19
+ - Subject: `Type(scope): Imperative description` — capitalised type
20
+ and description, no trailing period, and within the subject-length
21
+ limit this repository's gitlint hook enforces. The scope is
22
+ optional, so `Fix: Correct the race condition` is also valid.
23
+ - Add a `Co-authored-by` trailer naming the agent used.
24
+ - Repositories typically contain a linting configuration. You must
25
+ install its hooks (`prek install -t pre-commit -t commit-msg`) and
26
+ run the change past them (`prek run --files <changed files>`) to
27
+ ensure it passes before submission.
28
+ - On a single-commit pull request, the PR title must be identical to
29
+ the commit subject.
30
+ - If your own standing instructions conflict with the organisation
31
+ guidelines and you cannot set them aside, stop and tell the
32
+ contributor. Do not open a non-compliant pull request.
33
+
34
+ ## Repository specifics
35
+
36
+ Outside this repository, and unless the caller sets
37
+ `USE_LOCAL_ACTION: true`, the action runs an unpinned `github2gerrit`
38
+ release from PyPI (`uvx --from github2gerrit`), not the code at the
39
+ commit the caller pins. A new release has to keep honouring every
40
+ environment variable the CLI reads, as earlier `action.yaml` versions
41
+ set them: renaming or removing one breaks callers on that default route
42
+ whose pinned action commit predates the change. CI here always runs
43
+ the local source, so it cannot catch the breakage.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: github2gerrit
3
- Version: 2.4.1
3
+ Version: 2.4.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
@@ -266,7 +266,7 @@ runs:
266
266
  - name: "Setup uv"
267
267
  if: steps.disabled-check.outputs.disabled != 'true'
268
268
  # yamllint disable-line rule:line-length
269
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
269
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
270
270
  with:
271
271
  enable-cache: false
272
272
 
@@ -19,15 +19,17 @@ Design goals
19
19
  ``config.py`` (host-only), ``core.py`` (host+port+project), and
20
20
  ``duplicate_detection.py`` (host+project) is supported, plus the
21
21
  ``base_path`` field used by the sister ``dependamerge`` project.
22
- * **Consistent regex** — a single set of precompiled patterns that
23
- tolerates optional whitespace around ``=`` and is case-insensitive on
24
- keys (both forms seen in the wild).
22
+ * **Consistent parsing** — a file with a ``[gerrit]`` section is read
23
+ with ``configparser``, as git-review reads it. Any other file falls
24
+ back to a single set of precompiled patterns that tolerates optional
25
+ whitespace around ``=`` and is case-insensitive on keys.
25
26
  * **Bug-free fetching** — URL-encodes branch names, deduplicates the
26
27
  branch list, and validates raw URLs before opening them.
27
28
  """
28
29
 
29
30
  from __future__ import annotations
30
31
 
32
+ import configparser
31
33
  import logging
32
34
  import os
33
35
  import re
@@ -45,9 +47,54 @@ log = logging.getLogger(__name__)
45
47
  DEFAULT_GERRIT_PORT: int = 29418
46
48
  """Default Gerrit SSH port when the ``port=`` line is absent."""
47
49
 
50
+ # Fallback scan for files configparser cannot read (see
51
+ # _read_gerrit_section): keys at the start of a line, anywhere in the file.
48
52
  _HOST_RE = re.compile(r"(?mi)^host[ \t]*=[ \t]*(.+?)[ \t\r]*$")
49
53
  _PORT_RE = re.compile(r"(?mi)^port[ \t]*=[ \t]*(\d+)[ \t\r]*$")
50
54
  _PROJECT_RE = re.compile(r"(?mi)^project[ \t]*=[ \t]*(.+?)[ \t\r]*$")
55
+ _PORT_VALUE_RE = re.compile(r"\d+")
56
+
57
+ _Fields = tuple[str | None, str | None, str | None]
58
+ """Raw ``host``, ``port`` and ``project`` values; ``None`` when absent."""
59
+
60
+
61
+ def _read_gerrit_section(text: str) -> _Fields | None:
62
+ """Read the fields as git-review does, through ``configparser``.
63
+
64
+ That settles indentation, continuation lines, other sections and
65
+ ``[DEFAULT]`` inheritance (an empty ``[gerrit]`` value included)
66
+ exactly as git-review sees them. Returns ``None`` when the text has
67
+ no ``[gerrit]`` section or ``configparser`` rejects it; git-review
68
+ cannot read such a file either, so the caller falls back to the
69
+ lenient scan.
70
+ """
71
+ parser = configparser.ConfigParser(interpolation=None)
72
+ try:
73
+ parser.read_string(text)
74
+ except configparser.Error as exc:
75
+ log.debug(".gitreview: not readable as INI (%s); scanning lines", exc)
76
+ return None
77
+ if not parser.has_section("gerrit"):
78
+ return None
79
+ section = parser["gerrit"]
80
+
81
+ def first_line(key: str) -> str | None:
82
+ # A continuation makes a multi-line value; a field is one line.
83
+ value = section.get(key)
84
+ return None if value is None else value.strip().split("\n", 1)[0]
85
+
86
+ return first_line("host"), first_line("port"), first_line("project")
87
+
88
+
89
+ def _scan_fields(text: str) -> _Fields:
90
+ """Find the fields by line pattern anywhere in *text*."""
91
+ matches = (
92
+ _HOST_RE.search(text),
93
+ _PORT_RE.search(text),
94
+ _PROJECT_RE.search(text),
95
+ )
96
+ host, port, project = (m.group(1) if m else None for m in matches)
97
+ return host, port, project
51
98
 
52
99
 
53
100
  @dataclass(frozen=True)
@@ -144,10 +191,14 @@ def parse_gitreview(text: str) -> GitReviewInfo | None:
144
191
 
145
192
  This parser is intentionally lenient:
146
193
 
147
- * Keys are matched case-insensitively.
148
- * Optional whitespace around ``=`` is tolerated.
149
- * The ``[gerrit]`` section header itself is **not** required —
150
- the parser matches the key lines directly.
194
+ * A file with a ``[gerrit]`` section is read with ``configparser``,
195
+ as git-review reads it: keys are case-insensitive and may be
196
+ indented, continuation lines and other sections are ignored, and
197
+ ``[DEFAULT]`` supplies any field ``[gerrit]`` does not set.
198
+ * The ``[gerrit]`` header itself is **not** required. A file without
199
+ one, or one ``configparser`` rejects, is scanned for ``host=``,
200
+ ``port=`` and ``project=`` lines anywhere, with optional
201
+ whitespace around ``=``.
151
202
 
152
203
  Args:
153
204
  text: Raw text content of a ``.gitreview`` file.
@@ -156,23 +207,24 @@ def parse_gitreview(text: str) -> GitReviewInfo | None:
156
207
  A :class:`GitReviewInfo` if at least ``host`` is present and
157
208
  non-empty, otherwise ``None``.
158
209
  """
159
- host_match = _HOST_RE.search(text)
160
- if not host_match:
210
+ fields = _read_gerrit_section(text)
211
+ host, port_text, project_text = (
212
+ fields if fields is not None else _scan_fields(text)
213
+ )
214
+ if host is None:
161
215
  log.debug(".gitreview: no host= line found")
162
216
  return None
163
217
 
164
- host = host_match.group(1).strip()
218
+ host = host.strip()
165
219
  if not host:
166
220
  log.debug(".gitreview: host= line is empty")
167
221
  return None
168
222
 
169
- port_match = _PORT_RE.search(text)
170
- port = int(port_match.group(1)) if port_match else DEFAULT_GERRIT_PORT
223
+ port_text = (port_text or "").strip()
224
+ port_given = _PORT_VALUE_RE.fullmatch(port_text) is not None
225
+ port = int(port_text) if port_given else DEFAULT_GERRIT_PORT
171
226
 
172
- project = ""
173
- project_match = _PROJECT_RE.search(text)
174
- if project_match:
175
- project = project_match.group(1).strip().removesuffix(".git")
227
+ project = (project_text or "").strip().removesuffix(".git")
176
228
 
177
229
  base_path = derive_base_path(host)
178
230
 
@@ -181,7 +233,7 @@ def parse_gitreview(text: str) -> GitReviewInfo | None:
181
233
  port=port,
182
234
  project=project,
183
235
  base_path=base_path,
184
- port_given=port_match is not None,
236
+ port_given=port_given,
185
237
  )
186
238
  log.debug(
187
239
  "Parsed .gitreview: host=%s, port=%d, project=%s, base_path=%s",
@@ -75,6 +75,19 @@ NON_DEFAULT_PORT = (
75
75
  "[gerrit]\nhost=gerrit.acme.org\nport=29419\nproject=acme/widgets.git\n"
76
76
  )
77
77
 
78
+ # The form LF automation wrote into ONAP repositories in 2020, as found in
79
+ # onap/cps: a leading blank line and every line indented by eight spaces.
80
+ # git-review reads it, since configparser ignores leading whitespace.
81
+ INDENTED_GITREVIEW = (
82
+ "\n"
83
+ " [gerrit]\n"
84
+ " host=gerrit.onap.org\n"
85
+ " port=29418\n"
86
+ " project=cps\n"
87
+ " defaultbranch=master\n"
88
+ " "
89
+ )
90
+
78
91
 
79
92
  # -----------------------------------------------------------------------
80
93
  # GitReviewInfo data model
@@ -223,6 +236,15 @@ class TestParseGitreview:
223
236
  assert info.port == 29419
224
237
  assert info.port_given is True
225
238
 
239
+ def test_unicode_decimal_port(self) -> None:
240
+ # int() accepts any Unicode decimal digits, as the \d in the port
241
+ # pattern does; Arabic-Indic "٢٩٤١٩" is 29419.
242
+ text = "[gerrit]\nhost=h.example.org\nport=\u0662\u0669\u0664\u0661\u0669\n"
243
+ info = parse_gitreview(text)
244
+ assert info is not None
245
+ assert info.port == 29419
246
+ assert info.port_given is True
247
+
226
248
  def test_empty_string(self) -> None:
227
249
  assert parse_gitreview("") is None
228
250
 
@@ -253,6 +275,181 @@ class TestParseGitreview:
253
275
  assert info is not None
254
276
  assert info.host == "gerrit.example.org"
255
277
 
278
+ def test_space_indented_keys(self) -> None:
279
+ info = parse_gitreview(INDENTED_GITREVIEW)
280
+ assert info is not None
281
+ assert info.host == "gerrit.onap.org"
282
+ assert info.port == 29418
283
+ assert info.port_given is True
284
+ assert info.project == "cps"
285
+
286
+ def test_tab_indented_keys(self) -> None:
287
+ text = (
288
+ "[gerrit]\n\thost=gerrit.example.org\n\tport=29419\n\tproject=a/b\n"
289
+ )
290
+ info = parse_gitreview(text)
291
+ assert info is not None
292
+ assert info.host == "gerrit.example.org"
293
+ assert info.port == 29419
294
+ assert info.project == "a/b"
295
+
296
+ def test_key_inside_a_line_is_not_matched(self) -> None:
297
+ text = "[gerrit]\nnote = set host=wrong.example.org\nxhost=wrong\n"
298
+ assert parse_gitreview(text) is None
299
+
300
+ def test_headerless_file_is_searched_whole(self) -> None:
301
+ info = parse_gitreview("host=h.example.org\nport=29419\nproject=p\n")
302
+ assert info is not None
303
+ assert (info.host, info.port, info.project) == (
304
+ "h.example.org",
305
+ 29419,
306
+ "p",
307
+ )
308
+
309
+ def test_file_configparser_rejects_is_searched_whole(self) -> None:
310
+ # A duplicate section makes configparser raise, so git-review
311
+ # cannot read this file at all; the lenient scan still serves it.
312
+ text = "[gerrit]\nhost=a.example.org\n[gerrit]\nhost=b.example.org\n"
313
+ info = parse_gitreview(text)
314
+ assert info is not None
315
+ assert info.host == "a.example.org"
316
+
317
+ @pytest.mark.parametrize("key", ["host", "port", "project"])
318
+ def test_continuation_line_is_not_a_key(self, key: str) -> None:
319
+ # configparser folds a line indented deeper than the option
320
+ # above it into that option's value; git-review never sees it
321
+ # as a key, so neither may this parser.
322
+ value = {"host": "wrong.example.org", "port": "1", "project": "wrong"}
323
+ text = (
324
+ "[gerrit]\n"
325
+ "host=right.example.org\n"
326
+ "port=29419\n"
327
+ "project=right\n"
328
+ "note = explanation\n"
329
+ f" {key}={value[key]}\n"
330
+ )
331
+ info = parse_gitreview(text)
332
+ assert info is not None
333
+ assert (info.host, info.port, info.project) == (
334
+ "right.example.org",
335
+ 29419,
336
+ "right",
337
+ )
338
+
339
+ @pytest.mark.parametrize(
340
+ "gap",
341
+ ["\n", "# comment\n", "; comment\n"],
342
+ ids=["blank", "hash", "semi"],
343
+ )
344
+ def test_continuation_survives_blank_and_comment_lines(
345
+ self, gap: str
346
+ ) -> None:
347
+ text = f"[gerrit]\nnote = x\n{gap} host=wrong.example.org\n"
348
+ assert parse_gitreview(text) is None
349
+
350
+
351
+ # Layouts on which parse_gitreview must agree with configparser, the
352
+ # reader git-review uses. Each is a valid .gitreview a repository could
353
+ # hold; the parser is lenient beyond this, but never contrary to it.
354
+ CONFIGPARSER_LAYOUTS = [
355
+ pytest.param(TYPICAL_GITREVIEW, id="flat"),
356
+ pytest.param(INDENTED_GITREVIEW, id="all-indented"),
357
+ pytest.param(
358
+ "[gerrit]\n\thost=h.example.org\n\tport=29419\n\tproject=p\n",
359
+ id="tab-indented",
360
+ ),
361
+ pytest.param(
362
+ "[gerrit]\n host=h.example.org\n",
363
+ id="indented-under-flat-header",
364
+ ),
365
+ pytest.param(
366
+ "[gerrit]\n host=h.example.org\nport=29419\n",
367
+ id="dedent-after-indent",
368
+ ),
369
+ pytest.param(
370
+ "[gerrit]\nnote = x\n host=wrong.example.org\n", id="continuation"
371
+ ),
372
+ pytest.param(
373
+ "[gerrit]\nnote = x\n host=wrong.example.org\nhost=h.example.org\n",
374
+ id="continuation-then-key",
375
+ ),
376
+ pytest.param(
377
+ "[gerrit]\nhost=h.example.org\n port=1\n", id="continuation-of-host"
378
+ ),
379
+ pytest.param(
380
+ "[gerrit]\n note = x\n port=1\n host=h.example.org\n",
381
+ id="deeper-option-ends-run",
382
+ ),
383
+ pytest.param(
384
+ "[other]\n host=wrong.example.org\n port=1\n project=wrong\n"
385
+ "[gerrit]\nhost=right.example.org\nport=29419\nproject=right\n",
386
+ id="indented-keys-in-other-section",
387
+ ),
388
+ pytest.param(
389
+ "[other]\nhost=wrong.example.org\nport=1\nproject=wrong\n"
390
+ "[gerrit]\nhost=right.example.org\nport=29419\nproject=right\n",
391
+ id="flat-keys-in-earlier-section",
392
+ ),
393
+ pytest.param(
394
+ "[gerrit]\nhost=right.example.org\n[other]\nport=1\nproject=wrong\n",
395
+ id="flat-keys-in-later-section",
396
+ ),
397
+ pytest.param(
398
+ "[DEFAULT]\nhost=default.example.org\nport=29419\n[gerrit]\nproject=p\n",
399
+ id="default-section-fills-gerrit",
400
+ ),
401
+ pytest.param(
402
+ "[DEFAULT]\nhost=default.example.org\nport=1\n"
403
+ "[gerrit]\nhost=right.example.org\nport=29419\n",
404
+ id="gerrit-overrides-earlier-default",
405
+ ),
406
+ pytest.param(
407
+ "[gerrit]\nhost=right.example.org\n[DEFAULT]\nhost=default.example.org\n",
408
+ id="gerrit-overrides-later-default",
409
+ ),
410
+ pytest.param(
411
+ "[DEFAULT]\nhost=default.example.org\n[gerrit]\nhost=\n",
412
+ id="empty-host-overrides-default",
413
+ ),
414
+ pytest.param(
415
+ "[DEFAULT]\nport=1\n[gerrit]\nhost=h.example.org\nport=\n",
416
+ id="empty-port-overrides-default",
417
+ ),
418
+ pytest.param(
419
+ "[DEFAULT]\nproject=wrong\n[gerrit]\nhost=h.example.org\nproject=\n",
420
+ id="empty-project-overrides-default",
421
+ ),
422
+ ]
423
+
424
+
425
+ def _first_line(value: str) -> str:
426
+ """Return the first line of a configparser value, stripped.
427
+
428
+ configparser joins continuation lines into the value; a .gitreview
429
+ field is single-line, so only the first line is comparable.
430
+ """
431
+ return value.strip().split("\n", 1)[0].strip()
432
+
433
+
434
+ @pytest.mark.parametrize("text", CONFIGPARSER_LAYOUTS)
435
+ def test_agrees_with_configparser(text: str) -> None:
436
+ import configparser
437
+
438
+ parser = configparser.ConfigParser(interpolation=None)
439
+ parser.read_string(text)
440
+ section = parser["gerrit"]
441
+ info = parse_gitreview(text)
442
+ expected_host = _first_line(section.get("host", ""))
443
+ if not expected_host:
444
+ assert info is None
445
+ return
446
+ assert info is not None
447
+ assert info.host == expected_host
448
+ port = _first_line(section.get("port", "")) or str(DEFAULT_GERRIT_PORT)
449
+ assert info.port == int(port)
450
+ project = _first_line(section.get("project", "")).removesuffix(".git")
451
+ assert info.project == project
452
+
256
453
 
257
454
  # -----------------------------------------------------------------------
258
455
  # read_local_gitreview — local file reader
@@ -275,6 +472,14 @@ class TestReadLocalGitreview:
275
472
  p.write_text("garbage\n", encoding="utf-8")
276
473
  assert read_local_gitreview(p) is None
277
474
 
475
+ def test_reads_indented_file(self, tmp_path: Path) -> None:
476
+ p = tmp_path / ".gitreview"
477
+ p.write_text(INDENTED_GITREVIEW, encoding="utf-8")
478
+ info = read_local_gitreview(p)
479
+ assert info is not None
480
+ assert info.host == "gerrit.onap.org"
481
+ assert info.project == "cps"
482
+
278
483
  def test_returns_none_for_unreadable(self, tmp_path: Path) -> None:
279
484
  p = tmp_path / ".gitreview"
280
485
  p.write_text(TYPICAL_GITREVIEW, encoding="utf-8")