github2gerrit 1.2.0__tar.gz → 1.2.2__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 (131) hide show
  1. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/.pre-commit-config.yaml +6 -6
  2. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/PKG-INFO +2 -1
  3. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/action.yaml +1 -1
  4. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/pyproject.toml +8 -1
  5. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/cli.py +25 -12
  6. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/commit_normalization.py +3 -2
  7. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/core.py +243 -26
  8. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/gerrit_pr_closer.py +231 -28
  9. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/gerrit_query.py +67 -2
  10. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/netrc.py +4 -6
  11. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/pr_content_filter.py +1 -1
  12. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/similarity.py +21 -6
  13. github2gerrit-1.2.2/tests/test_clean_squash_title.py +298 -0
  14. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_commit_normalization.py +1 -1
  15. github2gerrit-1.2.2/tests/test_dependency_supersession.py +776 -0
  16. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/uv.lock +262 -196
  17. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/.editorconfig +0 -0
  18. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/.gitignore +0 -0
  19. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/.gitlint +0 -0
  20. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/.markdownlint.yaml +0 -0
  21. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/.readthedocs.yml +0 -0
  22. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/.yamllint +0 -0
  23. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/LICENSE +0 -0
  24. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/LICENSES/Apache-2.0.txt +0 -0
  25. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/README.md +0 -0
  26. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/REUSE.toml +0 -0
  27. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/docs/COMMIT_RULES.md +0 -0
  28. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
  29. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
  30. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/docs/RELEASE-v0.2.0.md +0 -0
  31. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/docs/github2gerrit_token_permissions_classic.png +0 -0
  32. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/sitecustomize.py +0 -0
  33. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/__init__.py +0 -0
  34. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/commit_rules.py +0 -0
  35. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/config.py +0 -0
  36. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/constants.py +0 -0
  37. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/duplicate_detection.py +0 -0
  38. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/error_codes.py +0 -0
  39. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/external_api.py +0 -0
  40. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/gerrit_rest.py +0 -0
  41. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/gerrit_urls.py +0 -0
  42. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/github_api.py +0 -0
  43. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/gitreview.py +0 -0
  44. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/gitutils.py +0 -0
  45. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/mapping_comment.py +0 -0
  46. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/models.py +0 -0
  47. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/orchestrator/__init__.py +0 -0
  48. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
  49. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/pr_commands.py +0 -0
  50. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/reconcile_matcher.py +0 -0
  51. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/rich_display.py +0 -0
  52. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/rich_logging.py +0 -0
  53. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/ssh_agent_setup.py +0 -0
  54. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/ssh_common.py +0 -0
  55. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/ssh_config_parser.py +0 -0
  56. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/ssh_discovery.py +0 -0
  57. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/trailers.py +0 -0
  58. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/src/github2gerrit/utils.py +0 -0
  59. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/conftest.py +0 -0
  60. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/fixtures/__init__.py +0 -0
  61. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/fixtures/make_repo.py +0 -0
  62. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/fixtures/ssh_config_samples.py +0 -0
  63. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_action_environment_mapping.py +0 -0
  64. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_action_outputs.py +0 -0
  65. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_action_pr_number_handling.py +0 -0
  66. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_action_step_validation.py +0 -0
  67. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_automation_only.py +0 -0
  68. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_change_id_deduplication.py +0 -0
  69. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_cli.py +0 -0
  70. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_cli_helpers.py +0 -0
  71. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_cli_netrc_options.py +0 -0
  72. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_cli_outputs_file.py +0 -0
  73. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_cli_url_and_dryrun.py +0 -0
  74. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_commit_rules.py +0 -0
  75. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_composite_action_coverage.py +0 -0
  76. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_config_and_reviewers.py +0 -0
  77. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_config_helpers.py +0 -0
  78. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_core_close_pr_policy.py +0 -0
  79. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_core_config_and_errors.py +0 -0
  80. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_core_gerrit_backref_comment.py +0 -0
  81. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_core_gerrit_push_errors.py +0 -0
  82. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_core_gerrit_rest_results.py +0 -0
  83. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_core_integration_fixture_repo.py +0 -0
  84. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_core_prepare_commits.py +0 -0
  85. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_core_shallow_clone.py +0 -0
  86. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_core_ssh_setup.py +0 -0
  87. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_core_ssrf_protection.py +0 -0
  88. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_dns_validation_and_no_gerrit.py +0 -0
  89. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_duplicate_detection.py +0 -0
  90. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_email_case_normalization.py +0 -0
  91. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_error_codes.py +0 -0
  92. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_external_api_framework.py +0 -0
  93. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_force_flag_cli.py +0 -0
  94. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_gerrit_change_id_footer.py +0 -0
  95. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_gerrit_change_status_checks.py +0 -0
  96. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_gerrit_pr_closer.py +0 -0
  97. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_gerrit_rest_client.py +0 -0
  98. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_gerrit_urls.py +0 -0
  99. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_gerrit_urls_more.py +0 -0
  100. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_ghe_and_gitreview_args.py +0 -0
  101. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_github_api_error_handling.py +0 -0
  102. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_github_api_helpers.py +0 -0
  103. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_github_api_retry_and_helpers.py +0 -0
  104. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_gitreview.py +0 -0
  105. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_gitutils_helpers.py +0 -0
  106. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_issue_157_regressions.py +0 -0
  107. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_mapping_comment_additional.py +0 -0
  108. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  109. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_metadata_and_reconciliation.py +0 -0
  110. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_metadata_trailer_separation_bug.py +0 -0
  111. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_misc_small_coverage.py +0 -0
  112. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_netrc.py +0 -0
  113. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_orphan_rest_side_effects.py +0 -0
  114. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_pr_commands.py +0 -0
  115. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_pr_content_filter.py +0 -0
  116. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_pr_content_filter_integration.py +0 -0
  117. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_pr_update_detection.py +0 -0
  118. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_reconciliation_extracted_module.py +0 -0
  119. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_reconciliation_plan_and_orphans.py +0 -0
  120. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_reconciliation_scenarios.py +0 -0
  121. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_ssh_agent.py +0 -0
  122. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_ssh_agent_ownership.py +0 -0
  123. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_ssh_artifact_prevention.py +0 -0
  124. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_ssh_common.py +0 -0
  125. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_ssh_discovery.py +0 -0
  126. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_ssh_discovery_dry_run.py +0 -0
  127. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_trailers_additional.py +0 -0
  128. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_url_parser.py +0 -0
  129. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/test_utils.py +0 -0
  130. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/unit/test_config_integration.py +0 -0
  131. {github2gerrit-1.2.0 → github2gerrit-1.2.2}/tests/unit/test_ssh_config_parser.py +0 -0
