github2gerrit 1.0.4__tar.gz → 1.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/.pre-commit-config.yaml +3 -3
  2. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/PKG-INFO +4 -3
  3. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/README.md +2 -2
  4. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/action.yaml +1 -1
  5. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/pyproject.toml +3 -0
  6. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/cli.py +7 -5
  7. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/core.py +292 -8
  8. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/gerrit_pr_closer.py +10 -0
  9. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/gitutils.py +2 -2
  10. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/pr_content_filter.py +19 -21
  11. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/utils.py +10 -2
  12. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_change_id_deduplication.py +1 -1
  13. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_cli_outputs_file.py +1 -1
  14. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_cli_url_and_dryrun.py +2 -2
  15. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_core_close_pr_policy.py +1 -3
  16. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_core_prepare_commits.py +1 -1
  17. github2gerrit-1.0.6/tests/test_core_shallow_clone.py +826 -0
  18. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_gerrit_change_id_footer.py +1 -1
  19. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_gitutils_helpers.py +1 -1
  20. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_utils.py +79 -0
  21. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/uv.lock +93 -86
  22. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/.editorconfig +0 -0
  23. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/.gitignore +0 -0
  24. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/.gitlint +0 -0
  25. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/.markdownlint.yaml +0 -0
  26. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/.readthedocs.yml +0 -0
  27. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/.yamllint +0 -0
  28. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/LICENSE +0 -0
  29. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/LICENSES/Apache-2.0.txt +0 -0
  30. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/REUSE.toml +0 -0
  31. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
  32. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
  33. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/docs/RELEASE-v0.2.0.md +0 -0
  34. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/docs/github2gerrit_token_permissions_classic.png +0 -0
  35. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/sitecustomize.py +0 -0
  36. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/__init__.py +0 -0
  37. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/commit_normalization.py +0 -0
  38. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/config.py +0 -0
  39. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/constants.py +0 -0
  40. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/duplicate_detection.py +0 -0
  41. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/error_codes.py +0 -0
  42. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/external_api.py +0 -0
  43. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/gerrit_query.py +0 -0
  44. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/gerrit_rest.py +0 -0
  45. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/gerrit_urls.py +0 -0
  46. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/github_api.py +0 -0
  47. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/mapping_comment.py +0 -0
  48. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/models.py +0 -0
  49. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/netrc.py +0 -0
  50. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/orchestrator/__init__.py +0 -0
  51. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
  52. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/reconcile_matcher.py +0 -0
  53. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/rich_display.py +0 -0
  54. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/rich_logging.py +0 -0
  55. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/similarity.py +0 -0
  56. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/ssh_agent_setup.py +0 -0
  57. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/ssh_common.py +0 -0
  58. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/ssh_config_parser.py +0 -0
  59. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/ssh_discovery.py +0 -0
  60. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/src/github2gerrit/trailers.py +0 -0
  61. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/conftest.py +0 -0
  62. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/fixtures/__init__.py +0 -0
  63. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/fixtures/make_repo.py +0 -0
  64. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/fixtures/ssh_config_samples.py +0 -0
  65. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_action_environment_mapping.py +0 -0
  66. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_action_outputs.py +0 -0
  67. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_action_pr_number_handling.py +0 -0
  68. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_action_step_validation.py +0 -0
  69. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_automation_only.py +0 -0
  70. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_cli.py +0 -0
  71. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_cli_helpers.py +0 -0
  72. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_cli_netrc_options.py +0 -0
  73. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_commit_normalization.py +0 -0
  74. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_composite_action_coverage.py +0 -0
  75. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_config_and_reviewers.py +0 -0
  76. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_config_helpers.py +0 -0
  77. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_core_config_and_errors.py +0 -0
  78. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_core_gerrit_backref_comment.py +0 -0
  79. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_core_gerrit_push_errors.py +0 -0
  80. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_core_gerrit_rest_results.py +0 -0
  81. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_core_integration_fixture_repo.py +0 -0
  82. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_core_ssh_setup.py +0 -0
  83. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_core_ssrf_protection.py +0 -0
  84. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_duplicate_detection.py +0 -0
  85. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_email_case_normalization.py +0 -0
  86. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_error_codes.py +0 -0
  87. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_external_api_framework.py +0 -0
  88. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_force_flag_cli.py +0 -0
  89. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_gerrit_change_status_checks.py +0 -0
  90. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_gerrit_pr_closer.py +0 -0
  91. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_gerrit_rest_client.py +0 -0
  92. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_gerrit_urls.py +0 -0
  93. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_gerrit_urls_more.py +0 -0
  94. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_ghe_and_gitreview_args.py +0 -0
  95. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_github_api_error_handling.py +0 -0
  96. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_github_api_helpers.py +0 -0
  97. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_github_api_retry_and_helpers.py +0 -0
  98. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_mapping_comment_additional.py +0 -0
  99. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  100. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_metadata_and_reconciliation.py +0 -0
  101. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_metadata_trailer_separation_bug.py +0 -0
  102. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_misc_small_coverage.py +0 -0
  103. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_netrc.py +0 -0
  104. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_orphan_rest_side_effects.py +0 -0
  105. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_pr_content_filter.py +0 -0
  106. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_pr_content_filter_integration.py +0 -0
  107. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_pr_update_detection.py +0 -0
  108. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_reconciliation_extracted_module.py +0 -0
  109. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_reconciliation_plan_and_orphans.py +0 -0
  110. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_reconciliation_scenarios.py +0 -0
  111. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_ssh_agent.py +0 -0
  112. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_ssh_agent_ownership.py +0 -0
  113. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_ssh_artifact_prevention.py +0 -0
  114. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_ssh_common.py +0 -0
  115. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_ssh_discovery.py +0 -0
  116. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_ssh_discovery_dry_run.py +0 -0
  117. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_trailers_additional.py +0 -0
  118. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/test_url_parser.py +0 -0
  119. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/unit/test_config_integration.py +0 -0
  120. {github2gerrit-1.0.4 → github2gerrit-1.0.6}/tests/unit/test_ssh_config_parser.py +0 -0
