github2gerrit 1.0.3__tar.gz → 1.0.4__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 (119) hide show
  1. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/.pre-commit-config.yaml +2 -2
  2. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/PKG-INFO +42 -1
  3. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/README.md +41 -0
  4. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/action.yaml +3 -3
  5. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/cli.py +65 -0
  6. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/core.py +18 -42
  7. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/duplicate_detection.py +10 -9
  8. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/gerrit_rest.py +57 -15
  9. github2gerrit-1.0.4/src/github2gerrit/netrc.py +832 -0
  10. github2gerrit-1.0.4/tests/test_cli_netrc_options.py +345 -0
  11. github2gerrit-1.0.4/tests/test_netrc.py +752 -0
  12. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/uv.lock +25 -25
  13. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/.editorconfig +0 -0
  14. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/.gitignore +0 -0
  15. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/.gitlint +0 -0
  16. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/.markdownlint.yaml +0 -0
  17. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/.readthedocs.yml +0 -0
  18. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/.yamllint +0 -0
  19. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/LICENSE +0 -0
  20. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/LICENSES/Apache-2.0.txt +0 -0
  21. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/REUSE.toml +0 -0
  22. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
  23. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
  24. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/docs/RELEASE-v0.2.0.md +0 -0
  25. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/docs/github2gerrit_token_permissions_classic.png +0 -0
  26. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/pyproject.toml +0 -0
  27. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/sitecustomize.py +0 -0
  28. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/__init__.py +0 -0
  29. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/commit_normalization.py +0 -0
  30. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/config.py +0 -0
  31. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/constants.py +0 -0
  32. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/error_codes.py +0 -0
  33. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/external_api.py +0 -0
  34. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/gerrit_pr_closer.py +0 -0
  35. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/gerrit_query.py +0 -0
  36. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/gerrit_urls.py +0 -0
  37. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/github_api.py +0 -0
  38. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/gitutils.py +0 -0
  39. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/mapping_comment.py +0 -0
  40. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/models.py +0 -0
  41. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/orchestrator/__init__.py +0 -0
  42. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
  43. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/pr_content_filter.py +0 -0
  44. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/reconcile_matcher.py +0 -0
  45. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/rich_display.py +0 -0
  46. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/rich_logging.py +0 -0
  47. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/similarity.py +0 -0
  48. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/ssh_agent_setup.py +0 -0
  49. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/ssh_common.py +0 -0
  50. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/ssh_config_parser.py +0 -0
  51. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/ssh_discovery.py +0 -0
  52. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/trailers.py +0 -0
  53. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/src/github2gerrit/utils.py +0 -0
  54. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/conftest.py +0 -0
  55. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/fixtures/__init__.py +0 -0
  56. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/fixtures/make_repo.py +0 -0
  57. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/fixtures/ssh_config_samples.py +0 -0
  58. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_action_environment_mapping.py +0 -0
  59. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_action_outputs.py +0 -0
  60. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_action_pr_number_handling.py +0 -0
  61. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_action_step_validation.py +0 -0
  62. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_automation_only.py +0 -0
  63. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_change_id_deduplication.py +0 -0
  64. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_cli.py +0 -0
  65. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_cli_helpers.py +0 -0
  66. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_cli_outputs_file.py +0 -0
  67. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_cli_url_and_dryrun.py +0 -0
  68. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_commit_normalization.py +0 -0
  69. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_composite_action_coverage.py +0 -0
  70. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_config_and_reviewers.py +0 -0
  71. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_config_helpers.py +0 -0
  72. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_core_close_pr_policy.py +0 -0
  73. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_core_config_and_errors.py +0 -0
  74. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_core_gerrit_backref_comment.py +0 -0
  75. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_core_gerrit_push_errors.py +0 -0
  76. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_core_gerrit_rest_results.py +0 -0
  77. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_core_integration_fixture_repo.py +0 -0
  78. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_core_prepare_commits.py +0 -0
  79. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_core_ssh_setup.py +0 -0
  80. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_core_ssrf_protection.py +0 -0
  81. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_duplicate_detection.py +0 -0
  82. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_email_case_normalization.py +0 -0
  83. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_error_codes.py +0 -0
  84. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_external_api_framework.py +0 -0
  85. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_force_flag_cli.py +0 -0
  86. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_gerrit_change_id_footer.py +0 -0
  87. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_gerrit_change_status_checks.py +0 -0
  88. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_gerrit_pr_closer.py +0 -0
  89. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_gerrit_rest_client.py +0 -0
  90. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_gerrit_urls.py +0 -0
  91. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_gerrit_urls_more.py +0 -0
  92. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_ghe_and_gitreview_args.py +0 -0
  93. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_github_api_error_handling.py +0 -0
  94. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_github_api_helpers.py +0 -0
  95. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_github_api_retry_and_helpers.py +0 -0
  96. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_gitutils_helpers.py +0 -0
  97. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_mapping_comment_additional.py +0 -0
  98. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  99. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_metadata_and_reconciliation.py +0 -0
  100. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_metadata_trailer_separation_bug.py +0 -0
  101. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_misc_small_coverage.py +0 -0
  102. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_orphan_rest_side_effects.py +0 -0
  103. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_pr_content_filter.py +0 -0
  104. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_pr_content_filter_integration.py +0 -0
  105. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_pr_update_detection.py +0 -0
  106. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_reconciliation_extracted_module.py +0 -0
  107. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_reconciliation_plan_and_orphans.py +0 -0
  108. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_reconciliation_scenarios.py +0 -0
  109. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_ssh_agent.py +0 -0
  110. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_ssh_agent_ownership.py +0 -0
  111. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_ssh_artifact_prevention.py +0 -0
  112. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_ssh_common.py +0 -0
  113. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_ssh_discovery.py +0 -0
  114. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_ssh_discovery_dry_run.py +0 -0
  115. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_trailers_additional.py +0 -0
  116. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_url_parser.py +0 -0
  117. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/test_utils.py +0 -0
  118. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/tests/unit/test_config_integration.py +0 -0
  119. {github2gerrit-1.0.3 → github2gerrit-1.0.4}/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: 9b1e9a33f1b33f6ecc2fdbb8d7cc894e951106e4 # frozen: v0.14.13
