github2gerrit 0.1.8__py3-none-any.whl → 0.1.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
github2gerrit/cli.py CHANGED
@@ -3,13 +3,10 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- import io
7
6
  import json
8
7
  import logging
9
8
  import os
10
- import shutil
11
9
  import tempfile
12
- import zipfile
13
10
  from collections.abc import Callable
14
11
  from concurrent.futures import ThreadPoolExecutor
15
12
  from concurrent.futures import as_completed
@@ -20,8 +17,6 @@ from typing import Protocol
20
17
  from typing import TypeVar
21
18
  from typing import cast
22
19
  from urllib.parse import urlparse
23
- from urllib.request import Request
24
- from urllib.request import urlopen
25
20
 
26
21
  import click
27
22
  import typer
@@ -270,12 +265,13 @@ def main(
270
265
  envvar="CI_TESTING",
271
266
  help="Enable CI testing mode (overrides .gitreview, handles unrelated repos).",
272
267
  ),
273
- duplicates: str = typer.Option(
268
+ duplicate_types: str = typer.Option(
274
269
  "open",
275
- "--duplicates",
276
- envvar="DUPLICATES",
270
+ "--duplicate-types",
271
+ envvar="DUPLICATE_TYPES",
277
272
  help=(
278
- 'Gerrit statuses for duplicate detection (comma-separated). E.g. "open,merged,abandoned". Default: "open".'
273
+ "Gerrit change states to evaluate when determining if a change should be considered a duplicate "
274
+ '(comma-separated). E.g. "open,merged,abandoned". Default: "open".'
279
275
  ),
280
276
  ),
281
277
  normalise_commit: bool = typer.Option(
@@ -295,11 +291,14 @@ def main(
295
291
  """
296
292
  Tool to convert GitHub pull requests into Gerrit changes
297
293
 
298
- - Providing a URL to a pull request: converts that pull request into a Gerrit change
294
+ - Providing a URL to a pull request: converts that pull request
295
+ into a Gerrit change
299
296
 
300
- - Providing a URL to a GitHub repository converts all open pull requests into Gerrit changes
297
+ - Providing a URL to a GitHub repository converts all open pull
298
+ requests into Gerrit changes
301
299
 
302
- - No arguments for CI/CD environment; reads parameters from environment variables
300
+ - No arguments for CI/CD environment; reads parameters from
301
+ environment variables
303
302
  """
304
303
  # Override boolean parameters with properly parsed environment variables
305
304
  # This ensures that string "false" from GitHub Actions is handled correctly
@@ -356,20 +355,26 @@ def main(
356
355
  os.environ["ISSUE_ID"] = issue_id
357
356
  os.environ["ALLOW_DUPLICATES"] = "true" if allow_duplicates else "false"
358
357
  os.environ["CI_TESTING"] = "true" if ci_testing else "false"
359
- if duplicates:
360
- os.environ["DUPLICATES"] = duplicates
358
+ if duplicate_types:
359
+ os.environ["DUPLICATE_TYPES"] = duplicate_types
361
360
  # URL mode handling
362
361
  if target_url:
363
362
  org, repo, pr = _parse_github_target(target_url)
363
+ log.debug("Parsed GitHub URL: org=%s, repo=%s, pr=%s", org, repo, pr)
364
364
  if org:
365
365
  os.environ["ORGANIZATION"] = org
366
+ log.debug("Set ORGANIZATION=%s", org)
366
367
  if org and repo:
367
- os.environ["GITHUB_REPOSITORY"] = f"{org}/{repo}"
368
+ github_repo = f"{org}/{repo}"
369
+ os.environ["GITHUB_REPOSITORY"] = github_repo
370
+ log.debug("Set GITHUB_REPOSITORY=%s", github_repo)
368
371
  if pr:
369
372
  os.environ["PR_NUMBER"] = str(pr)
370
373
  os.environ["SYNC_ALL_OPEN_PRS"] = "false"
374
+ log.debug("Set PR_NUMBER=%s", pr)
371
375
  else:
372
376
  os.environ["SYNC_ALL_OPEN_PRS"] = "true"
377
+ log.debug("Set SYNC_ALL_OPEN_PRS=true")
373
378
  os.environ["G2G_TARGET_URL"] = "1"
374
379
  # Debug: Show environment at CLI startup
375
380
  log.debug("CLI startup environment check:")
@@ -428,7 +433,7 @@ def _build_inputs_from_env() -> Inputs:
428
433
  issue_id=env_str("ISSUE_ID", ""),
429
434
  allow_duplicates=env_bool("ALLOW_DUPLICATES", False),
430
435
  ci_testing=env_bool("CI_TESTING", False),
431
- duplicates_filter=env_str("DUPLICATES", "open"),
436
+ duplicates_filter=env_str("DUPLICATE_TYPES", "open"),
432
437
  )
433
438
 
434
439
 
@@ -472,7 +477,7 @@ def _process_bulk(data: Inputs, gh: GitHubContext) -> bool:
472
477
 
473
478
  try:
474
479
  if data.duplicates_filter:
475
- os.environ["DUPLICATES"] = data.duplicates_filter
480
+ os.environ["DUPLICATE_TYPES"] = data.duplicates_filter
476
481
  check_for_duplicates(per_ctx, allow_duplicates=data.allow_duplicates)
477
482
  except DuplicateChangeError as exc:
478
483
  log_exception_conditionally(log, "Skipping PR #%d", pr_number)
@@ -748,6 +753,13 @@ def _prepare_local_checkout(workspace: Path, gh: GitHubContext, data: Inputs) ->
748
753
 
749
754
  def _fallback_to_api_archive(workspace: Path, gh: GitHubContext, data: Inputs, pr_num_str: str) -> None:
750
755
  """Fallback to GitHub API archive download for private repos."""
756
+ import io
757
+ import json
758
+ import shutil
759
+ import zipfile
760
+ from urllib.request import Request
761
+ from urllib.request import urlopen
762
+
751
763
  log.info("Attempting API archive fallback for PR #%s", pr_num_str)
752
764
 
753
765
  # Get GitHub token for authenticated requests
@@ -1011,7 +1023,7 @@ def _process() -> None:
1011
1023
  if gh.pr_number and not env_bool("SYNC_ALL_OPEN_PRS", False):
1012
1024
  try:
1013
1025
  if data.duplicates_filter:
1014
- os.environ["DUPLICATES"] = data.duplicates_filter
1026
+ os.environ["DUPLICATE_TYPES"] = data.duplicates_filter
1015
1027
  check_for_duplicates(gh, allow_duplicates=data.allow_duplicates)
1016
1028
  except DuplicateChangeError as exc:
1017
1029
  log_exception_conditionally(
github2gerrit/config.py CHANGED
@@ -77,6 +77,7 @@ KNOWN_KEYS: set[str] = {
77
77
  "ALLOW_GHE_URLS",
78
78
  "DRY_RUN",
79
79
  "ALLOW_DUPLICATES",
80
+ "DUPLICATE_TYPES",
80
81
  "ISSUE_ID",
81
82
  "G2G_VERBOSE",
82
83
  "G2G_SKIP_GERRIT_COMMENTS",
@@ -131,6 +132,22 @@ def _coerce_value(raw: str) -> str:
131
132
  # like SSH keys or known_hosts entries can be specified inline using
132
133
  # '\n' or '\r\n' in configuration files.
133
134
  normalized_newlines = unquoted.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\r\n", "\n")
135
+
136
+ # Additional sanitization for SSH private keys
137
+ if ("-----BEGIN" in normalized_newlines and "PRIVATE KEY-----" in normalized_newlines) or (
138
+ "ssh-" in normalized_newlines.lower() and "key" in normalized_newlines.lower()
139
+ ):
140
+ # Clean up SSH key formatting: remove extra whitespace, normalize line endings
141
+ lines = normalized_newlines.split("\n")
142
+ sanitized_lines = []
143
+ for line in lines:
144
+ cleaned = line.strip()
145
+ if cleaned:
146
+ # Remove any stray quotes that might have been embedded in the key content
147
+ cleaned = cleaned.replace('"', "").replace("'", "")
148
+ sanitized_lines.append(cleaned)
149
+ normalized_newlines = "\n".join(sanitized_lines)
150
+
134
151
  b = _normalize_bool_like(normalized_newlines)
135
152
  return b if b is not None else normalized_newlines
136
153
 
@@ -149,7 +166,7 @@ def _select_section(
149
166
 
150
167
  def _load_ini(path: Path) -> configparser.RawConfigParser:
151
168
  cp = configparser.RawConfigParser()
152
- # Preserve option case; mypy requires a cast for attribute assignment
169
+ # Preserve option case; mypy requires a cast for attribute requirement
153
170
  cast(Any, cp).optionxform = str
154
171
  try:
155
172
  with path.open("r", encoding="utf-8") as fh:
@@ -162,6 +179,9 @@ def _load_ini(path: Path) -> configparser.RawConfigParser:
162
179
  # We collapse these into a single line with '\n' escapes so that
163
180
  # configparser can ingest them reliably; later, _coerce_value()
164
181
  # converts the escapes back to real newlines.
182
+ #
183
+ # We also handle SSH private keys and other multi-line values that
184
+ # might have formatting inconsistencies by sanitizing them.
165
185
  lines = raw_text.splitlines()
166
186
  out_lines: list[str] = []
167
187
  i = 0
@@ -171,6 +191,8 @@ def _load_ini(path: Path) -> configparser.RawConfigParser:
171
191
  if eq_idx != -1:
172
192
  left = line[: eq_idx + 1]
173
193
  rhs = line[eq_idx + 1 :].strip()
194
+
195
+ # Handle standard multi-line quoted values: key = "
174
196
  if rhs == '"':
175
197
  i += 1
176
198
  block: list[str] = []
@@ -187,10 +209,64 @@ def _load_ini(path: Path) -> configparser.RawConfigParser:
187
209
  else:
188
210
  # No closing quote found; fall through
189
211
  # and keep original line
212
+ log.debug("Multi-line quote not properly closed for line: %s", line[:50])
190
213
  out_lines.append(line)
191
214
  continue
215
+
216
+ # Handle SSH private keys and other values that start with a quote
217
+ # but contain embedded content that might confuse configparser
218
+ elif rhs.startswith('"') and not rhs.endswith('"'):
219
+ # This looks like a multi-line value that starts on the same line
220
+ # Collect all content until we find a line ending with a quote
221
+ content_lines = [rhs[1:]] # Remove opening quote
222
+ i += 1
223
+
224
+ while i < len(lines):
225
+ current_line = lines[i]
226
+ if current_line.strip().endswith('"') and not current_line.strip().endswith('\\"'):
227
+ # Found closing quote - remove it and add final line
228
+ final_content = current_line.rstrip()
229
+ if final_content.endswith('"'):
230
+ final_content = final_content[:-1]
231
+ if final_content: # Only add if there's content after removing quote
232
+ content_lines.append(final_content)
233
+ break
234
+ else:
235
+ content_lines.append(current_line)
236
+ i += 1
237
+
238
+ # Join all content and sanitize for SSH keys
239
+ full_content = "\\n".join(content_lines)
240
+
241
+ # Special handling for SSH private keys - remove extra whitespace and line breaks
242
+ key_name = left.split("=")[0].strip().upper()
243
+ if "SSH" in key_name and "KEY" in key_name:
244
+ # For SSH keys, clean up base64 content by removing whitespace within lines
245
+ sanitized_lines = []
246
+ for content_line in content_lines:
247
+ cleaned = content_line.strip()
248
+ # Preserve SSH key headers/footers but clean base64 content
249
+ if cleaned.startswith("-----") or not cleaned:
250
+ sanitized_lines.append(cleaned)
251
+ else:
252
+ # Remove any embedded quotes and whitespace from base64 content
253
+ cleaned = cleaned.replace('"', "").replace("'", "").strip()
254
+ if cleaned:
255
+ sanitized_lines.append(cleaned)
256
+ full_content = "\\n".join(sanitized_lines)
257
+
258
+ log.debug(
259
+ "Processed multi-line value for key %s (length: %d)",
260
+ left.split("=")[0].strip(),
261
+ len(full_content),
262
+ )
263
+ out_lines.append(f'{left} "{full_content}"')
264
+ i += 1
265
+ continue
266
+
192
267
  out_lines.append(line)
193
268
  i += 1
269
+
194
270
  preprocessed = "\n".join(out_lines) + ("\n" if out_lines else "")
195
271
  cp.read_string(preprocessed)
196
272
  except FileNotFoundError:
@@ -272,6 +348,8 @@ def load_org_config(
272
348
 
273
349
  # Report unknown configuration keys to help users catch typos
274
350
  unknown_keys = set(normalized.keys()) - KNOWN_KEYS
351
+ log.debug("All parsed keys from config: %s", sorted(normalized.keys()))
352
+ log.debug("Known keys: %s", sorted(KNOWN_KEYS))
275
353
  if unknown_keys:
276
354
  log.warning(
277
355
  "Unknown configuration keys found in [%s]: %s. "
github2gerrit/core.py CHANGED
@@ -30,10 +30,7 @@ import logging
30
30
  import os
31
31
  import re
32
32
  import shlex
33
- import shutil
34
- import socket
35
33
  import stat
36
- import tempfile
37
34
  import urllib.parse
38
35
  import urllib.request
39
36
  from collections.abc import Iterable
@@ -789,6 +786,8 @@ class Orchestrator:
789
786
 
790
787
  def _strategy_open_fchmod(self, key_path: Path, key_content: str) -> None:
791
788
  """Strategy: open with os.open and specific flags, then fchmod."""
789
+ import os
790
+ import stat
792
791
 
793
792
  flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
794
793
  mode = stat.S_IRUSR | stat.S_IWUSR # 0o600
@@ -802,6 +801,7 @@ class Orchestrator:
802
801
 
803
802
  def _strategy_umask_open(self, key_path: Path, key_content: str) -> None:
804
803
  """Strategy: set umask, create file, restore umask."""
804
+ import os
805
805
 
806
806
  original_umask = os.umask(0o077) # Only owner can read/write
807
807
  try:
@@ -813,6 +813,8 @@ class Orchestrator:
813
813
 
814
814
  def _strategy_stat_constants(self, key_path: Path, key_content: str) -> None:
815
815
  """Strategy: use stat constants for permission setting."""
816
+ import os
817
+ import stat
816
818
 
817
819
  with open(key_path, "w", encoding="utf-8") as f:
818
820
  f.write(key_content)
@@ -824,9 +826,13 @@ class Orchestrator:
824
826
 
825
827
  def _create_key_in_memory_fs(self, key_path: Path, key_content: str) -> bool:
826
828
  """Fallback: try to create key in memory filesystem."""
829
+ import shutil
830
+ import tempfile
831
+
827
832
  try:
828
833
  # Try to create in memory filesystem if available
829
834
  # Use secure temporary directories
835
+ import tempfile
830
836
 
831
837
  temp_dir = tempfile.gettempdir()
832
838
  memory_dirs = [temp_dir]
@@ -1411,6 +1417,7 @@ class Orchestrator:
1411
1417
  signed_lines = [ln for ln in lines_cur if ln.startswith("Signed-off-by:")]
1412
1418
  change_id_lines = [ln for ln in lines_cur if ln.startswith("Change-Id:")]
1413
1419
  github_hash_lines = [ln for ln in lines_cur if ln.startswith("GitHub-Hash:")]
1420
+ github_pr_lines = [ln for ln in lines_cur if ln.startswith("GitHub-PR:")]
1414
1421
 
1415
1422
  msg_parts = [title, "", body] if title or body else [current_body]
1416
1423
  commit_message = "\n".join(msg_parts).strip()
@@ -1440,8 +1447,19 @@ class Orchestrator:
1440
1447
  seen_so.add(ln)
1441
1448
  if change_id_lines:
1442
1449
  trailers_out.append(change_id_lines[-1])
1450
+
1451
+ # GitHub-PR trailer (must appear after Change-Id)
1452
+ if github_pr_lines:
1453
+ pr_line = github_pr_lines[-1]
1454
+ else:
1455
+ pr_line = f"GitHub-PR: {gh.server_url}/{gh.repository}/pull/{gh.pr_number}" if gh.pr_number else ""
1456
+
1443
1457
  if trailers_out:
1444
1458
  commit_message += "\n\n" + "\n".join(trailers_out)
1459
+ if pr_line:
1460
+ commit_message += "\n" + pr_line
1461
+ elif pr_line:
1462
+ commit_message += "\n\n" + pr_line
1445
1463
 
1446
1464
  author = run_cmd(
1447
1465
  ["git", "show", "-s", "--pretty=format:%an <%ae>", "HEAD"],
@@ -2337,6 +2355,7 @@ class Orchestrator:
2337
2355
  - Verify GitHub token by fetching repository and PR metadata
2338
2356
  - Do NOT perform any write operations
2339
2357
  """
2358
+ import socket
2340
2359
 
2341
2360
  log.info("Dry-run: starting preflight checks")
2342
2361
  if os.getenv("G2G_DRYRUN_DISABLE_NETWORK", "").strip().lower() in (
@@ -289,6 +289,8 @@ class DuplicateDetector:
289
289
  Returns:
290
290
  Hex-encoded SHA256 hash string (first 16 characters for readability)
291
291
  """
292
+ import hashlib
293
+
292
294
  # Build hash input from stable, unique PR identifiers
293
295
  # Use server_url + repository + pr_number for global uniqueness
294
296
  hash_input = f"{gh.server_url}/{gh.repository}/pull/{gh.pr_number}"
@@ -371,7 +373,7 @@ class DuplicateDetector:
371
373
  q_parts = []
372
374
  if gerrit_project:
373
375
  q_parts.append(f"project:{gerrit_project}")
374
- # Build status clause from DUPLICATES filter (default: open)
376
+ # Build status clause from DUPLICATE_TYPES filter (default: open)
375
377
  dup_filter = (self.duplicates_filter or "open").strip().lower()
376
378
  selected = [s.strip() for s in dup_filter.split(",") if s.strip()]
377
379
  valid = {
@@ -642,7 +644,7 @@ def check_for_duplicates(
642
644
  detector = DuplicateDetector(
643
645
  repo,
644
646
  lookback_days=lookback_days,
645
- duplicates_filter=os.getenv("DUPLICATES", "open"),
647
+ duplicates_filter=os.getenv("DUPLICATE_TYPES", "open"),
646
648
  )
647
649
  detector.check_for_duplicates(target_pr, allow_duplicates=allow_duplicates, gh=gh)
648
650
 
@@ -29,14 +29,12 @@ import functools
29
29
  import logging
30
30
  import random
31
31
  import socket
32
- import subprocess
33
32
  import time
34
33
  import urllib.error
35
34
  from collections.abc import Callable
36
35
  from dataclasses import dataclass
37
36
  from dataclasses import field
38
37
  from enum import Enum
39
- from pathlib import Path
40
38
  from typing import Any
41
39
  from typing import NoReturn
42
40
  from typing import TypeVar
@@ -443,6 +441,9 @@ def curl_download(
443
441
  Raises:
444
442
  RuntimeError: If curl command fails after retries
445
443
  """
444
+ import subprocess
445
+ from pathlib import Path
446
+
446
447
  if policy is None:
447
448
  policy = RetryPolicy(max_attempts=3, timeout=timeout)
448
449
 
@@ -171,7 +171,9 @@ def build_client(token: str | None = None) -> GhClient:
171
171
  def get_repo_from_env(client: GhClient) -> GhRepository:
172
172
  """Return the repository object based on GITHUB_REPOSITORY."""
173
173
  full = _getenv_str("GITHUB_REPOSITORY")
174
+ log.debug("GITHUB_REPOSITORY environment variable: '%s'", full)
174
175
  if not full or "/" not in full:
176
+ log.error("Invalid GITHUB_REPOSITORY: '%s' (expected format: 'owner/repo')", full)
175
177
  raise ValueError(_MSG_BAD_GITHUB_REPOSITORY)
176
178
  repo = client.get_repo(full)
177
179
  return repo
github2gerrit/gitutils.py CHANGED
@@ -11,10 +11,8 @@ from __future__ import annotations
11
11
 
12
12
  import logging
13
13
  import os
14
- import re
15
14
  import shlex
16
15
  import subprocess
17
- import tempfile
18
16
  import time
19
17
  from collections.abc import Callable
20
18
  from collections.abc import Iterable
@@ -483,7 +481,9 @@ def git_commit_amend(
483
481
  # Write message to a temp file to avoid shell-escaping issues
484
482
  tmp_path: Path | None = None
485
483
  if message is not None:
486
- with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as _tf:
484
+ import tempfile as _tempfile
485
+
486
+ with _tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as _tf:
487
487
  _tf.write(message)
488
488
  _tf.flush()
489
489
  tmp_path = Path(_tf.name)
@@ -493,6 +493,9 @@ def git_commit_amend(
493
493
  # Determine whether to add -s; only suppress if message already has a sign-off for current committer
494
494
  effective_signoff = bool(signoff)
495
495
  try:
496
+ import os
497
+ import re
498
+
496
499
  # Resolve committer email (prefer repo-local; fallback to global/env)
497
500
  committer_email = os.getenv("GIT_COMMITTER_EMAIL", "").strip()
498
501
  if not committer_email:
@@ -583,7 +586,9 @@ def git_commit_new(
583
586
  # Write message to a temp file to avoid shell-escaping issues
584
587
  tmp_path: Path | None = None
585
588
  if message is not None:
586
- with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as _tf:
589
+ import tempfile as _tempfile
590
+
591
+ with _tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as _tf:
587
592
  _tf.write(message)
588
593
  _tf.flush()
589
594
  tmp_path = Path(_tf.name)
@@ -593,6 +598,9 @@ def git_commit_new(
593
598
  # Determine whether to add -s; only suppress if message already has a sign-off for current committer
594
599
  effective_signoff = bool(signoff)
595
600
  try:
601
+ import os
602
+ import re
603
+
596
604
  # Resolve committer email (prefer repo-local; fallback to global/env)
597
605
  committer_email = os.getenv("GIT_COMMITTER_EMAIL", "").strip()
598
606
  if not committer_email:
@@ -163,6 +163,7 @@ def remove_commit_trailers(message: str) -> str:
163
163
  - Signed-off-by: Name <email>
164
164
  - Issue-ID: ABC-123
165
165
  - GitHub-Hash: deadbeefcafebabe
166
+ - GitHub-PR: https://github.com/org/repo/pull/123
166
167
  - Co-authored-by: ...
167
168
 
168
169
  Args:
@@ -173,7 +174,7 @@ def remove_commit_trailers(message: str) -> str:
173
174
  """
174
175
  lines = (message or "").splitlines()
175
176
  out: list[str] = []
176
- trailer_re = re.compile(r"(?i)^(change-id|signed-off-by|issue-id|github-hash|co-authored-by):")
177
+ trailer_re = re.compile(r"(?i)^(change-id|signed-off-by|issue-id|github-hash|github-pr|co-authored-by):")
177
178
  for ln in lines:
178
179
  if trailer_re.match(ln.strip()):
179
180
  continue
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github2gerrit
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: Submit a GitHub pull request to a Gerrit repository.
5
5
  Author-email: Matthew Watkins <mwatkins@linuxfoundation.org>
6
6
  License-Expression: Apache-2.0
@@ -140,6 +140,27 @@ with:
140
140
  When allowed, duplicates generate warnings but processing continues.
141
141
  The tool exits with code 3 when it detects duplicates and they are not allowed.
142
142
 
143
+ ### Configuring duplicate detection scope
144
+
145
+ By default, the duplicate detector considers changes with status `open` when searching for potential duplicates.
146
+ You can customize which Gerrit change states to check using `--duplicate-types` or setting `DUPLICATE_TYPES`:
147
+
148
+ ```bash
149
+ # CLI usage - check against open and merged changes
150
+ github2gerrit --duplicate-types=open,merged https://github.com/org/repo
151
+
152
+ # Environment variable
153
+ DUPLICATE_TYPES=open,merged,abandoned github2gerrit https://github.com/org/repo
154
+
155
+ # GitHub Actions
156
+ uses: onap/github2gerrit@main
157
+ with:
158
+ DUPLICATE_TYPES: 'open,merged'
159
+ ```
160
+
161
+ Valid change states include `open`, `merged`, and `abandoned`. This setting determines which existing changes
162
+ to check when evaluating whether a new change would be a duplicate.
163
+
143
164
  ## Commit Message Normalization
144
165
 
145
166
  The tool includes intelligent commit message normalization that automatically
@@ -297,7 +318,7 @@ jobs:
297
318
  steps:
298
319
  - name: Submit PR to Gerrit
299
320
  id: g2g
300
- uses: lfit/github2gerrit@main
321
+ uses: lfreleng-actions/github2gerrit-action@main
301
322
  with:
302
323
  SUBMIT_SINGLE_COMMITS: "false"
303
324
  USE_PR_AS_COMMIT: "false"
@@ -343,7 +364,7 @@ uv run github2gerrit
343
364
  uvx github2gerrit https://github.com/owner/repo/pull/123
344
365
 
345
366
  # Install from specific version/source
346
- uvx --from git+https://github.com/lfit/github2gerrit@main github2gerrit https://github.com/owner/repo/pull/123
367
+ uvx --from git+https://github.com/lfreleng-actions/github2gerrit-action@main github2gerrit https://github.com/owner/repo/pull/123
347
368
  ```
348
369
 
349
370
  ### Available Options
@@ -368,6 +389,7 @@ Key options include:
368
389
  - `--use-pr-as-commit`: Use PR title/body as commit message (`USE_PR_AS_COMMIT`)
369
390
  - `--issue-id`: Add an Issue-ID trailer (e.g., "Issue-ID: ABC-123") to the commit message (`ISSUE_ID`)
370
391
  - `--preserve-github-prs`: Don't close GitHub PRs after submission (`PRESERVE_GITHUB_PRS`)
392
+ - `--duplicate-types`: Configure which Gerrit change states to check for duplicates (`DUPLICATE_TYPES`)
371
393
 
372
394
  For a complete list of all available options, see the [Inputs](#inputs) section.
373
395
 
@@ -524,7 +546,7 @@ jobs:
524
546
 
525
547
  - name: Submit PR to Gerrit (with explicit overrides)
526
548
  id: g2g
527
- uses: lfit/github2gerrit@main
549
+ uses: lfreleng-actions/github2gerrit-action@main
528
550
  with:
529
551
  # Behavior
530
552
  SUBMIT_SINGLE_COMMITS: "false"
@@ -593,7 +615,7 @@ alignment between action inputs, environment variables, and CLI flags:
593
615
  | `CI_TESTING` | `CI_TESTING` | `--ci-testing` | No | `"false"` | Enable CI testing mode (overrides .gitreview) |
594
616
  | `ISSUE_ID` | `ISSUE_ID` | `--issue-id` | No | `""` | Issue ID to include (e.g., ABC-123) |
595
617
  | `G2G_USE_SSH_AGENT` | `G2G_USE_SSH_AGENT` | N/A | No | `"true"` | Use SSH agent for authentication |
596
- | `DUPLICATES` | `DUPLICATES` | `--duplicates` | No | `"open"` | Comma-separated Gerrit statuses for duplicate detection |
618
+ | `DUPLICATE_TYPES` | `DUPLICATE_TYPES` | `--duplicate-types` | No | `"open"` | Comma-separated Gerrit change states to check for duplicate detection |
597
619
  | `GERRIT_SERVER` | `GERRIT_SERVER` | `--gerrit-server` | No² | `""` | Gerrit server hostname (auto-derived if enabled) |
598
620
  | `GERRIT_SERVER_PORT` | `GERRIT_SERVER_PORT` | `--gerrit-server-port` | No | `"29418"` | Gerrit SSH port |
599
621
  | `GERRIT_PROJECT` | `GERRIT_PROJECT` | `--gerrit-project` | No² | `""` | Gerrit project name |
@@ -777,7 +799,7 @@ For CI/CD pipelines (like GitHub Actions), use `uvx` to install and run without
777
799
  uvx github2gerrit <PR_URL> --dry-run
778
800
 
779
801
  # Install from a specific version or source
780
- uvx --from git+https://github.com/lfit/github2gerrit@main github2gerrit <PR_URL>
802
+ uvx --from git+https://github.com/lfreleng-actions/github2gerrit-action@main github2gerrit <PR_URL>
781
803
 
782
804
  # Run with specific Python version
783
805
  uvx --python 3.11 github2gerrit <PR_URL>
@@ -0,0 +1,24 @@
1
+ github2gerrit/__init__.py,sha256=N1Vj1HJ28LKCJLAynQdm5jFGQQAz9YSMzZhEfvbBgow,886
2
+ github2gerrit/cli.py,sha256=WlEOfomSxXOvSbN58ti2J-R4PA9_ZV6ZbyHQDC_JUQs,45980
3
+ github2gerrit/commit_normalization.py,sha256=u4AZigz3qOpz5XYpUOq3WUqsY-o08YrkgaT160eyIIs,16594
4
+ github2gerrit/config.py,sha256=Jxo2q9qZZwEWc_kLYnO_hJ9ToSthYlyYxQnmN_yJRcY,21494
5
+ github2gerrit/core.py,sha256=4BDh4jm7gjDcVEIrY_3xkrMomDypdp8KYKRdkjugEGs,105660
6
+ github2gerrit/duplicate_detection.py,sha256=_CHVCaxqLoDBuk3wq2SIsekPtqRzmOwCZTQfe2ZbVgI,26554
7
+ github2gerrit/external_api.py,sha256=EVHh__v6lRq_ojpBI_nkGZ31AQSZ9NG8tDqVUid23XY,17638
8
+ github2gerrit/gerrit_rest.py,sha256=NmC95hb2hLpnko8Uu3OwPEKktNv-k3qrmuA7A2q-H0Y,10562
9
+ github2gerrit/gerrit_urls.py,sha256=pw4rjIQeQ-i-vbPqsOZjZpdz7FO03pnMUMWc8L9Mcic,12893
10
+ github2gerrit/github_api.py,sha256=6S9DRt-Dza78cgxQwZ3_ujI0yPPozDGBIKnYgHan760,8388
11
+ github2gerrit/gitutils.py,sha256=3ob1K7dlKeWYYYOPXIIJ56p2N49lm1yj8XHHg2zSlhw,25929
12
+ github2gerrit/models.py,sha256=sRS4rPMF_wjV19HMFTzhHXFMfsLFzm9TFb1JydJsSyQ,1875
13
+ github2gerrit/pr_content_filter.py,sha256=w4MqSZ4uLX-3Da1DL_A56zVQa7xJ4h5jHMNteOOG2AE,16896
14
+ github2gerrit/similarity.py,sha256=Gf8xDQzDDsgauhYNGueUFaQRnzt7PJ27AaLVo-Bpa7M,15332
15
+ github2gerrit/ssh_agent_setup.py,sha256=fvKuMdF9Hv5ioaSu_alzLcjeoQLGGFRLS2wb46KdNzE,11889
16
+ github2gerrit/ssh_common.py,sha256=f0QQmnj6yIW74N3ZjGkYmJryEstjEOmq41EIXOZdGTM,8102
17
+ github2gerrit/ssh_discovery.py,sha256=zH0tX9VN1pn8DLM1J9QQquoKBhA9gk5drXHvMSHNNLg,13021
18
+ github2gerrit/utils.py,sha256=ADE5YZe1h3PB3cgfxbsCuYU-Xs-1CMHrDHxfIdariFE,3369
19
+ github2gerrit-0.1.10.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
20
+ github2gerrit-0.1.10.dist-info/METADATA,sha256=D3HNkThf9wbgtqo-K0SxaaT0DLaLW20fA2CMFFi6vn4,31914
21
+ github2gerrit-0.1.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
+ github2gerrit-0.1.10.dist-info/entry_points.txt,sha256=MxN2_liIKo3-xJwtAulAeS5GcOS6JS96nvwOQIkP3W8,56
23
+ github2gerrit-0.1.10.dist-info/top_level.txt,sha256=bWTYXjvuu4sSU90KLT1JlnjD7xV_iXZ-vKoulpjLTy8,14
24
+ github2gerrit-0.1.10.dist-info/RECORD,,
@@ -1,24 +0,0 @@
1
- github2gerrit/__init__.py,sha256=N1Vj1HJ28LKCJLAynQdm5jFGQQAz9YSMzZhEfvbBgow,886
2
- github2gerrit/cli.py,sha256=j2jdsfO3WZqHF0UIINVWJrPzMUHtIwzGKmbAhvP1Q3c,45483
3
- github2gerrit/commit_normalization.py,sha256=u4AZigz3qOpz5XYpUOq3WUqsY-o08YrkgaT160eyIIs,16594
4
- github2gerrit/config.py,sha256=sTiujlOjCsJbaA-Ftr5XR--9nxs7XH8uEWD-IbGqLYk,17370
5
- github2gerrit/core.py,sha256=xSUBhB_xOpCrVbnNu2m1QqKrZJTa8zJrmEK28-ehqPg,105033
6
- github2gerrit/duplicate_detection.py,sha256=enzLMsMQOeB5yU_iEOfTN5oGuZ0LUZNENsQ4fFnKKRg,26520
7
- github2gerrit/external_api.py,sha256=ypMKGIsCAYTvP2u55l8X5eUC88T6EKvyZvlsAWRFVcU,17629
8
- github2gerrit/gerrit_rest.py,sha256=NmC95hb2hLpnko8Uu3OwPEKktNv-k3qrmuA7A2q-H0Y,10562
9
- github2gerrit/gerrit_urls.py,sha256=pw4rjIQeQ-i-vbPqsOZjZpdz7FO03pnMUMWc8L9Mcic,12893
10
- github2gerrit/github_api.py,sha256=1cTZMunDx6_oLNR3Z__ya6vsw0_PFS5ww3jQla5SVFA,8229
11
- github2gerrit/gitutils.py,sha256=7E-bYux4E-5IJMSxYx8U0sKzD-wkn3QyEfZ7SGHkGrY,25803
12
- github2gerrit/models.py,sha256=sRS4rPMF_wjV19HMFTzhHXFMfsLFzm9TFb1JydJsSyQ,1875
13
- github2gerrit/pr_content_filter.py,sha256=w4MqSZ4uLX-3Da1DL_A56zVQa7xJ4h5jHMNteOOG2AE,16896
14
- github2gerrit/similarity.py,sha256=xPqdypI-Fmpeb74K4lcqZBxj17i8avS_x73dXckeicA,15268
15
- github2gerrit/ssh_agent_setup.py,sha256=fvKuMdF9Hv5ioaSu_alzLcjeoQLGGFRLS2wb46KdNzE,11889
16
- github2gerrit/ssh_common.py,sha256=f0QQmnj6yIW74N3ZjGkYmJryEstjEOmq41EIXOZdGTM,8102
17
- github2gerrit/ssh_discovery.py,sha256=zH0tX9VN1pn8DLM1J9QQquoKBhA9gk5drXHvMSHNNLg,13021
18
- github2gerrit/utils.py,sha256=ADE5YZe1h3PB3cgfxbsCuYU-Xs-1CMHrDHxfIdariFE,3369
19
- github2gerrit-0.1.8.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
20
- github2gerrit-0.1.8.dist-info/METADATA,sha256=XMojs1cbwj9CPacFK6YDE3TczE5s4tE4YFPKZXGUUIY,30930
21
- github2gerrit-0.1.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
- github2gerrit-0.1.8.dist-info/entry_points.txt,sha256=MxN2_liIKo3-xJwtAulAeS5GcOS6JS96nvwOQIkP3W8,56
23
- github2gerrit-0.1.8.dist-info/top_level.txt,sha256=bWTYXjvuu4sSU90KLT1JlnjD7xV_iXZ-vKoulpjLTy8,14
24
- github2gerrit-0.1.8.dist-info/RECORD,,