@@ -59,7 +59,7 @@ repos:
59
59
  types: [yaml]
60
60
 
61
61
  - repo: https://github.com/astral-sh/ruff-pre-commit
62
- rev: 45ef068da5f21267bb2a7ec4a623092959f09ce5 # frozen: v0.14.14
62
+ rev: fa93bc3224c614a0e9786d3e2d3d48edcca246eb # frozen: v0.15.1
63
63
  hooks:
64
64
  - id: ruff
65
65
  files: ^(src|scripts|tests)/.+\.py$
@@ -110,7 +110,7 @@ repos:
110
110
  # Replaces: https://github.com/rhysd/actionlint
111
111
  # Permits actionlint to run both locally and with precommit.ci/GitHub
112
112
  - repo: https://github.com/Mateusz-Grzelinski/actionlint-py
113
- rev: 85c37735ea69e5baf0681530e57e63deee0ce733 # frozen: v1.7.10.24
113
+ rev: 694e2c0dfb4253d51f3c6c54b8f9fec0a16764dc # frozen: v1.7.11.24
114
114
  hooks:
115
115
  - id: actionlint
116
116
 
@@ -121,7 +121,7 @@ repos:
121
121
  - id: codespell
122
122
 
123
123
  - repo: https://github.com/python-jsonschema/check-jsonschema
124
- rev: ccf21790019848af3eb4464be2a9d5efed6358f3 # frozen: 0.36.1
124
+ rev: ec368acd16deee9c560c105ab6d27db4ee19a5ec # frozen: 0.36.2
125
125
  hooks:
126
126
  - id: check-github-actions
127
127
  - id: check-github-workflows
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github2gerrit
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: Submit a GitHub pull request to a Gerrit repository.
5
5
  Project-URL: Homepage, https://github.com/lfreleng-actions/github2gerrit-action
6
6
  Project-URL: Repository, https://github.com/lfreleng-actions/github2gerrit-action
@@ -22,6 +22,7 @@ Classifier: Topic :: Software Development :: Build Tools
22
22
  Classifier: Topic :: Software Development :: Version Control
23
23
  Classifier: Typing :: Typed
24
24
  Requires-Python: >=3.11
25
+ Requires-Dist: cryptography>=46.0.5
25
26
  Requires-Dist: git-review>=2.5.0
26
27
  Requires-Dist: pygerrit2>=2.0.15
27
28
  Requires-Dist: pygithub>=2.8.1
@@ -991,7 +992,7 @@ Common issues and solutions:
991
992
  The comprehensive [Inputs](#inputs) table above documents all environment variables.
992
993
  Key variables for CLI usage include:
993
994
 
994
- - `G2G_LOG_LEVEL`: Set to `DEBUG` for verbose output (default: `INFO`)
995
+ - `G2G_LOG_LEVEL`: Set to `DEBUG` for verbose output (default: `WARNING`)
995
996
  - `G2G_VERBOSE`: Set to `true` to enable debug logging (same as `--verbose` flag)
996
997
  - `GERRIT_SSH_PRIVKEY_G2G`: SSH private key content
997
998
  - `GERRIT_KNOWN_HOSTS`: SSH known hosts entries
@@ -1204,7 +1205,7 @@ The following environment variables control internal behavior but are not action
1204
1205
 
1205
1206
  | Environment Variable | Description | Default |
1206
1207
  | ---------------------------- | ---------------------------------------------- | ------------------------------------------ |
1207
- | `G2G_LOG_LEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) | `"INFO"` |
1208
+ | `G2G_LOG_LEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) | `"WARNING"` |
1208
1209
  | `G2G_ENABLE_DERIVATION` | Enable auto-derivation of Gerrit parameters | `"true"` |