62
+ rev: 45ef068da5f21267bb2a7ec4a623092959f09ce5 # frozen: v0.14.14
63
63
  hooks:
64
64
  - id: ruff
65
65
  files: ^(src|scripts|tests)/.+\.py$
@@ -121,7 +121,7 @@ repos:
121
121
  - id: codespell
122
122
 
123
123
  - repo: https://github.com/python-jsonschema/check-jsonschema
124
- rev: b035497fb64e3f9faa91e833331688cc185891e6 # frozen: 0.36.0
124
+ rev: ccf21790019848af3eb4464be2a9d5efed6358f3 # frozen: 0.36.1
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.3
3
+ Version: 1.0.4
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
@@ -1270,6 +1270,47 @@ GERRIT_HTTP_USER = "bot-user"
1270
1270
  GERRIT_HTTP_PASSWORD = "${ENV:ODL_GERRIT_TOKEN}"
1271
1271
  ```
1272
1272
 
1273
+ ### Using .netrc Files
1274
+
1275
+ GitHub2Gerrit supports loading Gerrit credentials from `.netrc` files, following
1276
+ the standard format used by curl and other tools.
1277
+
1278
+ **Search order:**
1279
+
1280
+ 1. `.netrc` in the current directory
1281
+ 2. `~/.netrc` in your home directory
1282
+ 3. `~/_netrc` (Windows fallback)
1283
+
1284
+ **Example `.netrc` file:**
1285
+
1286
+ ```text
1287
+ machine gerrit.onap.org login myuser password mytoken
1288
+ machine gerrit.opendaylight.org login myuser password anothertoken
1289
+ ```
1290
+
1291
+ **CLI options:**
1292
+
1293
+ | Option | Description |
1294
+ | ------ | ----------- |
1295
+ | `--no-netrc` | Disable .netrc file lookup |
1296
+ | `--netrc-file PATH` | Use a specific .netrc file |
1297
+ | `--netrc-optional` | Do not fail if .netrc file is missing (default) |
1298
+ | `--netrc-required` | Require a .netrc file and fail if missing |
1299
+
1300
+ By default, `.netrc` lookup is optional (`--netrc-optional`): if the tool
1301
+ finds no `.netrc` file, it continues and falls back to environment variables.
1302
+ Use `--netrc-required` to enforce that a `.netrc` file must be present.
1303
+
1304
+ When a `.netrc` file is present, the tool loads credentials automatically.
1305
+ Explicit environment variables or CLI arguments take precedence over `.netrc`
1306
+ entries.
1307
+
1308
+ **Credential Priority Order:**
1309
+
1310
+ 1. **CLI arguments** (highest priority)
1311
+ 2. **`.netrc` file** (if not disabled with `--no-netrc`)
1312
+ 3. **Environment variables** (e.g., `GERRIT_HTTP_USER`, `GERRIT_HTTP_PASSWORD`)
1313
+
1273
1314
  The tool loads configuration from `~/.config/github2gerrit/configuration.txt`
1274
1315
  by default, or from the path specified in the `G2G_CONFIG_PATH` environment
1275
1316
  variable.
@@ -1225,6 +1225,47 @@ GERRIT_HTTP_USER = "bot-user"
1225
1225
  GERRIT_HTTP_PASSWORD = "${ENV:ODL_GERRIT_TOKEN}"
1226
1226
  ```
