github2gerrit 0.1.5__py3-none-any.whl → 0.1.7__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 +511 -271
- github2gerrit/commit_normalization.py +471 -0
- github2gerrit/config.py +32 -24
- github2gerrit/core.py +1092 -507
- github2gerrit/duplicate_detection.py +333 -217
- github2gerrit/external_api.py +518 -0
- github2gerrit/gerrit_rest.py +298 -0
- github2gerrit/gerrit_urls.py +353 -0
- github2gerrit/github_api.py +17 -95
- github2gerrit/gitutils.py +225 -41
- github2gerrit/models.py +3 -0
- github2gerrit/pr_content_filter.py +476 -0
- github2gerrit/similarity.py +458 -0
- github2gerrit/ssh_agent_setup.py +351 -0
- github2gerrit/ssh_common.py +244 -0
- github2gerrit/ssh_discovery.py +24 -67
- github2gerrit/utils.py +113 -0
- github2gerrit-0.1.7.dist-info/METADATA +798 -0
- github2gerrit-0.1.7.dist-info/RECORD +24 -0
- github2gerrit-0.1.5.dist-info/METADATA +0 -555
- github2gerrit-0.1.5.dist-info/RECORD +0 -15
- {github2gerrit-0.1.5.dist-info → github2gerrit-0.1.7.dist-info}/WHEEL +0 -0
- {github2gerrit-0.1.5.dist-info → github2gerrit-0.1.7.dist-info}/entry_points.txt +0 -0
- {github2gerrit-0.1.5.dist-info → github2gerrit-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {github2gerrit-0.1.5.dist-info → github2gerrit-0.1.7.dist-info}/top_level.txt +0 -0
github2gerrit/ssh_discovery.py
CHANGED
@@ -16,6 +16,8 @@ import os
|
|
16
16
|
import socket
|
17
17
|
from pathlib import Path
|
18
18
|
|
19
|
+
from .external_api import ApiType
|
20
|
+
from .external_api import external_api_call
|
19
21
|
from .gitutils import CommandError
|
20
22
|
from .gitutils import run_cmd
|
21
23
|
|
@@ -28,31 +30,17 @@ class SSHDiscoveryError(Exception):
|
|
28
30
|
|
29
31
|
|
30
32
|
# Error message constants to comply with TRY003
|
31
|
-
_MSG_HOST_UNREACHABLE =
|
32
|
-
"Host {hostname}:{port} is not reachable. "
|
33
|
-
"Check network connectivity and server availability."
|
34
|
-
)
|
33
|
+
_MSG_HOST_UNREACHABLE = "Host {hostname}:{port} is not reachable. Check network connectivity and server availability."
|
35
34
|
_MSG_NO_KEYS_FOUND = (
|
36
|
-
"No SSH host keys found for {hostname}:{port}. "
|
37
|
-
"The server may not be running SSH or may be blocking connections."
|
35
|
+
"No SSH host keys found for {hostname}:{port}. The server may not be running SSH or may be blocking connections."
|
38
36
|
)
|
39
37
|
_MSG_NO_VALID_KEYS = (
|
40
|
-
"No valid SSH host keys found for {hostname}:{port}. "
|
41
|
-
"The ssh-keyscan output was empty or malformed."
|
42
|
-
)
|
43
|
-
_MSG_CONNECTION_FAILED = (
|
44
|
-
"Failed to connect to {hostname}:{port} for SSH key discovery. "
|
45
|
-
"Error: {error}"
|
46
|
-
)
|
47
|
-
_MSG_KEYSCAN_FAILED = (
|
48
|
-
"ssh-keyscan failed with return code {returncode}: {error}"
|
49
|
-
)
|
50
|
-
_MSG_UNEXPECTED_ERROR = (
|
51
|
-
"Unexpected error during SSH key discovery for {hostname}:{port}: {error}"
|
52
|
-
)
|
53
|
-
_MSG_SAVE_FAILED = (
|
54
|
-
"Failed to save host keys to configuration file {config_file}: {error}"
|
38
|
+
"No valid SSH host keys found for {hostname}:{port}. The ssh-keyscan output was empty or malformed."
|
55
39
|
)
|
40
|
+
_MSG_CONNECTION_FAILED = "Failed to connect to {hostname}:{port} for SSH key discovery. Error: {error}"
|
41
|
+
_MSG_KEYSCAN_FAILED = "ssh-keyscan failed with return code {returncode}: {error}"
|
42
|
+
_MSG_UNEXPECTED_ERROR = "Unexpected error during SSH key discovery for {hostname}:{port}: {error}"
|
43
|
+
_MSG_SAVE_FAILED = "Failed to save host keys to configuration file {config_file}: {error}"
|
56
44
|
|
57
45
|
|
58
46
|
def is_host_reachable(hostname: str, port: int, timeout: int = 5) -> bool:
|
@@ -64,9 +52,8 @@ def is_host_reachable(hostname: str, port: int, timeout: int = 5) -> bool:
|
|
64
52
|
return False
|
65
53
|
|
66
54
|
|
67
|
-
|
68
|
-
|
69
|
-
) -> str:
|
55
|
+
@external_api_call(ApiType.SSH, "fetch_ssh_host_keys")
|
56
|
+
def fetch_ssh_host_keys(hostname: str, port: int = 22, timeout: int = 10) -> str:
|
70
57
|
"""
|
71
58
|
Fetch SSH host keys for a given hostname and port using ssh-keyscan.
|
72
59
|
|
@@ -85,9 +72,7 @@ def fetch_ssh_host_keys(
|
|
85
72
|
|
86
73
|
# First check if the host is reachable
|
87
74
|
if not is_host_reachable(hostname, port, timeout=5):
|
88
|
-
raise SSHDiscoveryError(
|
89
|
-
_MSG_HOST_UNREACHABLE.format(hostname=hostname, port=port)
|
90
|
-
)
|
75
|
+
raise SSHDiscoveryError(_MSG_HOST_UNREACHABLE.format(hostname=hostname, port=port))
|
91
76
|
|
92
77
|
try:
|
93
78
|
# Use ssh-keyscan to fetch all available key types
|
@@ -142,23 +127,13 @@ def fetch_ssh_host_keys(
|
|
142
127
|
# ssh-keyscan returns 1 when it can't connect
|
143
128
|
error_msg = exc.stderr or exc.stdout or "Connection failed"
|
144
129
|
raise SSHDiscoveryError(
|
145
|
-
_MSG_CONNECTION_FAILED.format(
|
146
|
-
hostname=hostname, port=port, error=error_msg
|
147
|
-
)
|
130
|
+
_MSG_CONNECTION_FAILED.format(hostname=hostname, port=port, error=error_msg)
|
148
131
|
) from exc
|
149
132
|
else:
|
150
133
|
error_msg = exc.stderr or exc.stdout or "Unknown error"
|
151
|
-
raise SSHDiscoveryError(
|
152
|
-
_MSG_KEYSCAN_FAILED.format(
|
153
|
-
returncode=exc.returncode, error=error_msg
|
154
|
-
)
|
155
|
-
) from exc
|
134
|
+
raise SSHDiscoveryError(_MSG_KEYSCAN_FAILED.format(returncode=exc.returncode, error=error_msg)) from exc
|
156
135
|
except Exception as exc:
|
157
|
-
raise SSHDiscoveryError(
|
158
|
-
_MSG_UNEXPECTED_ERROR.format(
|
159
|
-
hostname=hostname, port=port, error=exc
|
160
|
-
)
|
161
|
-
) from exc
|
136
|
+
raise SSHDiscoveryError(_MSG_UNEXPECTED_ERROR.format(hostname=hostname, port=port, error=exc)) from exc
|
162
137
|
else:
|
163
138
|
return discovered_keys
|
164
139
|
|
@@ -196,9 +171,8 @@ def extract_gerrit_info_from_gitreview(content: str) -> tuple[str, int] | None:
|
|
196
171
|
return (hostname, port) if hostname else None
|
197
172
|
|
198
173
|
|
199
|
-
|
200
|
-
|
201
|
-
) -> str:
|
174
|
+
@external_api_call(ApiType.SSH, "discover_and_save_host_keys")
|
175
|
+
def discover_and_save_host_keys(hostname: str, port: int, organization: str, config_path: str | None = None) -> str:
|
202
176
|
"""
|
203
177
|
Discover SSH host keys and save them to the organization's configuration.
|
204
178
|
|
@@ -224,9 +198,7 @@ def discover_and_save_host_keys(
|
|
224
198
|
return host_keys
|
225
199
|
|
226
200
|
|
227
|
-
def save_host_keys_to_config(
|
228
|
-
host_keys: str, organization: str, config_path: str | None = None
|
229
|
-
) -> None:
|
201
|
+
def save_host_keys_to_config(host_keys: str, organization: str, config_path: str | None = None) -> None:
|
230
202
|
"""
|
231
203
|
Save SSH host keys to the organization's configuration file.
|
232
204
|
|
@@ -242,9 +214,7 @@ def save_host_keys_to_config(
|
|
242
214
|
from .config import DEFAULT_CONFIG_PATH
|
243
215
|
|
244
216
|
if config_path is None:
|
245
|
-
config_path = (
|
246
|
-
os.getenv("G2G_CONFIG_PATH", "").strip() or DEFAULT_CONFIG_PATH
|
247
|
-
)
|
217
|
+
config_path = os.getenv("G2G_CONFIG_PATH", "").strip() or DEFAULT_CONFIG_PATH
|
248
218
|
|
249
219
|
config_file = Path(config_path).expanduser()
|
250
220
|
|
@@ -313,9 +283,7 @@ def save_host_keys_to_config(
|
|
313
283
|
|
314
284
|
# Insert the GERRIT_KNOWN_HOSTS entry
|
315
285
|
escaped_keys = host_keys.replace("\n", "\\n")
|
316
|
-
new_lines.insert(
|
317
|
-
section_end, f'GERRIT_KNOWN_HOSTS = "{escaped_keys}"'
|
318
|
-
)
|
286
|
+
new_lines.insert(section_end, f'GERRIT_KNOWN_HOSTS = "{escaped_keys}"')
|
319
287
|
|
320
288
|
# Write the updated configuration
|
321
289
|
config_file.write_text("\n".join(new_lines), encoding="utf-8")
|
@@ -327,9 +295,7 @@ def save_host_keys_to_config(
|
|
327
295
|
)
|
328
296
|
|
329
297
|
except Exception as exc:
|
330
|
-
raise SSHDiscoveryError(
|
331
|
-
_MSG_SAVE_FAILED.format(config_file=config_file, error=exc)
|
332
|
-
) from exc
|
298
|
+
raise SSHDiscoveryError(_MSG_SAVE_FAILED.format(config_file=config_file, error=exc)) from exc
|
333
299
|
|
334
300
|
|
335
301
|
def auto_discover_gerrit_host_keys(
|
@@ -360,21 +326,14 @@ def auto_discover_gerrit_host_keys(
|
|
360
326
|
gerrit_port = 29418
|
361
327
|
|
362
328
|
if organization is None:
|
363
|
-
organization = (
|
364
|
-
os.getenv("ORGANIZATION")
|
365
|
-
or os.getenv("GITHUB_REPOSITORY_OWNER")
|
366
|
-
or ""
|
367
|
-
).strip()
|
329
|
+
organization = (os.getenv("ORGANIZATION") or os.getenv("GITHUB_REPOSITORY_OWNER") or "").strip()
|
368
330
|
|
369
331
|
if not gerrit_hostname:
|
370
332
|
log.debug("No Gerrit hostname provided for auto-discovery")
|
371
333
|
return None
|
372
334
|
|
373
335
|
if not organization:
|
374
|
-
log.warning(
|
375
|
-
"No organization specified for SSH host key auto-discovery. "
|
376
|
-
"Cannot save to configuration file."
|
377
|
-
)
|
336
|
+
log.warning("No organization specified for SSH host key auto-discovery. Cannot save to configuration file.")
|
378
337
|
save_to_config = False
|
379
338
|
|
380
339
|
log.info(
|
@@ -404,9 +363,7 @@ def auto_discover_gerrit_host_keys(
|
|
404
363
|
log.warning("SSH host key auto-discovery failed: %s", exc)
|
405
364
|
return None
|
406
365
|
except Exception as exc:
|
407
|
-
log.warning(
|
408
|
-
"Unexpected error during SSH host key auto-discovery: %s", exc
|
409
|
-
)
|
366
|
+
log.warning("Unexpected error during SSH host key auto-discovery: %s", exc)
|
410
367
|
return None
|
411
368
|
else:
|
412
369
|
return host_keys
|
github2gerrit/utils.py
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
2
|
+
# SPDX-FileCopyrightText: 2025 The Linux Foundation
|
3
|
+
|
4
|
+
"""Common utilities used across multiple modules.
|
5
|
+
|
6
|
+
This module consolidates helper functions that were previously duplicated
|
7
|
+
across cli.py, core.py, and gitutils.py to reduce maintenance overhead
|
8
|
+
and ensure consistent behavior.
|
9
|
+
"""
|
10
|
+
|
11
|
+
import logging
|
12
|
+
import os
|
13
|
+
from typing import Any
|
14
|
+
|
15
|
+
|
16
|
+
def env_bool(name: str, default: bool = False) -> bool:
|
17
|
+
"""Parse boolean environment variable correctly handling string values.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
name: Environment variable name
|
21
|
+
default: Default value if variable is not set
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
Boolean value parsed from environment variable
|
25
|
+
"""
|
26
|
+
val = os.getenv(name)
|
27
|
+
if val is None:
|
28
|
+
return default
|
29
|
+
s = val.strip().lower()
|
30
|
+
return s in ("1", "true", "yes", "on")
|
31
|
+
|
32
|
+
|
33
|
+
def env_str(name: str, default: str = "") -> str:
|
34
|
+
"""Get string environment variable with default fallback.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
name: Environment variable name
|
38
|
+
default: Default value if variable is not set
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
String value from environment variable or default
|
42
|
+
"""
|
43
|
+
val = os.getenv(name)
|
44
|
+
return val if val is not None else default
|
45
|
+
|
46
|
+
|
47
|
+
def parse_bool_env(value: str | None) -> bool:
|
48
|
+
"""Parse boolean environment variable correctly handling string values.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
value: String value to parse as boolean
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
Boolean value parsed from string
|
55
|
+
"""
|
56
|
+
if value is None:
|
57
|
+
return False
|
58
|
+
s = value.strip().lower()
|
59
|
+
return s in ("1", "true", "yes", "on")
|
60
|
+
|
61
|
+
|
62
|
+
def is_verbose_mode() -> bool:
|
63
|
+
"""Check if verbose mode is enabled via environment variable.
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
True if G2G_VERBOSE environment variable is set to a truthy value
|
67
|
+
"""
|
68
|
+
return os.getenv("G2G_VERBOSE", "").lower() in ("true", "1", "yes")
|
69
|
+
|
70
|
+
|
71
|
+
def log_exception_conditionally(logger: logging.Logger, message: str, *args: Any) -> None:
|
72
|
+
"""Log exception with traceback only if verbose mode is enabled.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
logger: Logger instance to use
|
76
|
+
message: Log message format string
|
77
|
+
*args: Arguments for message formatting
|
78
|
+
"""
|
79
|
+
if is_verbose_mode():
|
80
|
+
logger.exception(message, *args)
|
81
|
+
else:
|
82
|
+
logger.error(message, *args)
|
83
|
+
|
84
|
+
|
85
|
+
def append_github_output(outputs: dict[str, str]) -> None:
|
86
|
+
"""Append key-value pairs to GitHub Actions output file.
|
87
|
+
|
88
|
+
This function writes outputs to the GITHUB_OUTPUT file for use by
|
89
|
+
subsequent steps in a GitHub Actions workflow. It handles multiline
|
90
|
+
values using heredoc syntax when running in GitHub Actions.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
outputs: Dictionary of key-value pairs to write to output
|
94
|
+
"""
|
95
|
+
gh_out = os.getenv("GITHUB_OUTPUT")
|
96
|
+
if not gh_out:
|
97
|
+
return
|
98
|
+
|
99
|
+
try:
|
100
|
+
with open(gh_out, "a", encoding="utf-8") as fh:
|
101
|
+
for key, val in outputs.items():
|
102
|
+
if not val:
|
103
|
+
continue
|
104
|
+
if "\n" in val:
|
105
|
+
fh.write(f"{key}<<G2G\n")
|
106
|
+
fh.write(f"{val}\n")
|
107
|
+
fh.write("G2G\n")
|
108
|
+
else:
|
109
|
+
fh.write(f"{key}={val}\n")
|
110
|
+
except Exception as exc:
|
111
|
+
# Use a basic logger since we can't import from other modules
|
112
|
+
# without creating circular dependencies
|
113
|
+
logging.getLogger(__name__).debug("Failed to write GITHUB_OUTPUT: %s", exc)
|