1209
1210
  | `G2G_CONFIG_PATH` | Path to organization configuration file | `~/.config/github2gerrit/config.ini` |
1210
1211
  | `G2G_AUTO_SAVE_CONFIG` | Auto-save derived parameters to config | `"false"` (GitHub Actions), `"true"` (CLI) |
@@ -946,7 +946,7 @@ Common issues and solutions:
946
946
  The comprehensive [Inputs](#inputs) table above documents all environment variables.
947
947
  Key variables for CLI usage include:
948
948
 
949
- - `G2G_LOG_LEVEL`: Set to `DEBUG` for verbose output (default: `INFO`)
949
+ - `G2G_LOG_LEVEL`: Set to `DEBUG` for verbose output (default: `WARNING`)
950
950
  - `G2G_VERBOSE`: Set to `true` to enable debug logging (same as `--verbose` flag)
951
951
  - `GERRIT_SSH_PRIVKEY_G2G`: SSH private key content
952
952
  - `GERRIT_KNOWN_HOSTS`: SSH known hosts entries
@@ -1159,7 +1159,7 @@ The following environment variables control internal behavior but are not action
1159
1159
 
1160
1160
  | Environment Variable | Description | Default |
1161
1161
  | ---------------------------- | ---------------------------------------------- | ------------------------------------------ |
1162
- | `G2G_LOG_LEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) | `"INFO"` |
1162
+ | `G2G_LOG_LEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) | `"WARNING"` |
1163
1163
  | `G2G_ENABLE_DERIVATION` | Enable auto-derivation of Gerrit parameters | `"true"` |
1164
1164
  | `G2G_CONFIG_PATH` | Path to organization configuration file | `~/.config/github2gerrit/config.ini` |
1165
1165
  | `G2G_AUTO_SAVE_CONFIG` | Auto-save derived parameters to config | `"false"` (GitHub Actions), `"true"` (CLI) |
@@ -164,7 +164,7 @@ runs:
164
164
 
165
165
  - name: "Setup uv"
166
166
  # yamllint disable-line rule:line-length
167
- uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1
167
+ uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
168
168
  with:
169
169
  enable-cache: false
170
170
 
@@ -50,6 +50,9 @@ dependencies = [
50
50
 
51
51
  # Security: Fix CVE-2026-21441 (decompression-bomb vulnerability)
52
52
  "urllib3>=2.6.3",
53
+
54
+ # Security: Fix CVE-2026-26007 (SECT curve subgroup attack)
55
+ "cryptography>=46.0.5",
53
56
  ]
54
57
 
55
58
  [project.urls]