@@ -59,7 +59,7 @@ repos:
59
59
  types: [yaml]
60
60
 
61
61
  - repo: https://github.com/astral-sh/ruff-pre-commit
62
- rev: e05c5c0818279e5ac248ac9e954431ba58865e61 # frozen: v0.15.7
62
+ rev: 0c7b6c989466a93942def1f84baf36ddfcd60c83 # frozen: v0.15.14
63
63
  hooks:
64
64
  - id: ruff
65
65
  files: ^(src|scripts|tests)/.+\.py$
@@ -68,7 +68,7 @@ repos:
68
68
  files: ^(src|scripts|tests)/.+\.py$
69
69
 
70
70
  - repo: https://github.com/pre-commit/mirrors-mypy
71
- rev: a66e98df7b4aeeb3724184b332785976d062b92e # frozen: v1.19.1
71
+ rev: d2823d321df3af8f878f7ee3414dc94d037145b9 # frozen: v2.1.0
72
72
  hooks:
73
73
  - id: mypy
74
74
  files: ^src/.+\.py$
@@ -103,7 +103,7 @@ repos:
103
103
  # Replaces: https://github.com/rhysd/actionlint
104
104
  # Permits actionlint to run both locally and with precommit.ci/GitHub
105
105
  - repo: https://github.com/Mateusz-Grzelinski/actionlint-py
