github2gerrit 0.1.10__py3-none-any.whl → 0.1.12__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 +793 -198
- github2gerrit/commit_normalization.py +44 -15
- github2gerrit/config.py +76 -30
- github2gerrit/core.py +1571 -267
- github2gerrit/duplicate_detection.py +227 -113
- 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 +65 -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.10.dist-info → github2gerrit-0.1.12.dist-info}/METADATA +76 -24
- github2gerrit-0.1.12.dist-info/RECORD +31 -0
- {github2gerrit-0.1.10.dist-info → github2gerrit-0.1.12.dist-info}/WHEEL +1 -2
- github2gerrit-0.1.10.dist-info/RECORD +0 -24
- github2gerrit-0.1.10.dist-info/top_level.txt +0 -1
- {github2gerrit-0.1.10.dist-info → github2gerrit-0.1.12.dist-info}/entry_points.txt +0 -0
- {github2gerrit-0.1.10.dist-info → github2gerrit-0.1.12.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
@@ -92,6 +92,13 @@ KNOWN_KEYS: set[str] = {
|
|
92
92
|
# Gerrit REST auth
|
93
93
|
"GERRIT_HTTP_USER",
|
94
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",
|
95
102
|
}
|
96
103
|
|
97
104
|
_ENV_REF = re.compile(r"\$\{ENV:([A-Za-z_][A-Za-z0-9_]*)\}")
|
@@ -131,19 +138,29 @@ def _coerce_value(raw: str) -> str:
|
|
131
138
|
# Normalize escaped newline sequences into real newlines so that values
|
132
139
|
# like SSH keys or known_hosts entries can be specified inline using
|
133
140
|
# '\n' or '\r\n' in configuration files.
|
134
|
-
normalized_newlines =
|
141
|
+
normalized_newlines = (
|
142
|
+
unquoted.replace("\\r\\n", "\n")
|
143
|
+
.replace("\\n", "\n")
|
144
|
+
.replace("\r\n", "\n")
|
145
|
+
)
|
135
146
|
|
136
147
|
# Additional sanitization for SSH private keys
|
137
|
-
if (
|
138
|
-
"
|
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()
|
139
154
|
):
|
140
|
-
# Clean up SSH key formatting: remove extra whitespace, normalize
|
155
|
+
# Clean up SSH key formatting: remove extra whitespace, normalize
|
156
|
+
# line endings
|
141
157
|
lines = normalized_newlines.split("\n")
|
142
158
|
sanitized_lines = []
|
143
159
|
for line in lines:
|
144
160
|
cleaned = line.strip()
|
145
161
|
if cleaned:
|
146
|
-
# 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
|
147
164
|
cleaned = cleaned.replace('"', "").replace("'", "")
|
148
165
|
sanitized_lines.append(cleaned)
|
149
166
|
normalized_newlines = "\n".join(sanitized_lines)
|
@@ -209,26 +226,35 @@ def _load_ini(path: Path) -> configparser.RawConfigParser:
|
|
209
226
|
else:
|
210
227
|
# No closing quote found; fall through
|
211
228
|
# and keep original line
|
212
|
-
log.debug(
|
229
|
+
log.debug(
|
230
|
+
"Multi-line quote not properly closed for line: %s",
|
231
|
+
line[:50],
|
232
|
+
)
|
213
233
|
out_lines.append(line)
|
214
234
|
continue
|
215
235
|
|
216
|
-
# 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
|
217
238
|
# but contain embedded content that might confuse configparser
|
218
239
|
elif rhs.startswith('"') and not rhs.endswith('"'):
|
219
|
-
# This looks like a multi-line value that starts on the
|
220
|
-
#
|
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
|
221
244
|
content_lines = [rhs[1:]] # Remove opening quote
|
222
245
|
i += 1
|
223
246
|
|
224
247
|
while i < len(lines):
|
225
248
|
current_line = lines[i]
|
226
|
-
if current_line.strip().endswith(
|
249
|
+
if current_line.strip().endswith(
|
250
|
+
'"'
|
251
|
+
) and not current_line.strip().endswith('\\"'):
|
227
252
|
# Found closing quote - remove it and add final line
|
228
253
|
final_content = current_line.rstrip()
|
229
254
|
if final_content.endswith('"'):
|
230
255
|
final_content = final_content[:-1]
|
231
|
-
|
256
|
+
# Only add if there's content after removing quote
|
257
|
+
if final_content:
|
232
258
|
content_lines.append(final_content)
|
233
259
|
break
|
234
260
|
else:
|
@@ -238,19 +264,27 @@ def _load_ini(path: Path) -> configparser.RawConfigParser:
|
|
238
264
|
# Join all content and sanitize for SSH keys
|
239
265
|
full_content = "\\n".join(content_lines)
|
240
266
|
|
241
|
-
# Special handling for SSH private keys - remove extra
|
267
|
+
# Special handling for SSH private keys - remove extra
|
268
|
+
# whitespace and line breaks
|
242
269
|
key_name = left.split("=")[0].strip().upper()
|
243
270
|
if "SSH" in key_name and "KEY" in key_name:
|
244
|
-
# For SSH keys, clean up base64 content by removing
|
271
|
+
# For SSH keys, clean up base64 content by removing
|
272
|
+
# whitespace within lines
|
245
273
|
sanitized_lines = []
|
246
274
|
for content_line in content_lines:
|
247
275
|
cleaned = content_line.strip()
|
248
|
-
# Preserve SSH key headers/footers but clean base64
|
276
|
+
# Preserve SSH key headers/footers but clean base64
|
277
|
+
# content
|
249
278
|
if cleaned.startswith("-----") or not cleaned:
|
250
279
|
sanitized_lines.append(cleaned)
|
251
280
|
else:
|
252
|
-
# Remove any embedded quotes and whitespace from
|
253
|
-
|
281
|
+
# Remove any embedded quotes and whitespace from
|
282
|
+
# base64 content
|
283
|
+
cleaned = (
|
284
|
+
cleaned.replace('"', "")
|
285
|
+
.replace("'", "")
|
286
|
+
.strip()
|
287
|
+
)
|
254
288
|
if cleaned:
|
255
289
|
sanitized_lines.append(cleaned)
|
256
290
|
full_content = "\\n".join(sanitized_lines)
|
@@ -391,7 +425,10 @@ def filter_known(
|
|
391
425
|
|
392
426
|
def _is_github_actions_context() -> bool:
|
393
427
|
"""Detect if running in GitHub Actions environment."""
|
394
|
-
return
|
428
|
+
return (
|
429
|
+
os.getenv("GITHUB_ACTIONS") == "true"
|
430
|
+
or os.getenv("GITHUB_EVENT_NAME", "").strip() != ""
|
431
|
+
)
|
395
432
|
|
396
433
|
|
397
434
|
def _is_local_cli_context() -> bool:
|
@@ -417,7 +454,9 @@ def derive_gerrit_parameters(organization: str | None) -> dict[str, str]:
|
|
417
454
|
org = organization.strip().lower()
|
418
455
|
return {
|
419
456
|
"GERRIT_SSH_USER_G2G": f"{org}.gh2gerrit",
|
420
|
-
"GERRIT_SSH_USER_G2G_EMAIL": (
|
457
|
+
"GERRIT_SSH_USER_G2G_EMAIL": (
|
458
|
+
f"releng+{org}-gh2gerrit@linuxfoundation.org"
|
459
|
+
),
|
421
460
|
"GERRIT_SERVER": f"gerrit.{org}.org",
|
422
461
|
}
|
423
462
|
|
@@ -436,10 +475,10 @@ def apply_parameter_derivation(
|
|
436
475
|
- gerrit_ssh_user_g2g_email: releng+[org]-gh2gerrit@linuxfoundation.org
|
437
476
|
- gerrit_server: gerrit.[org].org
|
438
477
|
|
439
|
-
Derivation behavior
|
440
|
-
-
|
441
|
-
|
442
|
-
|
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
|
443
482
|
|
444
483
|
Args:
|
445
484
|
cfg: Configuration dictionary to augment
|
@@ -454,7 +493,9 @@ def apply_parameter_derivation(
|
|
454
493
|
|
455
494
|
# Check execution context to determine derivation strategy
|
456
495
|
is_github_actions = _is_github_actions_context()
|
457
|
-
enable_derivation =
|
496
|
+
enable_derivation = os.getenv(
|
497
|
+
"G2G_ENABLE_DERIVATION", "true"
|
498
|
+
).strip().lower() in (
|
458
499
|
"1",
|
459
500
|
"true",
|
460
501
|
"yes",
|
@@ -463,8 +504,8 @@ def apply_parameter_derivation(
|
|
463
504
|
|
464
505
|
if not enable_derivation:
|
465
506
|
log.debug(
|
466
|
-
"Parameter derivation disabled
|
467
|
-
"
|
507
|
+
"Parameter derivation disabled. Set G2G_ENABLE_DERIVATION=true to "
|
508
|
+
"enable automatic derivation."
|
468
509
|
)
|
469
510
|
return cfg
|
470
511
|
|
@@ -486,7 +527,7 @@ def apply_parameter_derivation(
|
|
486
527
|
newly_derived[key] = value
|
487
528
|
|
488
529
|
if newly_derived:
|
489
|
-
log.
|
530
|
+
log.debug(
|
490
531
|
"Derived parameters applied for organization '%s' (%s): %s",
|
491
532
|
organization,
|
492
533
|
"GitHub Actions" if is_github_actions else "Local CLI",
|
@@ -495,7 +536,9 @@ def apply_parameter_derivation(
|
|
495
536
|
# Save newly derived parameters to configuration file for future use
|
496
537
|
# Default to true for local CLI, false for GitHub Actions
|
497
538
|
default_auto_save = "false" if _is_github_actions_context() else "true"
|
498
|
-
auto_save_enabled = os.getenv(
|
539
|
+
auto_save_enabled = os.getenv(
|
540
|
+
"G2G_AUTO_SAVE_CONFIG", default_auto_save
|
541
|
+
).strip().lower() in (
|
499
542
|
"1",
|
500
543
|
"true",
|
501
544
|
"yes",
|
@@ -505,7 +548,7 @@ def apply_parameter_derivation(
|
|
505
548
|
# Save to config in local CLI mode to create persistent configuration
|
506
549
|
try:
|
507
550
|
save_derived_parameters_to_config(organization, newly_derived)
|
508
|
-
log.
|
551
|
+
log.debug(
|
509
552
|
"Automatically saved derived parameters to configuration "
|
510
553
|
"file for organization '%s'. "
|
511
554
|
"This creates a persistent configuration that you can "
|
@@ -542,7 +585,9 @@ def save_derived_parameters_to_config(
|
|
542
585
|
return
|
543
586
|
|
544
587
|
if config_path is None:
|
545
|
-
config_path =
|
588
|
+
config_path = (
|
589
|
+
os.getenv("G2G_CONFIG_PATH", "").strip() or DEFAULT_CONFIG_PATH
|
590
|
+
)
|
546
591
|
|
547
592
|
config_file = Path(config_path).expanduser()
|
548
593
|
|
@@ -550,7 +595,8 @@ def save_derived_parameters_to_config(
|
|
550
595
|
# Only update when a configuration file already exists
|
551
596
|
if not config_file.exists():
|
552
597
|
log.debug(
|
553
|
-
"Configuration file does not exist; skipping auto-save of
|
598
|
+
"Configuration file does not exist; skipping auto-save of "
|
599
|
+
"derived parameters: %s",
|
554
600
|
config_file,
|
555
601
|
)
|
556
602
|
return
|