1227
1227
 
1228
+ ### Using .netrc Files
1229
+
1230
+ GitHub2Gerrit supports loading Gerrit credentials from `.netrc` files, following
1231
+ the standard format used by curl and other tools.
1232
+
1233
+ **Search order:**
1234
+
1235
+ 1. `.netrc` in the current directory
1236
+ 2. `~/.netrc` in your home directory
1237
+ 3. `~/_netrc` (Windows fallback)
1238
+
1239
+ **Example `.netrc` file:**
1240
+
1241
+ ```text
1242
+ machine gerrit.onap.org login myuser password mytoken
1243
+ machine gerrit.opendaylight.org login myuser password anothertoken
1244
+ ```
1245
+
1246
+ **CLI options:**
1247
+
1248
+ | Option | Description |
1249
+ | ------ | ----------- |
1250
+ | `--no-netrc` | Disable .netrc file lookup |
1251
+ | `--netrc-file PATH` | Use a specific .netrc file |
1252
+ | `--netrc-optional` | Do not fail if .netrc file is missing (default) |
1253
+ | `--netrc-required` | Require a .netrc file and fail if missing |
1254
+
1255
+ By default, `.netrc` lookup is optional (`--netrc-optional`): if the tool
1256
+ finds no `.netrc` file, it continues and falls back to environment variables.
1257
+ Use `--netrc-required` to enforce that a `.netrc` file must be present.
1258
+
1259
+ When a `.netrc` file is present, the tool loads credentials automatically.
1260
+ Explicit environment variables or CLI arguments take precedence over `.netrc`
1261
+ entries.
1262
+
1263
+ **Credential Priority Order:**
1264
+
1265
+ 1. **CLI arguments** (highest priority)
1266
+ 2. **`.netrc` file** (if not disabled with `--no-netrc`)
1267
+ 3. **Environment variables** (e.g., `GERRIT_HTTP_USER`, `GERRIT_HTTP_PASSWORD`)
1268
+
1228
1269
  The tool loads configuration from `~/.config/github2gerrit/configuration.txt`
1229
1270
  by default, or from the path specified in the `G2G_CONFIG_PATH` environment
1230
1271
  variable.
@@ -158,19 +158,19 @@ runs:
158
158
  steps:
159
159
  - name: "Setup Python"
160
160
  # yamllint disable-line rule:line-length
161
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
161
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
162
162
  with:
163
163
  python-version-file: '${{ github.action_path }}/pyproject.toml'
164
164
 
165
165
  - name: "Setup uv"
166
166
  # yamllint disable-line rule:line-length
167
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
167
+ uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1
168
168
  with:
169
169
  enable-cache: false
170
170
 
171
171
  - name: "Checkout repository"
172
172
  # yamllint disable-line rule:line-length
173
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
173
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
174
174
  with:
175
175
  fetch-depth: ${{ inputs.FETCH_DEPTH }}
176
176
  # Ensure we are on the PR's head SHA when triggered by PR events
@@ -69,6 +69,8 @@ from .gitutils import enumerate_reviewer_emails
69
69
  from .gitutils import git
70
70
  from .models import GitHubContext
71
71
  from .models import Inputs
72
+ from .netrc import NetrcParseError
73
+ from .netrc import get_credentials_for_host
72
74
  from .rich_display import RICH_AVAILABLE
73
75
  from .rich_display import DummyProgressTracker
74
76
  from .rich_display import G2GProgressTracker
@@ -778,6 +780,29 @@ def main(
778
780
  envvar="AUTOMATION_ONLY",
779
781
  help="Accept pull requests from known automation tools.",
780
782
  ),
783
+ no_netrc: bool = typer.Option(
784
+ False,
785
+ "--no-netrc",
786
+ help="Disable .netrc credential lookup for Gerrit HTTP authentication",
787
+ envvar="G2G_NO_NETRC",
788
+ ),
789
+ netrc_file: Path | None = typer.Option( # noqa: B008
790
+ None,
791
+ "--netrc-file",
792
+ help="Explicit path to .netrc file for Gerrit HTTP credentials",
793
+ envvar="G2G_NETRC_FILE",
794
+ exists=True,
795
+ file_okay=True,
796
+ dir_okay=False,
797
+ readable=True,
798
+ resolve_path=True,
799
+ ),
800
+ netrc_optional: bool = typer.Option(
801
+ True,
802
+ "--netrc-optional/--netrc-required",
803
+ help="Whether to fail if .netrc file is not found (default: optional)",
804
+ envvar="G2G_NETRC_OPTIONAL",
805
+ ),
781
806
  ) -> None:
782
807
  """
783
808
  Tool to convert GitHub pull requests into Gerrit changes
@@ -836,6 +861,46 @@ def main(
836
861
  if os.getenv("AUTOMATION_ONLY"):
837
862
  automation_only = parse_bool_env(os.getenv("AUTOMATION_ONLY"))
838
863
 
864
+ # Store netrc options in environment for use by processing functions
865
+ os.environ["G2G_NO_NETRC"] = "true" if no_netrc else "false"
866
+ if netrc_file:
867
+ os.environ["G2G_NETRC_FILE"] = str(netrc_file)
868
+ os.environ["G2G_NETRC_OPTIONAL"] = "true" if netrc_optional else "false"
869
+
870
+ # Handle netrc credential loading if enabled
871
+ if not no_netrc:
872
+ # Get the Gerrit server from environment or CLI
873
+ gerrit_host = gerrit_server or os.getenv("GERRIT_SERVER", "")
874
+ if gerrit_host:
875
+ try:
876
+ netrc_creds = get_credentials_for_host(
877
+ host=gerrit_host,
878
+ netrc_file=netrc_file,
879
+ use_netrc=True,
880
+ netrc_optional=netrc_optional,
881
+ )
882
+ if netrc_creds:
883
+ # Set HTTP credentials from netrc if not already set
884
+ if not os.getenv("GERRIT_HTTP_USER"):
885
+ os.environ["GERRIT_HTTP_USER"] = netrc_creds.login
886
+ if not os.getenv("GERRIT_HTTP_PASSWORD"):
887
+ os.environ["GERRIT_HTTP_PASSWORD"] = (
888
+ netrc_creds.password
889
+ )
890
+ log.debug(
891
+ "Loaded Gerrit HTTP credentials for %s from .netrc",
892
+ gerrit_host,
893
+ )
894
+ except FileNotFoundError:
895
+ if not netrc_optional:
896
+ safe_typer_echo(
897
+ "❌ No .netrc file found and --netrc-required set"
898
+ )
899
+ sys.exit(int(ExitCode.CONFIGURATION_ERROR))
900
+ except NetrcParseError as e:
901
+ safe_typer_echo(f"❌ Failed to parse .netrc file: {e}")
902
+ sys.exit(int(ExitCode.CONFIGURATION_ERROR))
903
+
839
904
  # Set up logging level based on verbose flag
840
905
  if verbose:
841
906
  os.environ["G2G_LOG_LEVEL"] = "DEBUG"
@@ -307,29 +307,21 @@ class Orchestrator:
307
307
  return True
308
308
 
309
309
  try:
310
- # Get credentials if available
311
- http_user = (
312
- os.getenv("GERRIT_HTTP_USER", "").strip()
313
- or os.getenv("GERRIT_SSH_USER_G2G", "").strip()
314
- )
315
- http_pass = os.getenv("GERRIT_HTTP_PASSWORD", "").strip()
310
+ # Use centralized URL builder with automatic credential resolution.
311
+ # Credential priority: CLI args > .netrc > environment variables.
312
+ # This call uses .netrc > environment (no CLI args passed).
313
+ from .gerrit_rest import build_client_for_host
316
314
 
317
- if not http_user or not http_pass:
315
+ client = build_client_for_host(gerrit.host)
316
+
317
+ # Check if client has authentication
318
+ if not client.is_authenticated:
318
319
  log.debug(
319
320
  "Cannot update Gerrit change metadata: "
320
- "GERRIT_HTTP_USER/PASSWORD not configured"
321
+ "No credentials found (check .netrc or environment)"
321
322
  )
322
323
  return False
323
324
 
324
- # Use centralized URL builder
325
- from .gerrit_rest import build_client_for_host
326
-
327
- client = build_client_for_host(
328
- gerrit.host,
329
- http_user=http_user,
330
- http_password=http_pass,
331
- )
332
-
333
325
  encoded_id = urllib.parse.quote(change_id, safe="")
334
326
 
335
327
  # Get current commit message to preserve G2G metadata and trailers
@@ -4229,14 +4221,9 @@ class Orchestrator:
4229
4221
  # Create centralized URL builder (auto-discovers base path)
4230
4222
  url_builder = create_gerrit_url_builder(gerrit.host)
4231
4223
 
4232
- # Get authentication credentials
4233
- http_user = (
4234
- os.getenv("GERRIT_HTTP_USER", "").strip()
4235
- or os.getenv("GERRIT_SSH_USER_G2G", "").strip()
4236
- )
4237
- http_pass = os.getenv("GERRIT_HTTP_PASSWORD", "").strip()
4238
-
4239
4224
  # Query changes using centralized REST client
4225
+ # Credential priority: CLI args > .netrc > environment variables.
4226
+ # This call uses .netrc > environment (no CLI args passed).
4240
4227
  urls: list[str] = []
4241
4228
  nums: list[str] = []
4242
4229
  shas: list[str] = []
@@ -4256,8 +4243,6 @@ class Orchestrator:
4256
4243
  gerrit.host,
4257
4244
  timeout=8.0,
4258
4245
  max_attempts=5,
4259
- http_user=http_user or None,
4260
- http_password=http_pass or None,
4261
4246
  )
4262
4247
  try:
4263
4248
  log.debug("Gerrit API base URL (discovered): %s", api_base_url)
@@ -5349,13 +5334,9 @@ class Orchestrator:
5349
5334
  raise OrchestratorError(msg) from exc
5350
5335
 
5351
5336
  # Gerrit REST reachability and optional auth check
5337
+ # Credential priority: CLI args > .netrc > environment variables.
5352
5338
  base_path = os.getenv("GERRIT_HTTP_BASE_PATH", "").strip().strip("/")
5353
- http_user = (
5354
- os.getenv("GERRIT_HTTP_USER", "").strip()
5355
- or os.getenv("GERRIT_SSH_USER_G2G", "").strip()
5356
- )
5357
- http_pass = os.getenv("GERRIT_HTTP_PASSWORD", "").strip()
5358
- self._verify_gerrit_rest(gerrit.host, base_path, http_user, http_pass)
5339
+ self._verify_gerrit_rest(gerrit.host, base_path)
5359
5340
 
5360
5341
  # GitHub token and metadata checks
5361
5342
  try:
@@ -5403,33 +5384,28 @@ class Orchestrator:
5403
5384
  self,
5404
5385
  host: str,
5405
5386
  base_path: str,
5406
- http_user: str,
5407
- http_pass: str,
5408
5387
  ) -> None:
5409
5388
  """Probe Gerrit REST endpoint with optional auth.