106
- rev: 694e2c0dfb4253d51f3c6c54b8f9fec0a16764dc # frozen: v1.7.11.24
106
+ rev: c04ed26e40637cab1aa9879c693832a9c120fb20 # frozen: v1.7.12.24
107
107
  hooks:
108
108
  - id: actionlint
109
109
 
@@ -115,18 +115,18 @@ repos:
115
115
 
116
116
  # Requires a mirror, primary repo lacks .pre-commit-hooks.yaml
117
117
  - repo: https://github.com/DetachHead/basedpyright-prek-mirror
118
- rev: d58fe7fc44458fa7e41e2d38c04598a3c87833f2 # frozen: 1.38.3
118
+ rev: 78e6efd50b63647fecb7e65fc7032745d861e2c5 # frozen: 1.39.6
119
119
  hooks:
120
120
  - id: basedpyright
121
121
  files: ^src/.+\.py$
122
122
 
123
123
  - repo: https://github.com/lfreleng-actions/gha-workflow-linter
124
- rev: a7caf8f3a1a05688d1cee46615ff94def617e5a3 # frozen: v1.0.2
124
+ rev: 2c315e461ec3379bf9c1682360fdc1a3899f88c9 # frozen: v1.0.4
125
125
  hooks:
126
126
  - id: gha-workflow-linter
127
127
 
128
128
  - repo: https://github.com/python-jsonschema/check-jsonschema
129
- rev: 8db279a37c552206d2df62269ff6f9d31125815a # frozen: 0.37.0
129
+ rev: 943377262562a12b57292fc98fabd7dbf81451fe # frozen: 0.37.2
130
130
  hooks:
131
131
  - id: check-github-actions
132
132
  - id: check-github-workflows
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github2gerrit
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Submit a GitHub pull request to a Gerrit repository.
5
5
  Project-URL: Homepage, https://github.com/lfreleng-actions/github2gerrit-action
6
6
  Project-URL: Repository, https://github.com/lfreleng-actions/github2gerrit-action
@@ -22,6 +22,7 @@ Classifier: Topic :: Software Development :: Build Tools
22
22
  Classifier: Topic :: Software Development :: Version Control
23
23
  Classifier: Typing :: Typed
24
24
  Requires-Python: >=3.11
25
+ Requires-Dist: click>=8.1.7
25
26
  Requires-Dist: cryptography>=46.0.5
26
27
  Requires-Dist: git-review>=2.5.0
27
28
  Requires-Dist: pygerrit2>=2.0.15
@@ -221,7 +221,7 @@ runs:
221
221
  - name: "Setup uv"
222
222
  if: steps.disabled-check.outputs.disabled != 'true'
223
223
  # yamllint disable-line rule:line-length
224
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
224
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
225
225
  with:
226
226
  enable-cache: false
227
227
 