@@ -283,7 +283,9 @@ def _extract_and_display_pr_info(
283
283
  pr_info["Files Changed"] = "unknown"
284
284
 
285
285
  # Display the PR information
286
- display_pr_info(pr_info, "Pull Request Details", progress_tracker)
286
+ display_pr_info(
287
+ pr_info, "Pull Request Details", progress_tracker=progress_tracker
288
+ )
287
289
 
288
290
  except GitHub2GerritError:
289
291
  # Let our structured errors propagate
@@ -1099,8 +1101,8 @@ def main(
1099
1101
 
1100
1102
 
1101
1103
  def _setup_logging() -> logging.Logger:
1102
- level_name = os.getenv("G2G_LOG_LEVEL", "INFO").upper()
1103
- level = getattr(logging, level_name, logging.INFO)
1104
+ level_name = os.getenv("G2G_LOG_LEVEL", "WARNING").upper()
1105
+ level = getattr(logging, level_name, logging.WARNING)
1104
1106
  fmt = (
1105
1107
  "%(asctime)s %(levelname)-8s %(name)s %(filename)s:%(lineno)d | "
1106
1108
  "%(message)s"
@@ -1111,8 +1113,8 @@ def _setup_logging() -> logging.Logger:
1111
1113
 
1112
1114
  def _reconfigure_logging() -> None:
1113
1115
  """Reconfigure logging level based on current environment variables."""
1114
- level_name = os.getenv("G2G_LOG_LEVEL", "INFO").upper()
1115
- level = getattr(logging, level_name, logging.INFO)
1116
+ level_name = os.getenv("G2G_LOG_LEVEL", "WARNING").upper()
1117
+ level = getattr(logging, level_name, logging.WARNING)
1116
1118
  logging.getLogger().setLevel(level)
1117
1119
  for handler in logging.getLogger().handlers:
1118
1120
  handler.setLevel(level)
@@ -2547,9 +2547,10 @@ class Orchestrator:
2547
2547
  def _ensure_workspace_prepared(self, branch: str) -> None:
2548
2548
  """Ensure workspace is prepared with latest remote state.
2549
2549
 
2550
- Performs a single git fetch to avoid redundant SSH operations.
2551
- This consolidates multiple fetch operations that were causing
2552
- excessive SSH agent prompts.
2550
+ Performs a git fetch to get the latest branch state. Does NOT
2551
+ proactively unshallow to avoid performance impact on large repos.
2552
+ If checkout later fails due to missing commits in shallow clone,
2553
+ _checkout_with_unshallow_fallback() will handle it reactively.
2553
2554
 
2554
2555
  Args:
2555
2556
  branch: The branch to fetch from origin
@@ -2575,6 +2576,276 @@ class Orchestrator:
2575
2576
  # Don't mark as prepared if fetch failed
2576
2577
  raise
2577
2578
 
2579
+ def _is_shallow_clone(self) -> bool:
2580
+ """Check if the current workspace is a shallow clone.
2581
+
2582
+ Returns:
2583
+ True if the repository is a shallow clone, False otherwise.
2584
+ """
2585
+ shallow_file = self.workspace / ".git" / "shallow"
2586
+ if shallow_file.exists():
2587
+ return True
2588
+ # Also check via git command for edge cases (e.g., worktrees)
2589
+ try:
2590
+ result = run_cmd(
2591
+ ["git", "rev-parse", "--is-shallow-repository"],
2592
+ cwd=self.workspace,
2593
+ check=False,
2594
+ )
2595
+ return result.stdout.strip().lower() == "true"
2596
+ except Exception:
2597
+ return False
2598
+
2599
+ def _unshallow_repository(self) -> bool:
2600
+ """Unshallow the repository to get full history.
2601
+
2602
+ Returns:
2603
+ True if unshallowing succeeded or repo is already full,
2604
+ False if unshallowing failed.
2605
+ """
2606
+ if not self._is_shallow_clone():
2607
+ log.debug("Repository is not shallow, no unshallow needed")
2608
+ return True
2609
+
2610
+ log.info("Unshallowing repository to fetch full history...")
2611
+ try:
2612
+ run_cmd(
2613
+ ["git", "fetch", "--unshallow", "origin"],
2614
+ cwd=self.workspace,
2615
+ env=self._ssh_env(),
2616
+ )
2617
+ except CommandError as exc:
2618
+ log.warning("Failed to unshallow repository: %s", exc)
2619
+ return False
2620
+ else:
2621
+ log.debug("Repository unshallowed successfully")
2622
+ return True
2623
+
2624
+ def _deepen_repository(self, depth: int = 100) -> bool:
2625
+ """Deepen the repository history by fetching more commits.
2626
+
2627
+ This is a lighter alternative to full unshallow, fetching only
2628
+ the specified number of additional commits.
2629
+
2630
+ Args:
2631
+ depth: Number of additional commits to fetch (default: 100)
2632
+
2633
+ Returns:
2634
+ True if deepening succeeded, False otherwise.
2635
+ """
2636
+ if not self._is_shallow_clone():
2637
+ log.debug("Repository is not shallow, no deepening needed")
2638
+ return True
2639
+
2640
+ log.debug("Deepening repository by %d commits...", depth)
2641
+ try:
2642
+ run_cmd(
2643
+ ["git", "fetch", f"--deepen={depth}", "origin"],
2644
+ cwd=self.workspace,
2645
+ env=self._ssh_env(),
2646
+ )
2647
+ except CommandError as exc:
2648
+ log.debug("Failed to deepen repository: %s", exc)
2649
+ return False
2650
+ else:
2651
+ log.debug("Repository deepened by %d commits", depth)
2652
+ return True
2653
+
2654
+ def _checkout_with_unshallow_fallback(
2655
+ self,
2656
+ branch_name: str,
2657
+ start_point: str,
2658
+ create_branch: bool = True,
2659
+ ) -> None:
2660
+ """Checkout a branch with graduated deepening fallback.
2661
+
2662
+ If the initial checkout fails because the start_point SHA is not
2663
+ available (common in shallow clones), this method will:
2664
+ 1. First try to deepen the repository (fetch 100 more commits)
2665
+ 2. If that fails, fully unshallow the repository
2666
+ 3. Retry the checkout after each attempt
2667
+
2668
+ This graduated approach minimizes performance impact for most cases
2669
+ while still handling edge cases where full history is needed.
2670
+
2671
+ Args:
2672
+ branch_name: Name of the branch to checkout or create
2673
+ start_point: The SHA or ref to start the branch from
2674
+ create_branch: If True, create a new branch (-b flag)
2675
+
2676
+ Raises:
2677
+ CommandError: If checkout fails even after unshallowing
2678
+ """
2679
+ cmd = ["git", "checkout"]
2680
+ if create_branch:
2681
+ cmd.extend(["-b", branch_name, start_point])
2682
+ else:
2683
+ cmd.append(branch_name)
2684
+
2685
+ checkout_exc: CommandError | None = None
2686
+ try:
2687
+ run_cmd(cmd, cwd=self.workspace)
2688
+ except CommandError as exc:
2689
+ checkout_exc = exc
2690
+
2691
+ if checkout_exc is None:
2692
+ return # Success on first attempt
2693
+
2694
+ # Analyze the failure
2695
+ error_msg = str(checkout_exc).lower()
2696
+ # Check if failure is due to missing commit (shallow clone issue)
2697
+ is_missing_commit = (
2698
+ "not a commit" in error_msg
2699
+ or "cannot be created from" in error_msg
2700
+ or "bad revision" in error_msg
2701
+ or "unknown revision" in error_msg
2702
+ or "invalid reference" in error_msg
2703
+ )
2704
+
2705
+ if not is_missing_commit:
2706
+ # Not a shallow clone issue, re-raise immediately
2707
+ raise checkout_exc
2708
+
2709
+ log.warning(
2710
+ "Checkout failed, SHA %s not available in shallow clone. "
2711
+ "Attempting graduated deepening...",
2712
+ start_point,
2713
+ )
2714
+
2715
+ # Step 1: Try deepening first (cheaper than full unshallow)
2716
+ if self._deepen_repository(depth=100):
2717
+ log.debug("Retrying checkout after deepening...")
2718
+ try:
2719
+ run_cmd(cmd, cwd=self.workspace)
2720
+ except CommandError as deepen_exc:
2721
+ log.debug(
2722
+ "Checkout still failed after deepening: %s", deepen_exc
2723
+ )
2724
+ else:
2725
+ log.info("Checkout succeeded after deepening repository")
2726
+ return
2727
+
2728
+ # Step 2: Full unshallow as last resort
2729
+ log.info("Deepening insufficient, performing full unshallow...")
2730
+ if not self._unshallow_repository():
2731
+ log.error(
2732
+ "Failed to unshallow repository. Cannot checkout SHA: %s",
2733
+ start_point,
2734
+ )
2735
+ raise checkout_exc
2736
+
2737
+ # Retry the checkout after full unshallow
2738
+ log.debug("Retrying checkout after full unshallow...")
2739
+ run_cmd(cmd, cwd=self.workspace)
2740
+ log.info("Checkout succeeded after full unshallow")
2741
+
2742
+ def _merge_squash_with_unshallow_fallback(self, head_sha: str) -> None:
2743
+ """Perform git merge --squash with graduated deepening fallback.
2744
+
2745
+ If the initial merge fails because the shallow clone lacks a common
2746
+ ancestor between the base and head (causing "refusing to merge
2747
+ unrelated histories"), this method will:
2748
+ 1. First try to deepen the repository (fetch 100 more commits)
2749
+ 2. If that fails, fully unshallow the repository
2750
+ 3. Retry the merge after each attempt
2751
+
2752
+ This mirrors the graduated approach used by
2753
+ ``_checkout_with_unshallow_fallback`` but for merge operations,
2754
+ which are equally susceptible to shallow clone limitations.
2755
+
2756
+ Args:
2757
+ head_sha: The SHA to merge (PR head commit)
2758
+
2759
+ Raises:
2760
+ CommandError: If merge fails even after unshallowing, or if
2761
+ the failure is not related to shallow clone history
2762
+ """
2763
+ merge_cmd = ["git", "merge", "--squash", head_sha]
2764
+
2765
+ merge_exc: CommandError | None = None
2766
+ try:
2767
+ run_cmd(merge_cmd, cwd=self.workspace)
2768
+ except CommandError as exc:
2769
+ merge_exc = exc
2770
+
2771
+ if merge_exc is None:
2772
+ return # Success on first attempt
2773
+
2774
+ # Check if the failure is due to unrelated histories in a shallow clone
2775
+ error_str = str(merge_exc).lower()
2776
+ stderr_str = (merge_exc.stderr or "").lower()
2777
+ combined = f"{error_str} {stderr_str}"
2778
+
2779
+ is_unrelated_histories = (
2780
+ "refusing to merge unrelated histories" in combined
2781
+ or "unrelated histories" in combined
2782
+ or "no common ancestor" in combined
2783
+ )
2784
+
2785
+ if not is_unrelated_histories or not self._is_shallow_clone():
2786
+ # Not a shallow clone issue — let the caller handle the error
2787
+ raise merge_exc
2788
+
2789
+ log.warning(
2790
+ "Merge --squash failed due to unrelated histories in shallow "
2791
+ "clone. Attempting graduated deepening to recover..."
2792
+ )
2793
+
2794
+ # Step 1: Try deepening first (cheaper than full unshallow)
2795
+ if self._deepen_repository(depth=100):
2796
+ log.debug("Retrying merge --squash after deepening...")
2797
+ # Reset the failed merge state before retrying
2798
+ run_cmd(
2799
+ ["git", "merge", "--abort"],
2800
+ cwd=self.workspace,
2801
+ check=False,
2802
+ )
2803
+ try:
2804
+ run_cmd(merge_cmd, cwd=self.workspace)
2805
+ except CommandError as deepen_exc:
2806
+ log.debug(
2807
+ "Merge --squash still failed after deepening: %s",
2808
+ deepen_exc,
2809
+ )
2810
+ # Re-check whether this is still a shallow-history problem;
2811
+ # if the error has changed (e.g. real merge conflict), an
2812
+ # expensive full unshallow cannot help — propagate immediately.
2813
+ deepen_combined = (
2814
+ f"{deepen_exc} {deepen_exc.stderr or ''}".lower()
2815
+ )
2816
+ deepen_is_unrelated = (
2817
+ "refusing to merge unrelated histories" in deepen_combined
2818
+ or "unrelated histories" in deepen_combined
2819
+ or "no common ancestor" in deepen_combined
2820
+ )
2821
+ if not deepen_is_unrelated:
2822
+ raise
2823
+ else:
2824
+ log.info("Merge --squash succeeded after deepening repository")
2825
+ return
2826
+
2827
+ # Step 2: Full unshallow as last resort
2828
+ log.info(
2829
+ "Deepening insufficient, performing full unshallow for merge..."
2830
+ )
2831
+ # Reset the failed merge state before retrying
2832
+ run_cmd(
2833
+ ["git", "merge", "--abort"],
2834
+ cwd=self.workspace,
2835
+ check=False,
2836
+ )
2837
+ if not self._unshallow_repository():
2838
+ log.error(
2839
+ "Failed to unshallow repository. Cannot merge SHA: %s",
2840
+ head_sha,
2841
+ )
2842
+ raise merge_exc
2843
+
2844
+ # Retry the merge after full unshallow
2845
+ log.debug("Retrying merge --squash after full unshallow...")
2846
+ run_cmd(merge_cmd, cwd=self.workspace)
2847
+ log.info("Merge --squash succeeded after full unshallow")
2848
+
2578
2849
  def _cleanup_ssh(self) -> None:
2579
2850
  """Clean up temporary SSH files created by this tool.
2580
2851
 
@@ -2787,8 +3058,10 @@ class Orchestrator:
2787
3058
  ).stdout.strip()
2788
3059
  tmp_branch = f"g2g_tmp_{gh.pr_number or 'pr'!s}_{os.getpid()}"
2789
3060
  os.environ["G2G_TMP_BRANCH"] = tmp_branch
2790
- run_cmd(
2791
- ["git", "checkout", "-b", tmp_branch, base_sha], cwd=self.workspace
3061
+ self._checkout_with_unshallow_fallback(
3062
+ branch_name=tmp_branch,
3063
+ start_point=base_sha,
3064
+ create_branch=True,
2792
3065
  )
2793
3066
  change_ids: list[str] = []
2794
3067
  for idx, csha in enumerate(commit_list):
@@ -2937,9 +3210,20 @@ class Orchestrator:
2937
3210
 
2938
3211
  except Exception as debug_exc:
2939
3212
  log.warning("Failed to analyze merge situation: %s", debug_exc)
3213
+ # Proactively deepen if merge-base fails in a shallow clone,
3214
+ # as this strongly indicates the shallow history is insufficient
3215
+ # for the upcoming merge --squash operation.
3216
+ if self._is_shallow_clone():
3217
+ log.info(
3218
+ "merge-base failed in shallow clone — proactively "
3219
+ "deepening repository to improve merge success chances"
3220
+ )
3221
+ self._deepen_repository(depth=100)
2940
3222
 
2941
- run_cmd(
2942
- ["git", "checkout", "-b", tmp_branch, base_sha], cwd=self.workspace
3223
+ self._checkout_with_unshallow_fallback(
3224
+ branch_name=tmp_branch,
3225
+ start_point=base_sha,
3226
+ create_branch=True,
2943
3227
  )
2944
3228
 
2945
3229
  # Show git status before attempting merge
@@ -2966,7 +3250,7 @@ class Orchestrator:
2966
3250
 
2967
3251
  log.debug("About to run: git merge --squash %s", head_sha)
2968
3252
  try:
2969
- run_cmd(["git", "merge", "--squash", head_sha], cwd=self.workspace)
3253
+ self._merge_squash_with_unshallow_fallback(head_sha)
2970
3254
  except CommandError as merge_exc:
2971
3255
  # Enhanced error handling for git merge failures
2972
3256
  error_details = self._analyze_merge_failure(
@@ -1492,6 +1492,16 @@ def cleanup_closed_github_prs(
1492
1492
  )
1493
1493
  continue
1494
1494
 
1495
+ except GerritRestError as exc:
1496
+ # Wrap in GitHub2GerritError to ensure SSL/connection errors
1497
+ # fail the workflow
1498
+ log.exception("Failed to perform Gerrit cleanup for closed GitHub PRs")
1499
+ raise GitHub2GerritError(
1500
+ ExitCode.GERRIT_CONNECTION_ERROR,
1501
+ message="❌ Gerrit REST API error during cleanup",
1502
+ details=str(exc),
1503
+ original_exception=exc,
1504
+ ) from exc
1495
1505
  except Exception:
1496
1506
  log.exception("Failed to perform Gerrit cleanup for closed GitHub PRs")
1497
1507
  return 0
@@ -51,8 +51,8 @@ _LOGGER_NAME = "github2gerrit.git"
51
51
  log = logging.getLogger(_LOGGER_NAME)
52
52
  if not log.handlers:
53
53
  # Provide a minimal default if the app has not configured logging.
54
- level_name = os.getenv("G2G_LOG_LEVEL", "INFO").upper()
55
- level = getattr(logging, level_name, logging.INFO)
54
+ level_name = os.getenv("G2G_LOG_LEVEL", "WARNING").upper()
55
+ level = getattr(logging, level_name, logging.WARNING)
56
56
  fmt = (
57
57
  "%(asctime)s %(levelname)-8s %(name)s %(filename)s:%(lineno)d | "
58
58
  "%(message)s"
@@ -44,27 +44,6 @@ _MULTIPLE_NEWLINES_PATTERN = re.compile(r"\n{3,}")
44
44
  _EMOJI_PATTERN = re.compile(r":[a-z_]+:") # GitHub emoji codes like :sparkles:
45
45
 
46
46
 
47
- @dataclass
48
- class FilterConfig:
49
- """Configuration for PR content filtering."""
50
-
51
- # Global options
52
- enabled: bool = True
53
- remove_emoji_codes: bool = True
54
- deduplicate_title_in_body: bool = True
55
-
56
- # Author-specific filtering
57
- author_rules: dict[str, str] = field(default_factory=dict)
58
-
59
- # Rule-specific configurations
60
- dependabot_config: DependabotConfig = field(
61
- default_factory=lambda: DependabotConfig()
62
- )
63
- precommit_config: PrecommitConfig = field(
64
- default_factory=lambda: PrecommitConfig()
65
- )
66
-
67
-
68
47
  @dataclass
69
48
  class DependabotConfig:
70
49
  """Configuration for Dependabot PR filtering."""
@@ -85,6 +64,25 @@ class PrecommitConfig:
85
64
  # Future: add pre-commit.ci specific options
86
65
 
87
66
 
67
+ @dataclass
68
+ class FilterConfig:
69
+ """Configuration for PR content filtering."""
70
+
71
+ # Global options
72
+ enabled: bool = True
73
+ remove_emoji_codes: bool = True
74
+ deduplicate_title_in_body: bool = True
75
+
76
+ # Author-specific filtering
77
+ author_rules: dict[str, str] = field(default_factory=dict)
78
+
79
+ # Rule-specific configurations
80
+ dependabot_config: DependabotConfig = field(
81
+ default_factory=DependabotConfig
82
+ )
83
+ precommit_config: PrecommitConfig = field(default_factory=PrecommitConfig)
84
+
85
+
88
86
  class FilterRule(ABC):
89
87
  """Abstract base class for PR content filtering rules."""
90
88
 
@@ -63,9 +63,17 @@ def is_verbose_mode() -> bool:
63
63
  """Check if verbose mode is enabled via environment variable.
64
64
 
65
65
  Returns:
66
- True if G2G_VERBOSE environment variable is set to a truthy value
66
+ True if G2G_VERBOSE environment variable is set to a truthy value,
67
+ or if GitHub Actions debug mode is enabled (RUNNER_DEBUG=1 or
68
+ ACTIONS_STEP_DEBUG=true)
67
69
  """
68
- return os.getenv("G2G_VERBOSE", "").lower() in ("true", "1", "yes")
70
+ if os.getenv("G2G_VERBOSE", "").lower() in ("true", "1", "yes"):
71
+ return True
72
+ # GitHub Actions sets RUNNER_DEBUG=1 when re-running with debug logging
73
+ if os.getenv("RUNNER_DEBUG", "") == "1":
74
+ return True
75
+ # GitHub Actions also sets ACTIONS_STEP_DEBUG=true for step-level debugging
76
+ return os.getenv("ACTIONS_STEP_DEBUG", "").lower() == "true"
69
77
 
70
78
 
71
79
  def log_exception_conditionally(
@@ -275,7 +275,7 @@ Change-Id: {reused_change_id}
275
275
  monkeypatch.setattr("github2gerrit.core.run_cmd", mock_run_cmd)
276
276
 
277
277
  # Mock GitHub API for Change-ID reuse
278
- monkeypatch.setattr("github2gerrit.core.build_client", lambda: object())
278
+ monkeypatch.setattr("github2gerrit.core.build_client", object)
279
279
  monkeypatch.setattr(
280
280
  "github2gerrit.core.get_repo_from_env", lambda _: object()
281
281
  )
@@ -232,7 +232,7 @@ def test_multi_pr_url_mode_writes_aggregated_outputs(
232
232
  def __init__(self, number: int) -> None:
233
233
  self.number = number
234
234
 
235
- monkeypatch.setattr(cli_mod, "build_client", lambda: object())
235
+ monkeypatch.setattr(cli_mod, "build_client", object)
236
236
  monkeypatch.setattr(cli_mod, "get_repo_from_env", lambda _client: object())
237
237
  monkeypatch.setattr(
238
238
  cli_mod,
@@ -168,7 +168,7 @@ def test_repo_url_dry_run_invokes_for_each_open_pr(
168
168
  monkeypatch.setattr(cli_mod, "Orchestrator", _DummyOrchestrator)
169
169
 
170
170
  # Patch PyGithub wrapper functions used by CLI bulk path
171
- monkeypatch.setattr(cli_mod, "build_client", lambda: object())
171
+ monkeypatch.setattr(cli_mod, "build_client", object)
172
172
  monkeypatch.setattr(cli_mod, "get_repo_from_env", lambda _client: object())
173
173
  monkeypatch.setattr(
174
174
  cli_mod, "iter_open_pulls", lambda _repo: iter(dummy_prs)
@@ -212,7 +212,7 @@ def test_url_mode_sets_environment_for_config_resolution(
212
212
  monkeypatch.setattr(cli_mod, "Orchestrator", _DummyOrchestrator)
213
213
 
214
214
  # Minimal patches for bulk flow
215
- monkeypatch.setattr(cli_mod, "build_client", lambda: object())
215
+ monkeypatch.setattr(cli_mod, "build_client", object)
216
216
  monkeypatch.setattr(cli_mod, "get_repo_from_env", lambda _client: object())
217
217
  monkeypatch.setattr(
218
218
  cli_mod,
@@ -102,9 +102,7 @@ def test_close_pr_invoked_for_pull_request_target_event(
102
102
  self.closed_state: str | None = None
103
103
 
104
104
  # Patch the GitHub helper functions used by the close path
105
- monkeypatch.setattr(
106
- "github2gerrit.core.build_client", lambda: DummyClient()
107
- )
105
+ monkeypatch.setattr("github2gerrit.core.build_client", DummyClient)
108
106
  monkeypatch.setattr(
109
107
  "github2gerrit.core.get_repo_from_env", lambda _c: DummyRepo()
110
108
  )
@@ -237,7 +237,7 @@ def test_prepare_squashed_commit_reuses_change_id_from_comments(
237
237
  monkeypatch.setattr("github2gerrit.core.run_cmd", fake_run_cmd)
238
238
 
239
239
  # GitHub API helpers used to fetch PR comments
240
- monkeypatch.setattr("github2gerrit.core.build_client", lambda: object())
240
+ monkeypatch.setattr("github2gerrit.core.build_client", object)
241
241
  monkeypatch.setattr(
242
242
  "github2gerrit.core.get_repo_from_env", lambda _c: object()
243
243
  )