5410
5389
 
5411
5390
  Uses the centralized gerrit_rest client to ensure proper base path
5412
5391
  handling and consistent API interactions.
5392
+
5393
+ Credential priority: CLI args > .netrc > environment variables.
5413
5394
  """
5414
5395
  from .gerrit_rest import build_client_for_host
5415
5396
 
5416
5397
  try:
5417
- # Use centralized client builder that handles base path correctly
5398
+ # Use centralized client builder for base path and credentials
5418
5399
  client = build_client_for_host(
5419
5400
  host,
5420
5401
  timeout=8.0,
5421
5402
  max_attempts=3,
5422
- http_user=http_user,
5423
- http_password=http_pass,
5424
5403
  )
5425
5404
 
5426
5405
  # Test connectivity with appropriate endpoint
5427
- if http_user and http_pass:
5406
+ if client.is_authenticated:
5428
5407
  _ = client.get("/accounts/self")
5429
- log.debug(
5430
- "Gerrit REST authenticated access verified for user '%s'",
5431
- http_user,
5432
- )
5408
+ log.debug("Gerrit REST authenticated access verified")
5433
5409
  else:
5434
5410
  _ = client.get("/dashboard/self")
5435
5411
  log.debug("Gerrit REST endpoint reachable (unauthenticated)")
@@ -268,22 +268,23 @@ class DuplicateDetector:
268
268
  return None
269
269
 
270
270
  def _build_gerrit_rest_client(self, gerrit_host: str) -> Any | None:
271
- """Build a Gerrit REST API client using centralized framework."""
272
- from .gerrit_rest import build_client_for_host
271
+ """Build a Gerrit REST API client using centralized framework.
273
272
 
274
- http_user = (
275
- os.getenv("GERRIT_HTTP_USER", "").strip()
276
- or os.getenv("GERRIT_SSH_USER_G2G", "").strip()
277
- )
278
- http_pass = os.getenv("GERRIT_HTTP_PASSWORD", "").strip()
273
+ Credential resolution is handled by build_client_for_host with priority:
274
+ 1. Explicit CLI arguments (if passed to build_client_for_host)
275
+ 2. .netrc file
276
+ 3. Environment variables (GERRIT_HTTP_USER/GERRIT_HTTP_PASSWORD)
277
+
278
+ This method does not pass explicit credentials, so only .netrc and
279
+ environment variables are used.
280
+ """
281
+ from .gerrit_rest import build_client_for_host
279
282
 
280
283
  try:
281
284
  return build_client_for_host(
282
285
  gerrit_host,
283
286
  timeout=8.0,
284
287
  max_attempts=3,
285
- http_user=http_user or None,
286
- http_password=http_pass or None,
287
288
  )
288
289
  except Exception as exc:
289
290
  log.debug("Failed to create Gerrit REST client: %s", exc)
@@ -30,11 +30,11 @@ from __future__ import annotations
30
30
  import base64
31
31
  import json
32
32
  import logging
33
- import os
34
33
  import urllib.error
35
34
  import urllib.parse
36
35
  import urllib.request
37
36
  from dataclasses import dataclass
37
+ from pathlib import Path
38
38
  from typing import Any
39
39
  from typing import Final
40
40
  from urllib.parse import urljoin
@@ -43,6 +43,8 @@ from .external_api import ApiType
43
43
  from .external_api import RetryPolicy
44
44
  from .external_api import external_api_call
45
45
  from .gerrit_urls import create_gerrit_url_builder
46
+ from .netrc import GerritCredentials
47
+ from .netrc import resolve_gerrit_credentials
46
48
  from .utils import log_exception_conditionally
47
49
 
48
50
 
@@ -156,6 +158,11 @@ class GerritRestClient:
156
158
 
157
159
  # Public API
158
160
 
161
+ @property
162
+ def is_authenticated(self) -> bool:
163
+ """Return True if client has authentication credentials."""
164
+ return self._auth is not None
165
+
159
166
  def get(self, path: str) -> Any:
160
167
  """HTTP GET, returning parsed JSON."""
161
168
  return self._request_json_with_retry("GET", path)
@@ -290,37 +297,72 @@ def build_client_for_host(
290
297
  max_attempts: int = 5,
291
298
  http_user: str | None = None,
292
299
  http_password: str | None = None,
300
+ use_netrc: bool = True,
301
+ netrc_file: Path | None = None,
302
+ credentials: GerritCredentials | None = None,
293
303
  ) -> GerritRestClient:
294
304
  """
295
305
  Build a GerritRestClient for a given host using the centralized URL builder.
296
306
 
297
307
  - Uses auto-discovered or environment-provided base path.
298
- - Reads HTTP auth from arguments or environment:
299
- GERRIT_HTTP_USER / GERRIT_HTTP_PASSWORD
300
- If user is not provided, falls back to GERRIT_SSH_USER_G2G per project
301
- norms.
308
+ - Reads HTTP auth from multiple sources in priority order:
309
+ 1. Pre-resolved GerritCredentials object (if provided)
310
+ 2. Explicit http_user/http_password arguments
311
+ 3. .netrc file (if use_netrc=True)
312
+ 4. Environment variables: GERRIT_HTTP_USER / GERRIT_HTTP_PASSWORD
313
+ If user is not provided, falls back to GERRIT_SSH_USER_G2G per project
314
+ norms.
302
315
 
303
316
  Args:
304
317
  host: Gerrit hostname (no scheme)
305
318
  timeout: Request timeout in seconds.
306
319
  max_attempts: Max retry attempts for transient failures.
307
- http_user: Optional HTTP user.
308
- http_password: Optional HTTP password/token.
320
+ http_user: Optional HTTP user (deprecated, use credentials).
321
+ http_password: Optional HTTP password/token (deprecated, use credentials).
322
+ use_netrc: Whether to try .netrc for credentials (default: True).
323
+ netrc_file: Explicit path to a .netrc file (optional).
324
+ credentials: Pre-resolved GerritCredentials object (preferred).
309
325
 
310
326
  Returns:
311
327
  Configured GerritRestClient.
312
328
  """
313
329
  builder = create_gerrit_url_builder(host)
314
330
  base_url = builder.api_url()
315
- user = (
316
- (http_user or "").strip()
317
- or os.getenv("GERRIT_HTTP_USER", "").strip()
318
- or os.getenv("GERRIT_SSH_USER_G2G", "").strip()
331
+
332
+ # Use pre-resolved credentials if provided
333
+ if credentials is not None and credentials.is_valid:
334
+ log.debug(
335
+ "Using pre-resolved credentials from %s",
336
+ credentials.auth_method_display(),
337
+ )
338
+ auth: tuple[str, str] | None = (
339
+ credentials.username,
340
+ credentials.password,
341
+ )
342
+ return GerritRestClient(
343
+ base_url=base_url,
344
+ auth=auth,
345
+ timeout=timeout,
346
+ max_attempts=max_attempts,
347
+ )
348
+
349
+ # Otherwise, resolve credentials using centralized function
350
+ resolved = resolve_gerrit_credentials(
351
+ host=host,
352
+ explicit_username=http_user,
353
+ explicit_password=http_password,
354
+ use_netrc=use_netrc,
355
+ netrc_file=netrc_file,
356
+ env_username_var="GERRIT_HTTP_USER",
357
+ env_password_var="GERRIT_HTTP_PASSWORD", # noqa: S106
358
+ fallback_env_username_var="GERRIT_SSH_USER_G2G",
359
+ fallback_env_password_var=None,
319
360
  )
320
- passwd = (http_password or "").strip() or os.getenv(
321
- "GERRIT_HTTP_PASSWORD", ""
322
- ).strip()
323
- auth: tuple[str, str] | None = (user, passwd) if user and passwd else None
361
+
362
+ auth = None
363
+ if resolved is not None and resolved.is_valid:
364
+ auth = (resolved.username, resolved.password)
365
+
324
366
  return GerritRestClient(
325
367
  base_url=base_url, auth=auth, timeout=timeout, max_attempts=max_attempts
326
368
  )