github2gerrit 0.1.9__py3-none-any.whl → 0.1.11__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 +796 -200
- github2gerrit/commit_normalization.py +44 -15
- github2gerrit/config.py +77 -30
- github2gerrit/core.py +1576 -260
- github2gerrit/duplicate_detection.py +224 -100
- github2gerrit/external_api.py +76 -25
- github2gerrit/gerrit_query.py +286 -0
- github2gerrit/gerrit_rest.py +53 -18
- github2gerrit/gerrit_urls.py +90 -33
- github2gerrit/github_api.py +19 -6
- github2gerrit/gitutils.py +43 -14
- github2gerrit/mapping_comment.py +345 -0
- github2gerrit/models.py +15 -1
- github2gerrit/orchestrator/__init__.py +25 -0
- github2gerrit/orchestrator/reconciliation.py +589 -0
- github2gerrit/pr_content_filter.py +65 -17
- github2gerrit/reconcile_matcher.py +595 -0
- github2gerrit/rich_display.py +502 -0
- github2gerrit/rich_logging.py +316 -0
- github2gerrit/similarity.py +66 -19
- github2gerrit/ssh_agent_setup.py +59 -22
- github2gerrit/ssh_common.py +30 -11
- github2gerrit/ssh_discovery.py +67 -20
- github2gerrit/trailers.py +340 -0
- github2gerrit/utils.py +6 -2
- {github2gerrit-0.1.9.dist-info → github2gerrit-0.1.11.dist-info}/METADATA +99 -25
- github2gerrit-0.1.11.dist-info/RECORD +31 -0
- {github2gerrit-0.1.9.dist-info → github2gerrit-0.1.11.dist-info}/WHEEL +1 -2
- github2gerrit-0.1.9.dist-info/RECORD +0 -24
- github2gerrit-0.1.9.dist-info/top_level.txt +0 -1
- {github2gerrit-0.1.9.dist-info → github2gerrit-0.1.11.dist-info}/entry_points.txt +0 -0
- {github2gerrit-0.1.9.dist-info → github2gerrit-0.1.11.dist-info}/licenses/LICENSE +0 -0
@@ -173,7 +173,10 @@ class CommitNormalizer:
|
|
173
173
|
|
174
174
|
def _is_conventional_commit(self, title: str) -> bool:
|
175
175
|
"""Check if title is already in conventional commit format."""
|
176
|
-
pattern =
|
176
|
+
pattern = (
|
177
|
+
r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)"
|
178
|
+
r"(\(.+?\))?\s*!?\s*:\s*.+"
|
179
|
+
)
|
177
180
|
return bool(re.match(pattern, title, re.IGNORECASE))
|
178
181
|
|
179
182
|
def _is_automation_pr(self, title: str, author: str) -> bool:
|
@@ -187,7 +190,9 @@ class CommitNormalizer:
|
|
187
190
|
"greenkeeper[bot]",
|
188
191
|
]
|
189
192
|
|
190
|
-
if any(
|
193
|
+
if any(
|
194
|
+
author.lower().startswith(bot.lower()) for bot in automation_authors
|
195
|
+
):
|
191
196
|
return True
|
192
197
|
|
193
198
|
# Check for automation patterns in title
|
@@ -199,11 +204,17 @@ class CommitNormalizer:
|
|
199
204
|
r"pre-commit.*autoupdate",
|
200
205
|
]
|
201
206
|
|
202
|
-
return any(
|
207
|
+
return any(
|
208
|
+
re.search(pattern, title, re.IGNORECASE)
|
209
|
+
for pattern in automation_patterns
|
210
|
+
)
|
203
211
|
|
204
212
|
def _detect_preferences(self) -> None:
|
205
213
|
"""Detect repository preferences for conventional commits."""
|
206
|
-
log.debug(
|
214
|
+
log.debug(
|
215
|
+
"Detecting conventional commit preferences for workspace: %s",
|
216
|
+
self.workspace,
|
217
|
+
)
|
207
218
|
|
208
219
|
# Check .pre-commit-config.yaml
|
209
220
|
self._check_precommit_config()
|
@@ -262,14 +273,16 @@ class CommitNormalizer:
|
|
262
273
|
|
263
274
|
for title_pattern in titles:
|
264
275
|
# Extract conventional commit type from pattern
|
265
|
-
if title_pattern.startswith(
|
276
|
+
if title_pattern.startswith(
|
277
|
+
"/"
|
278
|
+
) and title_pattern.endswith("/i"):
|
266
279
|
pattern = title_pattern[1:-2] # Remove /pattern/i
|
267
280
|
if ":" in pattern:
|
268
281
|
commit_type = pattern.split(":")[0]
|
269
282
|
if commit_type in CONVENTIONAL_COMMIT_TYPES:
|
270
|
-
self.preferences.preferred_types[
|
283
|
+
self.preferences.preferred_types[
|
271
284
|
commit_type
|
272
|
-
)
|
285
|
+
] = self._get_capitalization(commit_type)
|
273
286
|
|
274
287
|
break # Use first found config
|
275
288
|
|
@@ -309,11 +322,15 @@ class CommitNormalizer:
|
|
309
322
|
match = re.match(r"^([a-zA-Z]+)", message)
|
310
323
|
if match:
|
311
324
|
commit_type = match.group(1).lower()
|
312
|
-
type_counts[commit_type] =
|
325
|
+
type_counts[commit_type] = (
|
326
|
+
type_counts.get(commit_type, 0) + 1
|
327
|
+
)
|
313
328
|
|
314
329
|
# Track capitalization
|
315
330
|
if commit_type not in capitalization_examples:
|
316
|
-
capitalization_examples[commit_type] = match.group(
|
331
|
+
capitalization_examples[commit_type] = match.group(
|
332
|
+
1
|
333
|
+
)
|
317
334
|
|
318
335
|
# Update preferences based on analysis
|
319
336
|
if type_counts:
|
@@ -330,7 +347,9 @@ class CommitNormalizer:
|
|
330
347
|
# Update preferred types
|
331
348
|
for commit_type in type_counts:
|
332
349
|
if commit_type in CONVENTIONAL_COMMIT_TYPES:
|
333
|
-
self.preferences.preferred_types[commit_type] =
|
350
|
+
self.preferences.preferred_types[commit_type] = (
|
351
|
+
self._apply_capitalization(commit_type)
|
352
|
+
)
|
334
353
|
|
335
354
|
except Exception as e:
|
336
355
|
log.debug("Failed to analyze git history: %s", e)
|
@@ -356,7 +375,9 @@ class CommitNormalizer:
|
|
356
375
|
title_lower = title.lower()
|
357
376
|
|
358
377
|
# Check for dependabot patterns first
|
359
|
-
if "dependabot" in author.lower() or any(
|
378
|
+
if "dependabot" in author.lower() or any(
|
379
|
+
re.search(pattern, title_lower) for pattern in DEPENDABOT_PATTERNS
|
380
|
+
):
|
360
381
|
return self.preferences.dependency_type
|
361
382
|
|
362
383
|
# Check for pre-commit.ci patterns
|
@@ -402,7 +423,8 @@ class CommitNormalizer:
|
|
402
423
|
for prefix in prefixes_to_remove:
|
403
424
|
title = re.sub(prefix, "", title, flags=re.IGNORECASE).strip()
|
404
425
|
|
405
|
-
# Ensure first letter is lowercase (will be adjusted by capitalization
|
426
|
+
# Ensure first letter is lowercase (will be adjusted by capitalization
|
427
|
+
# later)
|
406
428
|
if title and title[0].isupper():
|
407
429
|
title = title[0].lower() + title[1:]
|
408
430
|
|
@@ -415,7 +437,10 @@ class CommitNormalizer:
|
|
415
437
|
|
416
438
|
# Add scope if preferred for dependency updates
|
417
439
|
scope = ""
|
418
|
-
if
|
440
|
+
if (
|
441
|
+
commit_type == self.preferences.dependency_type
|
442
|
+
and self.preferences.use_scope
|
443
|
+
):
|
419
444
|
scope = f"({self.preferences.dependency_scope})"
|
420
445
|
|
421
446
|
return f"{formatted_type}{scope}: {title}"
|
@@ -439,7 +464,9 @@ class CommitNormalizer:
|
|
439
464
|
return "lower"
|
440
465
|
|
441
466
|
|
442
|
-
def normalize_commit_title(
|
467
|
+
def normalize_commit_title(
|
468
|
+
title: str, author: str, workspace: Path | None = None
|
469
|
+
) -> str:
|
443
470
|
"""
|
444
471
|
Normalize a commit title to conventional commit format.
|
445
472
|
|
@@ -455,7 +482,9 @@ def normalize_commit_title(title: str, author: str, workspace: Path | None = Non
|
|
455
482
|
return normalizer.normalize_commit_title(title, author)
|
456
483
|
|
457
484
|
|
458
|
-
def should_normalize_commit(
|
485
|
+
def should_normalize_commit(
|
486
|
+
title: str, author: str, workspace: Path | None = None
|
487
|
+
) -> bool:
|
459
488
|
"""
|
460
489
|
Check if a commit title should be normalized.
|
461
490
|
|
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",
|
@@ -91,6 +92,13 @@ KNOWN_KEYS: set[str] = {
|
|
91
92
|
# Gerrit REST auth
|
92
93
|
"GERRIT_HTTP_USER",
|
93
94
|
"GERRIT_HTTP_PASSWORD",
|
95
|
+
# Reconciliation configuration
|
96
|
+
"REUSE_STRATEGY",
|
97
|
+
"SIMILARITY_SUBJECT",
|
98
|
+
"SIMILARITY_FILES",
|
99
|
+
"ALLOW_ORPHAN_CHANGES",
|
100
|
+
"PERSIST_SINGLE_MAPPING_COMMENT",
|
101
|
+
"LOG_RECONCILE_JSON",
|
94
102
|
}
|
95
103
|
|
96
104
|
_ENV_REF = re.compile(r"\$\{ENV:([A-Za-z_][A-Za-z0-9_]*)\}")
|
@@ -130,19 +138,29 @@ def _coerce_value(raw: str) -> str:
|
|
130
138
|
# Normalize escaped newline sequences into real newlines so that values
|
131
139
|
# like SSH keys or known_hosts entries can be specified inline using
|
132
140
|
# '\n' or '\r\n' in configuration files.
|
133
|
-
normalized_newlines =
|
141
|
+
normalized_newlines = (
|
142
|
+
unquoted.replace("\\r\\n", "\n")
|
143
|
+
.replace("\\n", "\n")
|
144
|
+
.replace("\r\n", "\n")
|
145
|
+
)
|
134
146
|
|
135
147
|
# Additional sanitization for SSH private keys
|
136
|
-
if (
|
137
|
-
"
|
148
|
+
if (
|
149
|
+
"-----BEGIN" in normalized_newlines
|
150
|
+
and "PRIVATE KEY-----" in normalized_newlines
|
151
|
+
) or (
|
152
|
+
"ssh-" in normalized_newlines.lower()
|
153
|
+
and "key" in normalized_newlines.lower()
|
138
154
|
):
|
139
|
-
# Clean up SSH key formatting: remove extra whitespace, normalize
|
155
|
+
# Clean up SSH key formatting: remove extra whitespace, normalize
|
156
|
+
# line endings
|
140
157
|
lines = normalized_newlines.split("\n")
|
141
158
|
sanitized_lines = []
|
142
159
|
for line in lines:
|
143
160
|
cleaned = line.strip()
|
144
161
|
if cleaned:
|
145
|
-
# Remove any stray quotes that might have been embedded in the
|
162
|
+
# Remove any stray quotes that might have been embedded in the
|
163
|
+
# key content
|
146
164
|
cleaned = cleaned.replace('"', "").replace("'", "")
|
147
165
|
sanitized_lines.append(cleaned)
|
148
166
|
normalized_newlines = "\n".join(sanitized_lines)
|
@@ -208,26 +226,35 @@ def _load_ini(path: Path) -> configparser.RawConfigParser:
|
|
208
226
|
else:
|
209
227
|
# No closing quote found; fall through
|
210
228
|
# and keep original line
|
211
|
-
log.debug(
|
229
|
+
log.debug(
|
230
|
+
"Multi-line quote not properly closed for line: %s",
|
231
|
+
line[:50],
|
232
|
+
)
|
212
233
|
out_lines.append(line)
|
213
234
|
continue
|
214
235
|
|
215
|
-
# Handle SSH private keys and other values that start with a
|
236
|
+
# Handle SSH private keys and other values that start with a
|
237
|
+
# quote
|
216
238
|
# but contain embedded content that might confuse configparser
|
217
239
|
elif rhs.startswith('"') and not rhs.endswith('"'):
|
218
|
-
# This looks like a multi-line value that starts on the
|
219
|
-
#
|
240
|
+
# This looks like a multi-line value that starts on the
|
241
|
+
# same line
|
242
|
+
# Collect all content until we find a line ending with a
|
243
|
+
# quote
|
220
244
|
content_lines = [rhs[1:]] # Remove opening quote
|
221
245
|
i += 1
|
222
246
|
|
223
247
|
while i < len(lines):
|
224
248
|
current_line = lines[i]
|
225
|
-
if current_line.strip().endswith(
|
249
|
+
if current_line.strip().endswith(
|
250
|
+
'"'
|
251
|
+
) and not current_line.strip().endswith('\\"'):
|
226
252
|
# Found closing quote - remove it and add final line
|
227
253
|
final_content = current_line.rstrip()
|
228
254
|
if final_content.endswith('"'):
|
229
255
|
final_content = final_content[:-1]
|
230
|
-
|
256
|
+
# Only add if there's content after removing quote
|
257
|
+
if final_content:
|
231
258
|
content_lines.append(final_content)
|
232
259
|
break
|
233
260
|
else:
|
@@ -237,19 +264,27 @@ def _load_ini(path: Path) -> configparser.RawConfigParser:
|
|
237
264
|
# Join all content and sanitize for SSH keys
|
238
265
|
full_content = "\\n".join(content_lines)
|
239
266
|
|
240
|
-
# Special handling for SSH private keys - remove extra
|
267
|
+
# Special handling for SSH private keys - remove extra
|
268
|
+
# whitespace and line breaks
|
241
269
|
key_name = left.split("=")[0].strip().upper()
|
242
270
|
if "SSH" in key_name and "KEY" in key_name:
|
243
|
-
# For SSH keys, clean up base64 content by removing
|
271
|
+
# For SSH keys, clean up base64 content by removing
|
272
|
+
# whitespace within lines
|
244
273
|
sanitized_lines = []
|
245
274
|
for content_line in content_lines:
|
246
275
|
cleaned = content_line.strip()
|
247
|
-
# Preserve SSH key headers/footers but clean base64
|
276
|
+
# Preserve SSH key headers/footers but clean base64
|
277
|
+
# content
|
248
278
|
if cleaned.startswith("-----") or not cleaned:
|
249
279
|
sanitized_lines.append(cleaned)
|
250
280
|
else:
|
251
|
-
# Remove any embedded quotes and whitespace from
|
252
|
-
|
281
|
+
# Remove any embedded quotes and whitespace from
|
282
|
+
# base64 content
|
283
|
+
cleaned = (
|
284
|
+
cleaned.replace('"', "")
|
285
|
+
.replace("'", "")
|
286
|
+
.strip()
|
287
|
+
)
|
253
288
|
if cleaned:
|
254
289
|
sanitized_lines.append(cleaned)
|
255
290
|
full_content = "\\n".join(sanitized_lines)
|
@@ -390,7 +425,10 @@ def filter_known(
|
|
390
425
|
|
391
426
|
def _is_github_actions_context() -> bool:
|
392
427
|
"""Detect if running in GitHub Actions environment."""
|
393
|
-
return
|
428
|
+
return (
|
429
|
+
os.getenv("GITHUB_ACTIONS") == "true"
|
430
|
+
or os.getenv("GITHUB_EVENT_NAME", "").strip() != ""
|
431
|
+
)
|
394
432
|
|
395
433
|
|
396
434
|
def _is_local_cli_context() -> bool:
|
@@ -416,7 +454,9 @@ def derive_gerrit_parameters(organization: str | None) -> dict[str, str]:
|
|
416
454
|
org = organization.strip().lower()
|
417
455
|
return {
|
418
456
|
"GERRIT_SSH_USER_G2G": f"{org}.gh2gerrit",
|
419
|
-
"GERRIT_SSH_USER_G2G_EMAIL": (
|
457
|
+
"GERRIT_SSH_USER_G2G_EMAIL": (
|
458
|
+
f"releng+{org}-gh2gerrit@linuxfoundation.org"
|
459
|
+
),
|
420
460
|
"GERRIT_SERVER": f"gerrit.{org}.org",
|
421
461
|
}
|
422
462
|
|
@@ -435,10 +475,10 @@ def apply_parameter_derivation(
|
|
435
475
|
- gerrit_ssh_user_g2g_email: releng+[org]-gh2gerrit@linuxfoundation.org
|
436
476
|
- gerrit_server: gerrit.[org].org
|
437
477
|
|
438
|
-
Derivation behavior
|
439
|
-
-
|
440
|
-
|
441
|
-
|
478
|
+
Derivation behavior:
|
479
|
+
- Default: Automatic derivation enabled (G2G_ENABLE_DERIVATION=true by
|
480
|
+
default)
|
481
|
+
- Can be disabled by setting G2G_ENABLE_DERIVATION=false
|
442
482
|
|
443
483
|
Args:
|
444
484
|
cfg: Configuration dictionary to augment
|
@@ -453,7 +493,9 @@ def apply_parameter_derivation(
|
|
453
493
|
|
454
494
|
# Check execution context to determine derivation strategy
|
455
495
|
is_github_actions = _is_github_actions_context()
|
456
|
-
enable_derivation =
|
496
|
+
enable_derivation = os.getenv(
|
497
|
+
"G2G_ENABLE_DERIVATION", "true"
|
498
|
+
).strip().lower() in (
|
457
499
|
"1",
|
458
500
|
"true",
|
459
501
|
"yes",
|
@@ -462,8 +504,8 @@ def apply_parameter_derivation(
|
|
462
504
|
|
463
505
|
if not enable_derivation:
|
464
506
|
log.debug(
|
465
|
-
"Parameter derivation disabled
|
466
|
-
"
|
507
|
+
"Parameter derivation disabled. Set G2G_ENABLE_DERIVATION=true to "
|
508
|
+
"enable automatic derivation."
|
467
509
|
)
|
468
510
|
return cfg
|
469
511
|
|
@@ -485,7 +527,7 @@ def apply_parameter_derivation(
|
|
485
527
|
newly_derived[key] = value
|
486
528
|
|
487
529
|
if newly_derived:
|
488
|
-
log.
|
530
|
+
log.debug(
|
489
531
|
"Derived parameters applied for organization '%s' (%s): %s",
|
490
532
|
organization,
|
491
533
|
"GitHub Actions" if is_github_actions else "Local CLI",
|
@@ -494,7 +536,9 @@ def apply_parameter_derivation(
|
|
494
536
|
# Save newly derived parameters to configuration file for future use
|
495
537
|
# Default to true for local CLI, false for GitHub Actions
|
496
538
|
default_auto_save = "false" if _is_github_actions_context() else "true"
|
497
|
-
auto_save_enabled = os.getenv(
|
539
|
+
auto_save_enabled = os.getenv(
|
540
|
+
"G2G_AUTO_SAVE_CONFIG", default_auto_save
|
541
|
+
).strip().lower() in (
|
498
542
|
"1",
|
499
543
|
"true",
|
500
544
|
"yes",
|
@@ -504,7 +548,7 @@ def apply_parameter_derivation(
|
|
504
548
|
# Save to config in local CLI mode to create persistent configuration
|
505
549
|
try:
|
506
550
|
save_derived_parameters_to_config(organization, newly_derived)
|
507
|
-
log.
|
551
|
+
log.debug(
|
508
552
|
"Automatically saved derived parameters to configuration "
|
509
553
|
"file for organization '%s'. "
|
510
554
|
"This creates a persistent configuration that you can "
|
@@ -541,7 +585,9 @@ def save_derived_parameters_to_config(
|
|
541
585
|
return
|
542
586
|
|
543
587
|
if config_path is None:
|
544
|
-
config_path =
|
588
|
+
config_path = (
|
589
|
+
os.getenv("G2G_CONFIG_PATH", "").strip() or DEFAULT_CONFIG_PATH
|
590
|
+
)
|
545
591
|
|
546
592
|
config_file = Path(config_path).expanduser()
|
547
593
|
|
@@ -549,7 +595,8 @@ def save_derived_parameters_to_config(
|
|
549
595
|
# Only update when a configuration file already exists
|
550
596
|
if not config_file.exists():
|
551
597
|
log.debug(
|
552
|
-
"Configuration file does not exist; skipping auto-save of
|
598
|
+
"Configuration file does not exist; skipping auto-save of "
|
599
|
+
"derived parameters: %s",
|
553
600
|
config_file,
|
554
601
|
)
|
555
602
|
return
|