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
@@ -0,0 +1,502 @@
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
2
|
+
# SPDX-FileCopyrightText: 2025 The Linux Foundation
|
3
|
+
|
4
|
+
"""
|
5
|
+
Rich display utilities for enhanced CLI output.
|
6
|
+
|
7
|
+
Provides formatted output for PR information, progress tracking, and
|
8
|
+
operation status using Rich formatting library.
|
9
|
+
"""
|
10
|
+
|
11
|
+
from __future__ import annotations
|
12
|
+
|
13
|
+
import logging
|
14
|
+
from datetime import UTC
|
15
|
+
from datetime import datetime
|
16
|
+
from datetime import timedelta
|
17
|
+
from typing import Any
|
18
|
+
|
19
|
+
from .rich_logging import RichDisplayContext
|
20
|
+
from .rich_logging import setup_rich_aware_logging
|
21
|
+
|
22
|
+
|
23
|
+
try:
|
24
|
+
from rich.console import Console
|
25
|
+
from rich.live import Live
|
26
|
+
from rich.table import Table
|
27
|
+
from rich.text import Text
|
28
|
+
|
29
|
+
RICH_AVAILABLE = True
|
30
|
+
except ImportError:
|
31
|
+
RICH_AVAILABLE = False
|
32
|
+
|
33
|
+
# Fallback classes for when Rich is not available
|
34
|
+
class Live: # type: ignore
|
35
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
36
|
+
pass
|
37
|
+
|
38
|
+
def start(self) -> None:
|
39
|
+
pass
|
40
|
+
|
41
|
+
def stop(self) -> None:
|
42
|
+
pass
|
43
|
+
|
44
|
+
def update(self, *args: Any) -> None:
|
45
|
+
pass
|
46
|
+
|
47
|
+
class Text: # type: ignore
|
48
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
49
|
+
pass
|
50
|
+
|
51
|
+
def append(self, *args: Any, **kwargs: Any) -> None:
|
52
|
+
pass
|
53
|
+
|
54
|
+
class Console: # type: ignore
|
55
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
56
|
+
pass
|
57
|
+
|
58
|
+
def print(self, *args: Any, **kwargs: Any) -> None:
|
59
|
+
print(*args)
|
60
|
+
|
61
|
+
class Table: # type: ignore
|
62
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
63
|
+
pass
|
64
|
+
|
65
|
+
def add_column(self, *args: Any, **kwargs: Any) -> None:
|
66
|
+
pass
|
67
|
+
|
68
|
+
def add_row(self, *args: Any, **kwargs: Any) -> None:
|
69
|
+
pass
|
70
|
+
|
71
|
+
|
72
|
+
__all__ = [
|
73
|
+
"RICH_AVAILABLE",
|
74
|
+
"DummyProgressTracker",
|
75
|
+
"G2GProgressTracker",
|
76
|
+
"ProgressTracker",
|
77
|
+
"console",
|
78
|
+
"display_pr_info",
|
79
|
+
]
|
80
|
+
|
81
|
+
log = logging.getLogger("github2gerrit.rich_display")
|
82
|
+
|
83
|
+
# Global console instance
|
84
|
+
console = Console(markup=False) if RICH_AVAILABLE else Console()
|
85
|
+
|
86
|
+
|
87
|
+
def safe_console_print(
|
88
|
+
message: str,
|
89
|
+
*,
|
90
|
+
style: str = "white",
|
91
|
+
progress_tracker: Any = None,
|
92
|
+
err: bool = False,
|
93
|
+
) -> None:
|
94
|
+
"""
|
95
|
+
Safely print to console with proper progress tracker handling.
|
96
|
+
|
97
|
+
This function ensures that progress displays are properly suspended
|
98
|
+
and resumed around console output to prevent display corruption.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
message: Message to print
|
102
|
+
style: Rich style for the message
|
103
|
+
progress_tracker: Optional progress tracker to suspend/resume
|
104
|
+
err: Whether to print to stderr
|
105
|
+
"""
|
106
|
+
# Use Rich display context to manage logging interference
|
107
|
+
context_id = f"safe_console_print_{id(message)}"
|
108
|
+
|
109
|
+
with RichDisplayContext(context_id):
|
110
|
+
if progress_tracker:
|
111
|
+
progress_tracker.suspend()
|
112
|
+
|
113
|
+
try:
|
114
|
+
if RICH_AVAILABLE and not err:
|
115
|
+
console.print(message, style=style)
|
116
|
+
else:
|
117
|
+
print(
|
118
|
+
message, file=None if not err else __import__("sys").stderr
|
119
|
+
)
|
120
|
+
finally:
|
121
|
+
if progress_tracker:
|
122
|
+
progress_tracker.resume()
|
123
|
+
|
124
|
+
|
125
|
+
def safe_typer_echo(
|
126
|
+
message: str,
|
127
|
+
*,
|
128
|
+
progress_tracker: Any = None,
|
129
|
+
err: bool = False,
|
130
|
+
) -> None:
|
131
|
+
"""
|
132
|
+
Safely use typer.echo with proper progress tracker handling.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
message: Message to print
|
136
|
+
progress_tracker: Optional progress tracker to suspend/resume
|
137
|
+
err: Whether to print to stderr
|
138
|
+
"""
|
139
|
+
# Use Rich display context to manage logging interference
|
140
|
+
context_id = f"safe_typer_echo_{id(message)}"
|
141
|
+
|
142
|
+
with RichDisplayContext(context_id):
|
143
|
+
if progress_tracker:
|
144
|
+
progress_tracker.suspend()
|
145
|
+
|
146
|
+
try:
|
147
|
+
import typer
|
148
|
+
|
149
|
+
typer.echo(message, err=err)
|
150
|
+
finally:
|
151
|
+
if progress_tracker:
|
152
|
+
progress_tracker.resume()
|
153
|
+
|
154
|
+
|
155
|
+
def display_pr_info(
|
156
|
+
pr_info: dict[str, Any],
|
157
|
+
title: str = "",
|
158
|
+
progress_tracker: Any = None,
|
159
|
+
) -> None:
|
160
|
+
"""Display pull request information in a formatted table.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
pr_info: Dictionary containing PR information
|
164
|
+
title: Optional table title
|
165
|
+
progress_tracker: Optional progress tracker to suspend/resume
|
166
|
+
"""
|
167
|
+
# Use Rich display context to manage logging interference
|
168
|
+
context_id = f"pr_info_display_{id(pr_info)}"
|
169
|
+
|
170
|
+
if not RICH_AVAILABLE:
|
171
|
+
# Fallback display for when Rich is not available
|
172
|
+
with RichDisplayContext(context_id):
|
173
|
+
if progress_tracker:
|
174
|
+
progress_tracker.suspend()
|
175
|
+
print(f"\n=== {title or 'Pull Request Information'} ===")
|
176
|
+
for key, value in pr_info.items():
|
177
|
+
print(f"{key:15}: {value}")
|
178
|
+
print("=" * 50)
|
179
|
+
if progress_tracker:
|
180
|
+
progress_tracker.resume()
|
181
|
+
return
|
182
|
+
|
183
|
+
# Rich display with logging context
|
184
|
+
with RichDisplayContext(context_id):
|
185
|
+
table = Table(title=title)
|
186
|
+
table.add_column("Property", style="cyan")
|
187
|
+
table.add_column("Value", style="green")
|
188
|
+
|
189
|
+
# Add rows for each piece of PR information
|
190
|
+
for key, value in pr_info.items():
|
191
|
+
table.add_row(str(key), str(value))
|
192
|
+
|
193
|
+
if progress_tracker:
|
194
|
+
progress_tracker.suspend()
|
195
|
+
console.print(table)
|
196
|
+
if progress_tracker:
|
197
|
+
progress_tracker.resume()
|
198
|
+
|
199
|
+
|
200
|
+
class ProgressTracker:
|
201
|
+
"""Base progress tracker for GitHub and Gerrit operations."""
|
202
|
+
|
203
|
+
def __init__(self, operation_type: str, target: str):
|
204
|
+
"""Initialize progress tracker for an operation.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
operation_type: Type of operation (e.g., "GitHub to Gerrit")
|
208
|
+
target: Target identifier (e.g., organization name, repository)
|
209
|
+
"""
|
210
|
+
self.operation_type = operation_type
|
211
|
+
self.target = target
|
212
|
+
self.start_time = datetime.now(UTC)
|
213
|
+
self.console = console
|
214
|
+
|
215
|
+
# Progress counters
|
216
|
+
self.current_operation = "Initializing..."
|
217
|
+
self.errors_count = 0
|
218
|
+
self.warnings_count = 0
|
219
|
+
|
220
|
+
# Rich Live display
|
221
|
+
self.live: Live | None = None
|
222
|
+
self.rich_available = RICH_AVAILABLE
|
223
|
+
self._rich_initially_available = RICH_AVAILABLE
|
224
|
+
self.paused = False
|
225
|
+
|
226
|
+
# Initialize Rich-aware logging on first progress tracker creation
|
227
|
+
if RICH_AVAILABLE:
|
228
|
+
setup_rich_aware_logging()
|
229
|
+
|
230
|
+
# Fallback for when Rich is not available
|
231
|
+
self._last_display = ""
|
232
|
+
self._last_operation = ""
|
233
|
+
|
234
|
+
# Rich display context for managing logging interference
|
235
|
+
self._rich_context: RichDisplayContext | None = None
|
236
|
+
|
237
|
+
def start(self) -> None:
|
238
|
+
"""Start the progress display with in-place updates."""
|
239
|
+
# Start Rich display context to manage logging interference
|
240
|
+
if self.rich_available:
|
241
|
+
context_id = f"progress_tracker_{id(self)}"
|
242
|
+
self._rich_context = RichDisplayContext(context_id)
|
243
|
+
self._rich_context.__enter__()
|
244
|
+
|
245
|
+
self.console.print(
|
246
|
+
"🔄 GitHub to Gerrit ", style="bold blue", end=""
|
247
|
+
)
|
248
|
+
self.console.print(f"for {self.target}", style="bold cyan")
|
249
|
+
else:
|
250
|
+
print(f"🔄 GitHub to Gerrit for {self.target}")
|
251
|
+
self._last_operation = ""
|
252
|
+
|
253
|
+
def stop(self) -> None:
|
254
|
+
"""Stop the progress display."""
|
255
|
+
# Clean up Rich display context
|
256
|
+
if self._rich_context:
|
257
|
+
import contextlib
|
258
|
+
|
259
|
+
with contextlib.suppress(Exception):
|
260
|
+
self._rich_context.__exit__(None, None, None)
|
261
|
+
self._rich_context = None
|
262
|
+
self.paused = False
|
263
|
+
|
264
|
+
def suspend(self) -> None:
|
265
|
+
"""Temporarily pause the display to allow clean printing."""
|
266
|
+
self.paused = True
|
267
|
+
|
268
|
+
def resume(self) -> None:
|
269
|
+
"""Resume the display after it was suspended."""
|
270
|
+
self.paused = False
|
271
|
+
|
272
|
+
def update_operation(self, operation: str) -> None:
|
273
|
+
"""Update the current operation description."""
|
274
|
+
self.current_operation = operation
|
275
|
+
if not self.paused and operation != self._last_operation:
|
276
|
+
if self.rich_available:
|
277
|
+
# Just print the new operation - don't try in-place updates
|
278
|
+
# with Rich
|
279
|
+
self.console.print(f"📋 {operation}", style="dim white")
|
280
|
+
else:
|
281
|
+
print(f"📋 {operation}")
|
282
|
+
self._last_operation = operation
|
283
|
+
|
284
|
+
def add_error(self, message: str | None = None) -> None:
|
285
|
+
"""Increment the error counter."""
|
286
|
+
self.errors_count += 1
|
287
|
+
if message and not self.rich_available:
|
288
|
+
# Only log when Rich is not available to avoid breaking clean
|
289
|
+
# display
|
290
|
+
log.error("Progress tracker error: %s", message)
|
291
|
+
self._refresh_display()
|
292
|
+
|
293
|
+
def add_warning(self, message: str | None = None) -> None:
|
294
|
+
"""Increment the warning counter."""
|
295
|
+
self.warnings_count += 1
|
296
|
+
if message and not self.rich_available:
|
297
|
+
# Only log when Rich is not available to avoid breaking clean
|
298
|
+
# display
|
299
|
+
log.warning("Progress tracker warning: %s", message)
|
300
|
+
self._refresh_display()
|
301
|
+
|
302
|
+
def _refresh_display(self) -> None:
|
303
|
+
"""Refresh the display - no-op for simple print-based display."""
|
304
|
+
|
305
|
+
def _generate_display_text(self) -> Text:
|
306
|
+
"""Generate the current progress display text."""
|
307
|
+
if not self.rich_available:
|
308
|
+
return Text()
|
309
|
+
|
310
|
+
text = Text()
|
311
|
+
|
312
|
+
# Main operation line
|
313
|
+
text.append("🔄 ", style="bold blue")
|
314
|
+
text.append(f"{self.operation_type}", style="bold cyan")
|
315
|
+
text.append(f" for {self.target}", style="white")
|
316
|
+
|
317
|
+
# Status counters
|
318
|
+
if self.errors_count > 0 or self.warnings_count > 0:
|
319
|
+
text.append(" | ", style="white")
|
320
|
+
if self.errors_count > 0:
|
321
|
+
text.append(f"{self.errors_count} errors", style="red")
|
322
|
+
if self.warnings_count > 0:
|
323
|
+
if self.errors_count > 0:
|
324
|
+
text.append(", ", style="white")
|
325
|
+
text.append(f"{self.warnings_count} warnings", style="yellow")
|
326
|
+
|
327
|
+
text.append("\n")
|
328
|
+
|
329
|
+
# Current operation line
|
330
|
+
text.append(f"📋 {self.current_operation}", style="dim white")
|
331
|
+
|
332
|
+
# Elapsed time
|
333
|
+
elapsed = datetime.now(UTC) - self.start_time
|
334
|
+
text.append(
|
335
|
+
f"\n⏱️ Elapsed: {self._format_duration(elapsed)}", style="dim blue"
|
336
|
+
)
|
337
|
+
|
338
|
+
return text
|
339
|
+
|
340
|
+
def _fallback_display(self) -> None:
|
341
|
+
"""Fallback display method - disabled to prevent duplicates."""
|
342
|
+
# Completely disable fallback display to prevent duplicate output
|
343
|
+
|
344
|
+
def _format_duration(self, duration: timedelta) -> str:
|
345
|
+
"""Format a duration for display."""
|
346
|
+
total_seconds = int(duration.total_seconds())
|
347
|
+
minutes = total_seconds // 60
|
348
|
+
seconds = total_seconds % 60
|
349
|
+
|
350
|
+
if minutes > 0:
|
351
|
+
return f"{minutes}m {seconds}s"
|
352
|
+
else:
|
353
|
+
return f"{seconds}s"
|
354
|
+
|
355
|
+
def get_summary(self) -> dict[str, Any]:
|
356
|
+
"""Get a summary of the operation progress."""
|
357
|
+
elapsed = datetime.now(UTC) - self.start_time
|
358
|
+
|
359
|
+
return {
|
360
|
+
"operation_type": self.operation_type,
|
361
|
+
"target": self.target,
|
362
|
+
"errors_count": self.errors_count,
|
363
|
+
"warnings_count": self.warnings_count,
|
364
|
+
"elapsed_time": self._format_duration(elapsed),
|
365
|
+
"current_operation": self.current_operation,
|
366
|
+
}
|
367
|
+
|
368
|
+
|
369
|
+
class G2GProgressTracker(ProgressTracker):
|
370
|
+
"""Specialized progress tracker for GitHub to Gerrit operations."""
|
371
|
+
|
372
|
+
def __init__(self, target: str):
|
373
|
+
super().__init__("GitHub to Gerrit", target)
|
374
|
+
self.prs_processed = 0
|
375
|
+
self.changes_submitted = 0
|
376
|
+
self.changes_updated = 0
|
377
|
+
self.duplicates_skipped = 0
|
378
|
+
|
379
|
+
def pr_processed(self) -> None:
|
380
|
+
"""Mark that a PR was processed."""
|
381
|
+
self.prs_processed += 1
|
382
|
+
self._refresh_display()
|
383
|
+
|
384
|
+
def change_submitted(self) -> None:
|
385
|
+
"""Mark that a new change was submitted to Gerrit."""
|
386
|
+
self.changes_submitted += 1
|
387
|
+
self._refresh_display()
|
388
|
+
|
389
|
+
def change_updated(self) -> None:
|
390
|
+
"""Mark that an existing change was updated in Gerrit."""
|
391
|
+
self.changes_updated += 1
|
392
|
+
self._refresh_display()
|
393
|
+
|
394
|
+
def duplicate_skipped(self) -> None:
|
395
|
+
"""Mark that a duplicate was skipped."""
|
396
|
+
self.duplicates_skipped += 1
|
397
|
+
self._refresh_display()
|
398
|
+
|
399
|
+
def _generate_display_text(self) -> Text:
|
400
|
+
"""Generate G2G-specific display text."""
|
401
|
+
if not self.rich_available:
|
402
|
+
return Text()
|
403
|
+
|
404
|
+
text = Text()
|
405
|
+
|
406
|
+
# Main progress line
|
407
|
+
text.append("🔄 GitHub to Gerrit ", style="bold blue")
|
408
|
+
text.append(f"for {self.target}", style="bold cyan")
|
409
|
+
|
410
|
+
# Stats
|
411
|
+
if self.prs_processed > 0:
|
412
|
+
text.append(f" | {self.prs_processed} PRs processed", style="white")
|
413
|
+
|
414
|
+
if self.changes_submitted > 0:
|
415
|
+
text.append(
|
416
|
+
f" | {self.changes_submitted} new changes", style="green"
|
417
|
+
)
|
418
|
+
|
419
|
+
if self.changes_updated > 0:
|
420
|
+
text.append(f" | {self.changes_updated} updated", style="yellow")
|
421
|
+
|
422
|
+
if self.duplicates_skipped > 0:
|
423
|
+
text.append(
|
424
|
+
f" | {self.duplicates_skipped} duplicates skipped",
|
425
|
+
style="dim white",
|
426
|
+
)
|
427
|
+
|
428
|
+
# Error/warning counts
|
429
|
+
if self.errors_count > 0:
|
430
|
+
text.append(f" | {self.errors_count} errors", style="red")
|
431
|
+
if self.warnings_count > 0:
|
432
|
+
text.append(f" | {self.warnings_count} warnings", style="yellow")
|
433
|
+
|
434
|
+
text.append("\n")
|
435
|
+
|
436
|
+
# Current operation line
|
437
|
+
text.append(f"📋 {self.current_operation}", style="dim white")
|
438
|
+
|
439
|
+
# Elapsed time
|
440
|
+
elapsed = datetime.now(UTC) - self.start_time
|
441
|
+
text.append(
|
442
|
+
f"\n⏱️ Elapsed: {self._format_duration(elapsed)}", style="dim blue"
|
443
|
+
)
|
444
|
+
|
445
|
+
return text
|
446
|
+
|
447
|
+
def get_summary(self) -> dict[str, Any]:
|
448
|
+
"""Get G2G-specific summary."""
|
449
|
+
summary = super().get_summary()
|
450
|
+
summary.update(
|
451
|
+
{
|
452
|
+
"prs_processed": self.prs_processed,
|
453
|
+
"changes_submitted": self.changes_submitted,
|
454
|
+
"changes_updated": self.changes_updated,
|
455
|
+
"duplicates_skipped": self.duplicates_skipped,
|
456
|
+
}
|
457
|
+
)
|
458
|
+
return summary
|
459
|
+
|
460
|
+
|
461
|
+
class DummyProgressTracker:
|
462
|
+
"""A no-op progress tracker for when progress display is disabled."""
|
463
|
+
|
464
|
+
def __init__(self, operation_type: str, target: str):
|
465
|
+
self.operation_type = operation_type
|
466
|
+
self.target = target
|
467
|
+
|
468
|
+
def start(self) -> None:
|
469
|
+
pass
|
470
|
+
|
471
|
+
def stop(self) -> None:
|
472
|
+
pass
|
473
|
+
|
474
|
+
def suspend(self) -> None:
|
475
|
+
pass
|
476
|
+
|
477
|
+
def resume(self) -> None:
|
478
|
+
pass
|
479
|
+
|
480
|
+
def update_operation(self, operation: str) -> None:
|
481
|
+
pass
|
482
|
+
|
483
|
+
def add_error(self, message: str | None = None) -> None:
|
484
|
+
pass
|
485
|
+
|
486
|
+
def add_warning(self, message: str | None = None) -> None:
|
487
|
+
pass
|
488
|
+
|
489
|
+
def pr_processed(self) -> None:
|
490
|
+
pass
|
491
|
+
|
492
|
+
def change_submitted(self) -> None:
|
493
|
+
pass
|
494
|
+
|
495
|
+
def change_updated(self) -> None:
|
496
|
+
pass
|
497
|
+
|
498
|
+
def duplicate_skipped(self) -> None:
|
499
|
+
pass
|
500
|
+
|
501
|
+
def get_summary(self) -> dict[str, Any]:
|
502
|
+
return {"operation_type": self.operation_type, "target": self.target}
|