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 +30 -18
- github2gerrit/config.py +79 -1
- github2gerrit/core.py +22 -3
- github2gerrit/duplicate_detection.py +4 -2
- github2gerrit/external_api.py +3 -2
- github2gerrit/github_api.py +2 -0
- github2gerrit/gitutils.py +12 -4
- github2gerrit/similarity.py +2 -1
- {github2gerrit-0.1.8.dist-info → github2gerrit-0.1.10.dist-info}/METADATA +28 -6
- github2gerrit-0.1.10.dist-info/RECORD +24 -0
- github2gerrit-0.1.8.dist-info/RECORD +0 -24
- {github2gerrit-0.1.8.dist-info → github2gerrit-0.1.10.dist-info}/WHEEL +0 -0
- {github2gerrit-0.1.8.dist-info → github2gerrit-0.1.10.dist-info}/entry_points.txt +0 -0
- {github2gerrit-0.1.8.dist-info → github2gerrit-0.1.10.dist-info}/licenses/LICENSE +0 -0
- {github2gerrit-0.1.8.dist-info → github2gerrit-0.1.10.dist-info}/top_level.txt +0 -0
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
|
-
|
268
|
+
duplicate_types: str = typer.Option(
|
274
269
|
"open",
|
275
|
-
"--
|
276
|
-
envvar="
|
270
|
+
"--duplicate-types",
|
271
|
+
envvar="DUPLICATE_TYPES",
|
277
272
|
help=(
|
278
|
-
|
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
|
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
|
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
|
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
|
360
|
-
os.environ["
|
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
|
-
|
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("
|
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["
|
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["
|
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
|
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
|
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("
|
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
|
|
github2gerrit/external_api.py
CHANGED
@@ -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
|
|
github2gerrit/github_api.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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:
|
github2gerrit/similarity.py
CHANGED
@@ -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.
|
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:
|
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/
|
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:
|
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
|
-
| `
|
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/
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|