@@ -30,6 +30,13 @@ dependencies = [
30
30
  # CLI framework (Typer)
31
31
  "typer>=0.20.1",
32
32
 
33
+ # Imported directly by the CLI (click.Group subclass for custom usage
34
+ # formatting; click.core.ParameterSource to detect explicit CLI flags)
35
+ # rather than only transitively via typer. Declared as a direct
36
+ # dependency so our import contract doesn't depend on typer's internal
37
+ # dependency choices.
38
+ "click>=8.1.7",
39
+
33
40
  # Rich formatting for CLI output
34
41
  "rich>=14.2.0",
35
42
 
@@ -208,7 +215,7 @@ disallow_untyped_calls = false
208
215
 
209
216
  [tool.coverage.run]
210
217
  branch = true
211
- source = ["src/github2gerrit"]
218
+ source = ["github2gerrit"]
212
219
  omit = [
213
220
  "tests/*",
214
221
  "src/github2gerrit/gerrit_rest.py",
@@ -2226,7 +2226,7 @@ def _process() -> None:
2226
2226
  ):
2227
2227
  try:
2228
2228
  log.debug(
2229
- "🔍 Checking for Gerrit change to abandon for PR #%s",
2229
+ "Checking for Gerrit change to abandon for PR #%s",
2230
2230
  gh.pr_number,
2231
2231
  )
2232
2232
  change_number = abandon_gerrit_change_for_closed_pr(
@@ -2238,16 +2238,29 @@ def _process() -> None:
2238
2238
  progress_tracker=None,
2239
2239
  )
2240
2240
  if change_number:
2241
- gerrit_change_url = (
2242
- f"https://{data.gerrit_server}/c/"
2243
- f"{data.gerrit_project}/+/{change_number}"
2244
- )
2245
- log.debug(
2246
- "✅ Successfully abandoned Gerrit change %s "
2247
- "for pull request #%s",
2248
- gerrit_change_url,
2249
- gh.pr_number,
2250
- )
2241
+ try:
2242
+ from .gerrit_urls import create_gerrit_url_builder
2243
+
2244
+ _url_builder = create_gerrit_url_builder(
2245
+ data.gerrit_server
2246
+ )
2247
+ gerrit_change_url = _url_builder.change_url(
2248
+ data.gerrit_project,
2249
+ int(change_number),
2250
+ )
2251
+ log.debug(
2252
+ "Successfully abandoned Gerrit "
2253
+ "change %s for pull request #%s",
2254
+ gerrit_change_url,
2255
+ gh.pr_number,
2256
+ )
2257
+ except Exception:
2258
+ log.debug(
2259
+ "Successfully abandoned Gerrit "
2260
+ "change %s for pull request #%s",
2261
+ change_number,
2262
+ gh.pr_number,
2263
+ )
2251
2264
  # Console output already done by
2252
2265
  # abandon_gerrit_change_for_closed_pr
2253
2266
  else:
@@ -2295,7 +2308,7 @@ def _process() -> None:
2295
2308
  log.warning("Gerrit cleanup failed: %s", exc)
2296
2309
 
2297
2310
  log.debug(
2298
- "Cleanup operations completed for closed PR #%s",
2311
+ "Cleanup operations completed for closed PR #%s",
2299
2312
  gh.pr_number or "unknown",
2300
2313
  )
2301
2314
  return
@@ -401,8 +401,9 @@ class CommitNormalizer:
401
401
  # Remove trailing ellipsis
402
402
  title = re.sub(r"\s*[.]{3,}.*$", "", title)
403
403
 
404
- # Remove markdown formatting
405
- title = re.sub(r"[*_`]", "", title)
404
+ # Remove markdown bold/code formatting but preserve underscores
405
+ # (which appear in package names and filesystem paths).
406
+ title = re.sub(r"[*`]", "", title)
406
407
 
407
408
  # For dependabot titles, extract the essential information
408
409
  for pattern in DEPENDABOT_PATTERNS:
@@ -154,6 +154,93 @@ def _clean_ellipses_from_message(message: str) -> str:
154
154
  return "\n".join(cleaned_lines)
155
155
 
156
156
 
157
+ def _clean_squash_title_line(title_line: str | None) -> str:
158
+ """Clean and truncate a squashed commit title line.
159
+
160
+ Handles markdown removal, separator splitting, and length
161
+ truncation while preserving conventional commit prefixes
162
+ and underscores in package/path names.
163
+
164
+ Args:
165
+ title_line: Raw title line from git log output.
166
+
167
+ Returns:
168
+ Cleaned title line, safe for use as a commit subject.
169
+ """
170
+ from .similarity import CC_PREFIX_RE
171
+
172
+ if not title_line:
173
+ return ""
174
+
175
+ # Remove markdown links
176
+ title_line = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", title_line)
177
+ # Remove trailing ellipsis/truncation
178
+ title_line = re.sub(r"\s*[.]{3,}.*$", "", title_line)
179
+ # Split on common separators to avoid leaking body content
180
+ for separator in [". Bumps ", " Bumps ", ". - ", " - "]:
181
+ if separator in title_line:
182
+ title_line = title_line.split(separator)[0].strip()
183
+ break
184
+ # Remove markdown bold/code formatting but preserve underscores
185
+ # (which appear in package names and filesystem paths).
186
+ title_line = re.sub(r"[*`]", "", title_line).strip()
187
+
188
+ if len(title_line) > 100:
189
+ # Detect conventional commit prefix length so that the
190
+ # ": " break-point does not split on the prefix separator
191
+ # (e.g. "Build(deps): " should not be treated as a sentence
192
+ # break).
193
+ cc_match = CC_PREFIX_RE.match(title_line)
194
+ cc_prefix_len = cc_match.end() if cc_match else 0
195
+
196
+ break_points = [". ", "! ", "? ", " - ", ": "]
197
+ max_bp_len = max(len(bp) for bp in break_points)
198
+ truncated = False
199
+ for bp in break_points:
200
+ # For the ": " break-point, start searching after the
201
+ # conventional commit prefix to avoid splitting there.
202
+ search_start = cc_prefix_len if bp == ": " else 0
203
+ # Extend the slice by (max_bp_len - 1) so that a
204
+ # break-point starting just before position 100 is
205
+ # still detected even if it spans across the boundary.
206
+ candidate_end = min(len(title_line), 100 + max_bp_len - 1)
207
+ candidate = title_line[search_start:candidate_end]
208
+ bp_offset = candidate.find(bp)
209
+ if bp_offset != -1:
210
+ bp_idx = search_start + bp_offset
211
+ # Only use this break-point if it starts within
212
+ # the 100-char limit.
213
+ if bp_idx >= 100:
214
+ continue
215
+ # Punctuation break-points (". ", ": ") — include
216
+ # the punctuation mark. Separator break-points
217
+ # (" - ") — truncate before the separator.
218
+ if bp[0].isspace():
219
+ title_line = title_line[:bp_idx].rstrip()
220
+ else:
221
+ title_line = title_line[
222
+ : bp_idx + len(bp.rstrip())
223
+ ].rstrip()
224
+ truncated = True
225
+ break
226
+
227
+ if not truncated and cc_prefix_len == 0:
228
+ # Non-CC title with no break-point found: fall back
229
+ # to word-boundary truncation at 100 characters.
230
+ words = title_line[:100].split()
231
+ title_line = (
232
+ " ".join(words[:-1])
233
+ if len(words) > 1
234
+ else title_line[:100].rstrip()
235
+ )
236
+ # For CC titles with no break-point: pass through the
237
+ # full title. The length is inherent to the structured
238
+ # subject (e.g. long dependency paths), not body-content
239
+ # leakage.
240
+
241
+ return title_line
242
+
243
+
157
244
  # ---------------------
158
245
  # Utility functions
159
246
  # ---------------------
@@ -678,6 +765,9 @@ class Orchestrator:
678
765
  2. GitHub-Hash trailer matching
679
766
  3. GitHub-PR trailer URL matching
680
767
  4. Mapping comment parsing from PR comments
768
+ 5. Dependency package match — find an open change that
769
+ bumps the same dependency (for Dependabot / Renovate
770
+ supersession).
681
771
 
682
772
  Args:
683
773
  gh: GitHub context containing PR information
@@ -793,6 +883,10 @@ class Orchestrator:
793
883
  except Exception as exc:
794
884
  log.debug("GitHub-Hash trailer query failed: %s", exc)
795
885
 
886
+ # Cache the PR title for reuse across strategies 4 and 5
887
+ # so we don't duplicate GitHub API requests.
888
+ cached_pr_title: str = ""
889
+
796
890
  # Strategy 4: Parse mapping comments from PR
797
891
  try:
798
892
  from .mapping_comment import parse_mapping_comments
@@ -800,6 +894,7 @@ class Orchestrator:
800
894
  client_gh = build_client()
801
895
  repo = get_repo_from_env(client_gh)
802
896
  pr_obj = get_pull(repo, int(gh.pr_number))
897
+ cached_pr_title = getattr(pr_obj, "title", "") or ""
803
898
 
804
899
  issue = pr_obj.as_issue()
805
900
  comments = list(issue.get_comments())
@@ -830,6 +925,110 @@ class Orchestrator:
830
925
  except Exception as exc:
831
926
  log.debug("Mapping comment parsing failed: %s", exc)
832
927
 
928
+ # Strategy 5: Dependency package match (supersession)
929
+ # When a new Dependabot/Renovate PR bumps the same dependency
930
+ # as an existing open Gerrit change, reuse that Change-Id so
931
+ # the push creates a new patchset instead of a duplicate change.
932
+ try:
933
+ from .gerrit_query import GerritChange
934
+ from .gerrit_query import query_open_changes_by_project
935
+ from .gerrit_rest import build_client_for_host
936
+ from .similarity import extract_dependency_package_from_subject
937
+ from .trailers import GITHUB_PR_TRAILER
938
+ from .trailers import parse_trailers
939
+
940
+ # Reuse PR title cached by Strategy 4 to avoid a
941
+ # duplicate GitHub API request.
942
+ pr_title = cached_pr_title
943
+ if not pr_title:
944
+ log.debug(
945
+ "Strategy 5: PR title cache miss, fetching from GitHub API",
946
+ )
947
+ try:
948
+ gh_client = build_client()
949
+ gh_repo = get_repo_from_env(gh_client)
950
+ pr_obj = get_pull(gh_repo, int(gh.pr_number))
951
+ pr_title = getattr(pr_obj, "title", "") or ""
952
+ except Exception:
953
+ pr_title = ""
954
+
955
+ current_pkg = extract_dependency_package_from_subject(pr_title)
956
+ if current_pkg:
957
+ log.debug(
958
+ "Strategy 5: searching for open changes that "
959
+ "bump dependency '%s'",
960
+ current_pkg,
961
+ )
962
+ dep_client = build_client_for_host(gerrit.host)
963
+ open_changes = query_open_changes_by_project(
964
+ dep_client,
965
+ gerrit.project,
966
+ branch=gh.base_ref,
967
+ max_results=200,
968
+ )
969
+
970
+ # Collect all matching changes, then select the
971
+ # oldest one (lowest change number) to avoid
972
+ # "downgrading" a newer change by uploading an
973
+ # older patchset to it.
974
+ candidates: list[tuple[int, GerritChange]] = []
975
+ for change in open_changes:
976
+ candidate_pkg = extract_dependency_package_from_subject(
977
+ change.subject
978
+ )
979
+ if candidate_pkg and candidate_pkg == current_pkg:
980
+ # Verify this is a GitHub2Gerrit change
981
+ commit_msg = change.commit_message or ""
982
+ trailers = parse_trailers(commit_msg)
983
+ if GITHUB_PR_TRAILER not in trailers:
984
+ log.debug(
985
+ "Strategy 5: skipping change %s "
986
+ "(no GitHub2Gerrit metadata)",
987
+ change.number,
988
+ )
989
+ continue
990
+ try:
991
+ change_num = int(change.number)
992
+ except (TypeError, ValueError):
993
+ log.debug(
994
+ "Strategy 5: skipping change with "
995
+ "invalid number %r for subject %r",
996
+ change.number,
997
+ change.subject,
998
+ )
999
+ continue
1000
+ candidates.append((change_num, change))
1001
+
1002
+ if candidates:
1003
+ # Prefer the oldest open change so the newest
1004
+ # PR always updates the original change and
1005
+ # the post-push sweep abandons the rest.
1006
+ candidates.sort(key=lambda t: t[0])
1007
+ _, oldest = candidates[0]
1008
+ change_ids = [oldest.change_id]
1009
+ log.info(
1010
+ "Found superseding target by dependency "
1011
+ "package '%s': change %s (%s) "
1012
+ "(oldest of %d candidate(s))",
1013
+ current_pkg,
1014
+ oldest.number,
1015
+ oldest.subject,
1016
+ len(candidates),
1017
+ )
1018
+ return change_ids
1019
+
1020
+ log.debug(
1021
+ "No open changes found for dependency '%s'",
1022
+ current_pkg,
1023
+ )
1024
+ else:
1025
+ log.debug(
1026
+ "Strategy 5 skipped: could not extract dependency "
1027
+ "package from PR title"
1028
+ )
1029
+ except Exception as exc:
1030
+ log.debug("Dependency package strategy failed: %s", exc)
1031
+
833
1032
  log.warning(
834
1033
  "⚠️ No existing Gerrit changes found for PR #%s",
835
1034
  gh.pr_number,
@@ -1936,6 +2135,49 @@ class Orchestrator:
1936
2135
  # Validate that no unexpected files were committed
1937
2136
  self._validate_committed_files(gh, result)
1938
2137
 
2138
+ # Post-push supersession sweep (Option A fallback).
2139
+ # After a successful push, check whether other open Gerrit
2140
+ # changes in the same project bump the same dependency
2141
+ # package. If Strategy 5 already reused the old Change-Id
2142
+ # (update-in-place), no duplicates should exist. If that
2143
+ # path was skipped (e.g. non-dependency PR, or the query
2144
+ # failed), this sweep catches and abandons stale changes.
2145
+ if not inputs.dry_run and gerrit and prep.change_ids:
2146
+ try:
2147
+ from .gerrit_pr_closer import (
2148
+ abandon_superseded_dependency_changes,
2149
+ )
2150
+
2151
+ # Derive the subject from the pushed commit,
2152
+ # regardless of whether change URL lookup
2153
+ # succeeded.
2154
+ push_subject = ""
2155
+ try:
2156
+ push_subject = run_cmd(
2157
+ [
2158
+ "git",
2159
+ "show",
2160
+ "-s",
2161
+ "--pretty=format:%s",
2162
+ "HEAD",
2163
+ ],
2164
+ cwd=self.workspace,
2165
+ ).stdout.strip()
2166
+ except Exception:
2167
+ push_subject = ""
2168
+
2169
+ if push_subject:
2170
+ abandon_superseded_dependency_changes(
2171
+ gerrit_server=gerrit.host,
2172
+ gerrit_project=gerrit.project,
2173
+ current_subject=push_subject,
2174
+ exclude_change_ids=prep.change_ids,
2175
+ dry_run=False,
2176
+ target_branch=self._resolve_target_branch(),
2177
+ )
2178
+ except Exception as exc:
2179
+ log.debug("Post-push supersession sweep skipped: %s", exc)
2180
+
1939
2181
  self._close_pull_request_if_required(gh)
1940
2182
 
1941
2183
  log.debug("Pipeline complete: %s", result)
@@ -3564,32 +3806,7 @@ class Orchestrator:
3564
3806
  return message_lines, signed_off, change_ids
3565
3807
 
3566
3808
  def _clean_title_line(title_line: str) -> str:
3567
- # Remove markdown links
3568
- title_line = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", title_line)
3569
- # Remove trailing ellipsis/truncation
3570
- title_line = re.sub(r"\s*[.]{3,}.*$", "", title_line)
3571
- # Split on common separators to avoid leaking body content
3572
- for separator in [". Bumps ", " Bumps ", ". - ", " - "]:
3573
- if separator in title_line:
3574
- title_line = title_line.split(separator)[0].strip()
3575
- break
3576
- # Remove simple markdown/formatting artifacts
3577
- title_line = re.sub(r"[*_`]", "", title_line).strip()
3578
- if len(title_line) > 100:
3579
- break_points = [". ", "! ", "? ", " - ", ": "]
3580
- for bp in break_points:
3581
- if bp in title_line[:100]:
3582
- title_line = title_line[
3583
- : title_line.index(bp) + len(bp.strip())
3584
- ]
3585
- break
3586
- else:
3587
- words = title_line[:100].split()
3588
- title_line = (
3589
- " ".join(words[:-1])
3590
- if len(words) > 1
3591
- else title_line[:100].rstrip()
3592
- )
3809
+ title_line = _clean_squash_title_line(title_line)
3593
3810
 
3594
3811
  # Apply conventional commit normalization if enabled
3595
3812
  if inputs.normalise_commit and gh.pr_number: