gitflow-analytics 1.3.6__py3-none-any.whl → 3.3.0__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.
Files changed (48) hide show
  1. gitflow_analytics/_version.py +1 -1
  2. gitflow_analytics/classification/batch_classifier.py +156 -4
  3. gitflow_analytics/cli.py +897 -179
  4. gitflow_analytics/config/loader.py +40 -1
  5. gitflow_analytics/config/schema.py +4 -0
  6. gitflow_analytics/core/cache.py +20 -0
  7. gitflow_analytics/core/data_fetcher.py +1254 -228
  8. gitflow_analytics/core/git_auth.py +169 -0
  9. gitflow_analytics/core/git_timeout_wrapper.py +347 -0
  10. gitflow_analytics/core/metrics_storage.py +12 -3
  11. gitflow_analytics/core/progress.py +219 -18
  12. gitflow_analytics/core/subprocess_git.py +145 -0
  13. gitflow_analytics/extractors/ml_tickets.py +3 -2
  14. gitflow_analytics/extractors/tickets.py +93 -8
  15. gitflow_analytics/integrations/jira_integration.py +1 -1
  16. gitflow_analytics/integrations/orchestrator.py +47 -29
  17. gitflow_analytics/metrics/branch_health.py +3 -2
  18. gitflow_analytics/models/database.py +72 -1
  19. gitflow_analytics/pm_framework/adapters/jira_adapter.py +12 -5
  20. gitflow_analytics/pm_framework/orchestrator.py +8 -3
  21. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +24 -4
  22. gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +3 -1
  23. gitflow_analytics/qualitative/core/llm_fallback.py +34 -2
  24. gitflow_analytics/reports/narrative_writer.py +118 -74
  25. gitflow_analytics/security/__init__.py +11 -0
  26. gitflow_analytics/security/config.py +189 -0
  27. gitflow_analytics/security/extractors/__init__.py +7 -0
  28. gitflow_analytics/security/extractors/dependency_checker.py +379 -0
  29. gitflow_analytics/security/extractors/secret_detector.py +197 -0
  30. gitflow_analytics/security/extractors/vulnerability_scanner.py +333 -0
  31. gitflow_analytics/security/llm_analyzer.py +347 -0
  32. gitflow_analytics/security/reports/__init__.py +5 -0
  33. gitflow_analytics/security/reports/security_report.py +358 -0
  34. gitflow_analytics/security/security_analyzer.py +414 -0
  35. gitflow_analytics/tui/app.py +3 -1
  36. gitflow_analytics/tui/progress_adapter.py +313 -0
  37. gitflow_analytics/tui/screens/analysis_progress_screen.py +407 -46
  38. gitflow_analytics/tui/screens/results_screen.py +219 -206
  39. gitflow_analytics/ui/__init__.py +21 -0
  40. gitflow_analytics/ui/progress_display.py +1477 -0
  41. gitflow_analytics/verify_activity.py +697 -0
  42. {gitflow_analytics-1.3.6.dist-info → gitflow_analytics-3.3.0.dist-info}/METADATA +2 -1
  43. {gitflow_analytics-1.3.6.dist-info → gitflow_analytics-3.3.0.dist-info}/RECORD +47 -31
  44. gitflow_analytics/cli_rich.py +0 -503
  45. {gitflow_analytics-1.3.6.dist-info → gitflow_analytics-3.3.0.dist-info}/WHEEL +0 -0
  46. {gitflow_analytics-1.3.6.dist-info → gitflow_analytics-3.3.0.dist-info}/entry_points.txt +0 -0
  47. {gitflow_analytics-1.3.6.dist-info → gitflow_analytics-3.3.0.dist-info}/licenses/LICENSE +0 -0
  48. {gitflow_analytics-1.3.6.dist-info → gitflow_analytics-3.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1477 @@
1
+ """
2
+ Rich-based progress display for GitFlow Analytics.
3
+
4
+ This module provides a sophisticated progress meter using the Rich library
5
+ for beautiful terminal output with live updates and statistics.
6
+ """
7
+
8
+ import threading
9
+ import time
10
+ from contextlib import contextmanager
11
+ from dataclasses import dataclass
12
+ from datetime import datetime, timedelta
13
+ from enum import Enum
14
+ from typing import Any, Dict, Optional
15
+
16
+ # Try to import psutil, but make it optional
17
+ try:
18
+ import psutil
19
+
20
+ PSUTIL_AVAILABLE = True
21
+ except ImportError:
22
+ PSUTIL_AVAILABLE = False
23
+
24
+ try:
25
+ from rich import box
26
+ from rich.align import Align
27
+ from rich.columns import Columns
28
+ from rich.console import Console, Group
29
+ from rich.layout import Layout
30
+ from rich.live import Live
31
+ from rich.panel import Panel
32
+ from rich.progress import (
33
+ BarColumn,
34
+ MofNCompleteColumn,
35
+ Progress,
36
+ SpinnerColumn,
37
+ TextColumn,
38
+ TimeElapsedColumn,
39
+ TimeRemainingColumn,
40
+ )
41
+ from rich.table import Table
42
+ from rich.text import Text
43
+
44
+ RICH_AVAILABLE = True
45
+ except ImportError:
46
+ RICH_AVAILABLE = False
47
+
48
+
49
+ class RepositoryStatus(Enum):
50
+ """Status of repository processing."""
51
+
52
+ PENDING = "pending"
53
+ PROCESSING = "processing"
54
+ COMPLETE = "complete"
55
+ ERROR = "error"
56
+ SKIPPED = "skipped"
57
+
58
+
59
+ @dataclass
60
+ class RepositoryInfo:
61
+ """Information about a repository being processed."""
62
+
63
+ name: str
64
+ status: RepositoryStatus = RepositoryStatus.PENDING
65
+ commits: int = 0
66
+ total_commits: int = 0
67
+ developers: int = 0
68
+ processing_time: float = 0.0
69
+ error_message: Optional[str] = None
70
+ start_time: Optional[datetime] = None
71
+
72
+ def get_status_icon(self) -> str:
73
+ """Get icon for current status."""
74
+ icons = {
75
+ RepositoryStatus.PENDING: "⏸", # More visible pending icon
76
+ RepositoryStatus.PROCESSING: "🔄", # Clearer processing icon
77
+ RepositoryStatus.COMPLETE: "✅", # Green checkmark
78
+ RepositoryStatus.ERROR: "❌", # Red X
79
+ RepositoryStatus.SKIPPED: "⊘",
80
+ }
81
+ return icons.get(self.status, "?")
82
+
83
+ def get_status_color(self) -> str:
84
+ """Get color for current status."""
85
+ colors = {
86
+ RepositoryStatus.PENDING: "dim white",
87
+ RepositoryStatus.PROCESSING: "yellow",
88
+ RepositoryStatus.COMPLETE: "green",
89
+ RepositoryStatus.ERROR: "red",
90
+ RepositoryStatus.SKIPPED: "dim yellow",
91
+ }
92
+ return colors.get(self.status, "white")
93
+
94
+
95
+ @dataclass
96
+ class ProgressStatistics:
97
+ """Overall progress statistics."""
98
+
99
+ total_commits: int = 0
100
+ total_commits_processed: int = 0
101
+ total_developers: int = 0
102
+ total_tickets: int = 0
103
+ total_repositories: int = 0
104
+ processed_repositories: int = 0
105
+ successful_repositories: int = 0
106
+ failed_repositories: int = 0
107
+ skipped_repositories: int = 0
108
+ processing_speed: float = 0.0 # commits per second
109
+ memory_usage: float = 0.0 # MB
110
+ cpu_percent: float = 0.0
111
+ start_time: Optional[datetime] = None
112
+ current_phase: str = "Initializing"
113
+
114
+ def get_elapsed_time(self) -> str:
115
+ """Get elapsed time as string."""
116
+ if not self.start_time:
117
+ return "0:00:00"
118
+ elapsed = datetime.now() - self.start_time
119
+ return str(elapsed).split(".")[0]
120
+
121
+
122
+ class RichProgressDisplay:
123
+ """Rich-based progress display for GitFlow Analytics."""
124
+
125
+ def __init__(self, version: str = "1.3.11", update_frequency: float = 0.25):
126
+ """
127
+ Initialize the progress display.
128
+
129
+ Args:
130
+ version: Version of GitFlow Analytics
131
+ update_frequency: How often to update display in seconds (default 0.25 for smooth updates)
132
+ """
133
+ if not RICH_AVAILABLE:
134
+ raise ImportError("Rich library is not available. Install with: pip install rich")
135
+
136
+ self.version = version
137
+ self.update_frequency = update_frequency
138
+ # Force terminal mode to ensure Rich works even when output is piped
139
+ self.console = Console(force_terminal=True)
140
+
141
+ # Progress tracking with enhanced styling
142
+ # Don't start the progress bars - they'll be rendered inside Live
143
+ self.overall_progress = Progress(
144
+ SpinnerColumn(style="bold cyan"),
145
+ TextColumn("[bold blue]{task.description}"),
146
+ BarColumn(bar_width=40, style="cyan", complete_style="green"),
147
+ MofNCompleteColumn(),
148
+ TextColumn("•"),
149
+ TimeRemainingColumn(),
150
+ transient=False,
151
+ )
152
+
153
+ self.repo_progress = Progress(
154
+ TextColumn("[cyan]{task.description}"),
155
+ BarColumn(bar_width=30, style="yellow", complete_style="green"),
156
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
157
+ TextColumn("•"),
158
+ TextColumn("{task.fields[speed]:.1f} commits/s"),
159
+ transient=False,
160
+ )
161
+
162
+ # Data tracking
163
+ self.repositories: Dict[str, RepositoryInfo] = {}
164
+ self.statistics = ProgressStatistics()
165
+ self.current_repo: Optional[str] = None
166
+
167
+ # Task IDs
168
+ self.overall_task_id = None
169
+ self.repo_task_id = None
170
+
171
+ # Thread safety
172
+ self._lock = threading.Lock()
173
+ self._live = None
174
+ self._layout = None
175
+ self._update_counter = 0 # For tracking updates
176
+
177
+ # System monitoring (only if psutil is available)
178
+ self._process = psutil.Process() if PSUTIL_AVAILABLE else None
179
+
180
+ def _create_header_panel(self) -> Panel:
181
+ """Create the header panel with title and version."""
182
+ title = Text(f"GitFlow Analytics v{self.version}", style="bold cyan", justify="center")
183
+ return Panel(
184
+ title,
185
+ box=box.DOUBLE,
186
+ padding=(0, 1),
187
+ style="bright_blue",
188
+ )
189
+
190
+ def _create_progress_panel(self) -> Panel:
191
+ """Create the main progress panel with prominent activity display."""
192
+ content_lines = []
193
+
194
+ # Overall progress with enhanced display
195
+ overall_text = Text("Overall Progress: ", style="bold cyan")
196
+ if self.statistics.processed_repositories > 0:
197
+ pct = (
198
+ self.statistics.processed_repositories / self.statistics.total_repositories
199
+ ) * 100
200
+ overall_text.append(
201
+ f"{self.statistics.processed_repositories}/{self.statistics.total_repositories} repositories ",
202
+ style="white",
203
+ )
204
+ overall_text.append(f"({pct:.1f}%)", style="bold green" if pct > 50 else "bold yellow")
205
+ content_lines.append(overall_text)
206
+ content_lines.append(self.overall_progress)
207
+
208
+ # Current repository progress - VERY prominent display
209
+ if self.current_repo:
210
+ content_lines.append(Text()) # Empty line for spacing
211
+ repo_info = self.repositories.get(self.current_repo)
212
+ if repo_info and repo_info.status == RepositoryStatus.PROCESSING:
213
+ # Animated activity indicator
214
+ spinner_frames = ["🔄", "🔃", "🔄", "🔃"]
215
+ frame_idx = int(time.time() * 2) % len(spinner_frames)
216
+
217
+ # Determine current action based on progress
218
+ if repo_info.commits == 0:
219
+ action = f"{spinner_frames[frame_idx]} Fetching commits from"
220
+ action_style = "bold yellow blink"
221
+ elif (
222
+ repo_info.total_commits > 0
223
+ and repo_info.commits < repo_info.total_commits * 0.3
224
+ ):
225
+ action = f"{spinner_frames[frame_idx]} Starting analysis of"
226
+ action_style = "bold yellow"
227
+ elif (
228
+ repo_info.total_commits > 0
229
+ and repo_info.commits < repo_info.total_commits * 0.7
230
+ ):
231
+ action = f"{spinner_frames[frame_idx]} Processing commits in"
232
+ action_style = "bold green"
233
+ else:
234
+ action = f"{spinner_frames[frame_idx]} Finalizing analysis of"
235
+ action_style = "bold cyan"
236
+
237
+ # Build the current activity text
238
+ current_text = Text(action + " ", style=action_style)
239
+ current_text.append(f"{repo_info.name}", style="bold white on blue")
240
+
241
+ # Add detailed progress info
242
+ if repo_info.total_commits > 0:
243
+ progress_pct = (repo_info.commits / repo_info.total_commits) * 100
244
+ current_text.append(
245
+ f"\n 📊 Progress: {repo_info.commits}/{repo_info.total_commits} commits ",
246
+ style="white",
247
+ )
248
+ current_text.append(f"({progress_pct:.1f}%)", style="bold green")
249
+
250
+ # Estimate time remaining
251
+ if repo_info.start_time and repo_info.commits > 0:
252
+ elapsed = (datetime.now() - repo_info.start_time).total_seconds()
253
+ rate = repo_info.commits / elapsed if elapsed > 0 else 0
254
+ remaining = (
255
+ (repo_info.total_commits - repo_info.commits) / rate if rate > 0 else 0
256
+ )
257
+ if remaining > 0:
258
+ current_text.append(f" - ETA: {remaining:.0f}s", style="dim white")
259
+ elif repo_info.commits > 0:
260
+ current_text.append(
261
+ f"\n 📊 Found {repo_info.commits} commits so far...", style="yellow"
262
+ )
263
+ else:
264
+ current_text.append("\n 📥 Cloning repository...", style="yellow blink")
265
+
266
+ content_lines.append(current_text)
267
+ content_lines.append(self.repo_progress)
268
+
269
+ # Create a group of all elements (Group already imported at top)
270
+ group_items = []
271
+ for item in content_lines:
272
+ group_items.append(item) # Both Text and Progress objects
273
+
274
+ return Panel(
275
+ Group(*group_items),
276
+ title="[bold]🚀 Live Progress Monitor[/bold]",
277
+ box=box.ROUNDED,
278
+ padding=(1, 2),
279
+ border_style="bright_blue",
280
+ )
281
+
282
+ def _create_repository_table(self) -> Panel:
283
+ """Create the repository status table with scrollable view."""
284
+ # Get terminal height to determine max visible rows
285
+ console_height = self.console.size.height
286
+ # Reserve space for header, progress, stats panels (approximately 18 lines)
287
+ available_height = max(10, console_height - 18)
288
+
289
+ table = Table(
290
+ show_header=True,
291
+ header_style="bold magenta",
292
+ box=box.SIMPLE_HEAD,
293
+ expand=True,
294
+ show_lines=False,
295
+ row_styles=["none", "dim"], # Alternate row colors for readability
296
+ )
297
+
298
+ table.add_column("#", width=4, justify="right", style="dim")
299
+ table.add_column("Repository", style="cyan", no_wrap=True, width=25)
300
+ table.add_column("Status", justify="center", width=15)
301
+ table.add_column("Progress", width=20)
302
+ table.add_column("Stats", justify="right", width=20)
303
+ table.add_column("Time", justify="right", width=8)
304
+
305
+ # Sort repositories: processing first, then error, then complete, then pending
306
+ sorted_repos = sorted(
307
+ self.repositories.values(),
308
+ key=lambda r: (
309
+ r.status != RepositoryStatus.PROCESSING,
310
+ r.status != RepositoryStatus.ERROR,
311
+ r.status != RepositoryStatus.COMPLETE,
312
+ r.name,
313
+ ),
314
+ )
315
+
316
+ # Calculate visible repositories
317
+ total_repos = len(sorted_repos)
318
+ visible_repos = sorted_repos[: available_height - 2] # Leave room for summary row
319
+
320
+ for idx, repo in enumerate(visible_repos, 1):
321
+ # Status with icon and animation for processing
322
+ if repo.status == RepositoryStatus.PROCESSING:
323
+ # Animated spinner for current repo
324
+ spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
325
+ frame_idx = int(time.time() * 10) % len(spinner_frames)
326
+ status_icon = spinner_frames[frame_idx]
327
+ status_text = Text(f"{status_icon} Processing", style="bold yellow")
328
+ else:
329
+ status_text = Text(
330
+ f"{repo.get_status_icon()} {repo.status.value.capitalize()}",
331
+ style=repo.get_status_color(),
332
+ )
333
+
334
+ # Progress bar for processing repos
335
+ progress_text = ""
336
+ if repo.status == RepositoryStatus.PROCESSING:
337
+ if repo.total_commits > 0:
338
+ progress_pct = (repo.commits / repo.total_commits) * 100
339
+ bar_width = 10
340
+ filled = int(bar_width * progress_pct / 100)
341
+ bar = "█" * filled + "░" * (bar_width - filled)
342
+ progress_text = f"[yellow]{bar}[/yellow] {progress_pct:.0f}%"
343
+ else:
344
+ progress_text = "[yellow]Fetching...[/yellow]"
345
+ elif repo.status == RepositoryStatus.COMPLETE:
346
+ progress_text = "[green]████████████[/green] 100%"
347
+ else:
348
+ progress_text = "[dim]────────────[/dim]"
349
+
350
+ # Stats column
351
+ stats_text = ""
352
+ if repo.commits > 0:
353
+ if repo.developers > 0:
354
+ stats_text = f"{repo.commits} commits, {repo.developers} devs"
355
+ else:
356
+ stats_text = f"{repo.commits} commits"
357
+ elif repo.status == RepositoryStatus.PROCESSING:
358
+ stats_text = "[yellow]Analyzing...[/yellow]"
359
+ else:
360
+ stats_text = "-"
361
+
362
+ # Time column
363
+ time_text = "-"
364
+ if repo.processing_time > 0:
365
+ time_text = f"{repo.processing_time:.1f}s"
366
+ elif repo.status == RepositoryStatus.PROCESSING and repo.start_time:
367
+ elapsed = (datetime.now() - repo.start_time).total_seconds()
368
+ time_text = f"[yellow]{elapsed:.0f}s[/yellow]"
369
+
370
+ table.add_row(
371
+ str(idx),
372
+ repo.name[:25], # Truncate long names
373
+ status_text,
374
+ progress_text,
375
+ stats_text,
376
+ time_text,
377
+ )
378
+
379
+ # Add summary row if there are more repositories
380
+ if total_repos > len(visible_repos):
381
+ remaining = total_repos - len(visible_repos)
382
+ table.add_row(
383
+ "...",
384
+ f"[dim italic]and {remaining} more repositories[/dim italic]",
385
+ "",
386
+ "",
387
+ "",
388
+ "",
389
+ )
390
+
391
+ # Add totals row
392
+ completed = sum(
393
+ 1 for r in self.repositories.values() if r.status == RepositoryStatus.COMPLETE
394
+ )
395
+ processing = sum(
396
+ 1 for r in self.repositories.values() if r.status == RepositoryStatus.PROCESSING
397
+ )
398
+ pending = sum(1 for r in self.repositories.values() if r.status == RepositoryStatus.PENDING)
399
+
400
+ title = f"[bold]Repository Status[/bold] (✅ {completed} | 🔄 {processing} | ⏸️ {pending})"
401
+
402
+ return Panel(
403
+ table,
404
+ title=title,
405
+ box=box.ROUNDED,
406
+ padding=(1, 2),
407
+ )
408
+
409
+ def _create_statistics_panel(self) -> Panel:
410
+ """Create the statistics panel with live updates."""
411
+ # Update system statistics (only if psutil is available)
412
+ with self._lock:
413
+ if self._process:
414
+ try:
415
+ self.statistics.memory_usage = self._process.memory_info().rss / 1024 / 1024
416
+ self.statistics.cpu_percent = self._process.cpu_percent()
417
+ except:
418
+ pass
419
+
420
+ stats_items = []
421
+
422
+ # Calculate overall completion percentage
423
+ if self.statistics.total_repositories > 0:
424
+ overall_pct = (
425
+ self.statistics.processed_repositories / self.statistics.total_repositories
426
+ ) * 100
427
+ completion_bar = self._create_mini_progress_bar(overall_pct, 20)
428
+ stats_items.append(f"[bold]Overall:[/bold] {completion_bar} {overall_pct:.1f}%")
429
+
430
+ # Main statistics row with live counters
431
+ main_stats = []
432
+ if self.statistics.total_commits > 0:
433
+ main_stats.append(
434
+ f"[bold cyan]📊 Commits:[/bold cyan] {self.statistics.total_commits:,}"
435
+ )
436
+ if self.statistics.total_developers > 0:
437
+ main_stats.append(
438
+ f"[bold cyan]👥 Developers:[/bold cyan] {self.statistics.total_developers}"
439
+ )
440
+ if self.statistics.total_tickets > 0:
441
+ main_stats.append(f"[bold cyan]🎫 Tickets:[/bold cyan] {self.statistics.total_tickets}")
442
+
443
+ if main_stats:
444
+ stats_items.append(" • ".join(main_stats))
445
+
446
+ # System performance with visual indicators
447
+ system_stats = []
448
+ if PSUTIL_AVAILABLE:
449
+ mem_icon = (
450
+ "🟢"
451
+ if self.statistics.memory_usage < 500
452
+ else "🟡" if self.statistics.memory_usage < 1000 else "🔴"
453
+ )
454
+ cpu_icon = (
455
+ "🟢"
456
+ if self.statistics.cpu_percent < 50
457
+ else "🟡" if self.statistics.cpu_percent < 80 else "🔴"
458
+ )
459
+ system_stats.append(f"{mem_icon} Memory: {self.statistics.memory_usage:.0f} MB")
460
+ system_stats.append(f"{cpu_icon} CPU: {self.statistics.cpu_percent:.1f}%")
461
+
462
+ if self.statistics.processing_speed > 0:
463
+ speed_icon = (
464
+ "🚀"
465
+ if self.statistics.processing_speed > 100
466
+ else "⚡" if self.statistics.processing_speed > 50 else "🐢"
467
+ )
468
+ system_stats.append(
469
+ f"{speed_icon} Speed: {self.statistics.processing_speed:.1f} commits/s"
470
+ )
471
+
472
+ if system_stats:
473
+ stats_items.append(" • ".join(system_stats))
474
+
475
+ # Enhanced phase display with activity indicator
476
+ phase_indicator = (
477
+ "⚙️"
478
+ if "Processing" in self.statistics.current_phase
479
+ else "🔍" if "Analyzing" in self.statistics.current_phase else "✨"
480
+ )
481
+ phase_text = f"{phase_indicator} [bold green]{self.statistics.current_phase}[/bold green]"
482
+ elapsed_text = f"⏱️ [bold blue]{self.statistics.get_elapsed_time()}[/bold blue]"
483
+
484
+ # Estimate total time if possible
485
+ eta_text = ""
486
+ if self.statistics.processed_repositories > 0 and self.statistics.total_repositories > 0:
487
+ if self.statistics.processed_repositories < self.statistics.total_repositories:
488
+ elapsed_seconds = (
489
+ (datetime.now() - self.statistics.start_time).total_seconds()
490
+ if self.statistics.start_time
491
+ else 0
492
+ )
493
+ if elapsed_seconds > 0:
494
+ rate = self.statistics.processed_repositories / elapsed_seconds
495
+ remaining = (
496
+ (
497
+ self.statistics.total_repositories
498
+ - self.statistics.processed_repositories
499
+ )
500
+ / rate
501
+ if rate > 0
502
+ else 0
503
+ )
504
+ if remaining > 0:
505
+ eta_text = f" • ETA: {timedelta(seconds=int(remaining))}"
506
+
507
+ stats_items.append(f"{phase_text} • {elapsed_text}{eta_text}")
508
+
509
+ content = "\n".join(stats_items)
510
+
511
+ return Panel(
512
+ content,
513
+ title="[bold]📈 Live Statistics[/bold]",
514
+ box=box.ROUNDED,
515
+ padding=(1, 2),
516
+ border_style=(
517
+ "green"
518
+ if self.statistics.processed_repositories == self.statistics.total_repositories
519
+ else "yellow"
520
+ ),
521
+ )
522
+
523
+ def _create_mini_progress_bar(self, percentage: float, width: int = 20) -> str:
524
+ """Create a mini progress bar for inline display."""
525
+ filled = int(width * percentage / 100)
526
+ bar = "█" * filled + "░" * (width - filled)
527
+ color = "green" if percentage >= 75 else "yellow" if percentage >= 50 else "cyan"
528
+ return f"[{color}]{bar}[/{color}]"
529
+
530
+ def _create_simple_layout(self) -> Panel:
531
+ """Create a simpler layout without embedded Progress objects."""
532
+ # Create a simple panel that we'll update dynamically
533
+ content = self._generate_display_content()
534
+ return Panel(
535
+ content,
536
+ title="[bold cyan]GitFlow Analytics Progress[/bold cyan]",
537
+ border_style="cyan",
538
+ padding=(1, 2),
539
+ )
540
+
541
+ def _generate_display_content(self) -> str:
542
+ """Generate the display content as a string."""
543
+ lines = []
544
+
545
+ # Header
546
+ lines.append(
547
+ f"[bold cyan]Analyzing {self.statistics.total_repositories} repositories[/bold cyan]"
548
+ )
549
+ lines.append("")
550
+
551
+ # Overall progress
552
+ if self.overall_task_id is not None:
553
+ task = self.overall_progress.tasks[0] if self.overall_progress.tasks else None
554
+ if task:
555
+ progress_pct = (task.completed / task.total * 100) if task.total > 0 else 0
556
+ bar = self._create_mini_progress_bar(progress_pct, 40)
557
+ lines.append(f"Overall Progress: {bar} {progress_pct:.1f}%")
558
+ lines.append(f"Status: {task.description}")
559
+
560
+ # Current repository
561
+ if self.current_repo:
562
+ lines.append("")
563
+ lines.append(f"[yellow]Current Repository:[/yellow] {self.current_repo}")
564
+ if self.repo_task_id is not None and self.repo_progress.tasks:
565
+ repo_task = self.repo_progress.tasks[0] if self.repo_progress.tasks else None
566
+ if repo_task:
567
+ lines.append(f" Commits: {repo_task.completed}/{repo_task.total}")
568
+
569
+ # Statistics
570
+ lines.append("")
571
+ lines.append("[bold green]Statistics:[/bold green]")
572
+ lines.append(
573
+ f" Processed: {self.statistics.processed_repositories}/{self.statistics.total_repositories}"
574
+ )
575
+ lines.append(f" Success: {self.statistics.successful_repositories}")
576
+ lines.append(f" Failed: {self.statistics.failed_repositories}")
577
+ lines.append(f" Skipped: {self.statistics.skipped_repositories}")
578
+
579
+ if self.statistics.total_commits_processed > 0:
580
+ lines.append(f" Total Commits: {self.statistics.total_commits_processed:,}")
581
+
582
+ # Repository list (last 5)
583
+ if self.repositories:
584
+ lines.append("")
585
+ lines.append("[bold]Recent Repositories:[/bold]")
586
+ recent = list(self.repositories.values())[-5:]
587
+ for repo in recent:
588
+ status_icon = {
589
+ RepositoryStatus.PENDING: "⏳",
590
+ RepositoryStatus.PROCESSING: "🔄",
591
+ RepositoryStatus.COMPLETE: "✅",
592
+ RepositoryStatus.ERROR: "❌",
593
+ RepositoryStatus.SKIPPED: "⏭️",
594
+ }.get(repo.status, "❓")
595
+ lines.append(f" {status_icon} {repo.name}")
596
+
597
+ return "\n".join(lines)
598
+
599
+ def _update_all_panels(self):
600
+ """Force update all panels in the layout."""
601
+ if self._layout and self._live:
602
+ # Update the simple panel with new content
603
+ new_content = self._generate_display_content()
604
+ self._layout.renderable = new_content
605
+ # Rich's auto_refresh handles the updates automatically
606
+ self._update_counter += 1
607
+
608
+ def start(self, total_items: int = 100, description: str = "Analyzing repositories"):
609
+ """
610
+ Start the progress display with full-screen live updates.
611
+
612
+ Args:
613
+ total_items: Total number of items to process
614
+ description: Description of the overall task
615
+ """
616
+ # Initialize statistics and progress without holding lock
617
+ self.statistics.start_time = datetime.now()
618
+ self.statistics.total_repositories = total_items
619
+ self.overall_task_id = self.overall_progress.add_task(description, total=total_items)
620
+
621
+ # Create a simpler layout that doesn't embed Progress objects
622
+ self._layout = self._create_simple_layout()
623
+
624
+ # Create and start Live display without holding any locks
625
+ try:
626
+ self._live = Live(
627
+ self._layout,
628
+ console=self.console,
629
+ refresh_per_second=2,
630
+ screen=True, # Full screen mode
631
+ auto_refresh=True,
632
+ )
633
+ self._live.start()
634
+ # Rich's auto_refresh will handle periodic updates
635
+ except Exception:
636
+ # Fallback to simple display if Live fails
637
+ self._live = None
638
+ self.console.print(
639
+ "[yellow]Note: Using simple progress display (Rich Live unavailable)[/yellow]"
640
+ )
641
+ self.console.print(Panel(f"GitFlow Analytics - {description}", title="Progress"))
642
+
643
+ def stop(self):
644
+ """Stop the progress display."""
645
+ with self._lock:
646
+ if self._live:
647
+ try:
648
+ self._live.stop()
649
+ except Exception:
650
+ pass # Ignore errors during cleanup
651
+ finally:
652
+ self._live = None
653
+ self._layout = None
654
+
655
+ def update_overall(self, completed: int, description: Optional[str] = None):
656
+ """Update overall progress."""
657
+ with self._lock:
658
+ if self.overall_task_id is not None:
659
+ update_kwargs = {"completed": completed}
660
+ if description:
661
+ update_kwargs["description"] = description
662
+ self.overall_progress.update(self.overall_task_id, **update_kwargs)
663
+
664
+ # Update the display with new content
665
+ self._update_all_panels()
666
+
667
+ def start_repository(self, repo_name: str, total_commits: int = 0):
668
+ """Start processing a repository with immediate visual feedback."""
669
+ with self._lock:
670
+ self.current_repo = repo_name
671
+
672
+ if repo_name not in self.repositories:
673
+ self.repositories[repo_name] = RepositoryInfo(name=repo_name)
674
+
675
+ repo_info = self.repositories[repo_name]
676
+ repo_info.status = RepositoryStatus.PROCESSING
677
+ repo_info.total_commits = total_commits
678
+ repo_info.start_time = datetime.now()
679
+
680
+ # Create or update repo progress task
681
+ if self.repo_task_id is not None:
682
+ self.repo_progress.remove_task(self.repo_task_id)
683
+
684
+ self.repo_task_id = self.repo_progress.add_task(
685
+ repo_name,
686
+ total=total_commits if total_commits > 0 else 100,
687
+ speed=0.0,
688
+ )
689
+
690
+ # Immediately update all panels to show the change
691
+ self._update_all_panels()
692
+
693
+ def update_repository(self, repo_name: str, commits: int, speed: float = 0.0):
694
+ """Update repository progress with continuous visual feedback."""
695
+ with self._lock:
696
+ if repo_name not in self.repositories:
697
+ return
698
+
699
+ repo_info = self.repositories[repo_name]
700
+ repo_info.commits = commits
701
+
702
+ if self.repo_task_id is not None and repo_name == self.current_repo:
703
+ self.repo_progress.update(
704
+ self.repo_task_id,
705
+ completed=commits,
706
+ speed=speed,
707
+ )
708
+
709
+ # Update overall statistics
710
+ self.statistics.processing_speed = speed
711
+
712
+ # Update total commits across all repos
713
+ self.statistics.total_commits = sum(r.commits for r in self.repositories.values())
714
+
715
+ # Force update all panels every time for continuous visual feedback
716
+ self._update_all_panels()
717
+
718
+ def finish_repository(
719
+ self, repo_name: str, success: bool = True, error_message: Optional[str] = None
720
+ ):
721
+ """Finish processing a repository with immediate status update."""
722
+ with self._lock:
723
+ if repo_name not in self.repositories:
724
+ return
725
+
726
+ repo_info = self.repositories[repo_name]
727
+ repo_info.status = RepositoryStatus.COMPLETE if success else RepositoryStatus.ERROR
728
+ repo_info.error_message = error_message
729
+
730
+ if repo_info.start_time:
731
+ repo_info.processing_time = (datetime.now() - repo_info.start_time).total_seconds()
732
+
733
+ self.statistics.processed_repositories += 1
734
+
735
+ # Immediately clear current repo if it was this one
736
+ if self.current_repo == repo_name:
737
+ self.current_repo = None
738
+ if self.repo_task_id is not None:
739
+ self.repo_progress.remove_task(self.repo_task_id)
740
+ self.repo_task_id = None
741
+
742
+ # Force immediate update to show completion
743
+ self._update_all_panels()
744
+
745
+ def update_statistics(self, **kwargs):
746
+ """
747
+ Update statistics.
748
+
749
+ Args:
750
+ **kwargs: Statistics to update (total_commits, total_developers, etc.)
751
+ """
752
+ with self._lock:
753
+ for key, value in kwargs.items():
754
+ if hasattr(self.statistics, key):
755
+ setattr(self.statistics, key, value)
756
+
757
+ if self._layout:
758
+ self._layout["stats"].update(self._create_statistics_panel())
759
+
760
+ def initialize_repositories(self, repository_list: list):
761
+ """Initialize all repositories with pending status and show them immediately.
762
+
763
+ Args:
764
+ repository_list: List of repositories to be processed.
765
+ Each item should have 'name' and optionally 'status' fields.
766
+ """
767
+ with self._lock:
768
+ # Pre-populate all repositories with their status
769
+ for idx, repo in enumerate(repository_list):
770
+ repo_name = repo.get("name", "Unknown")
771
+ status_str = repo.get("status", "pending")
772
+
773
+ # Map status string to enum
774
+ status_map = {
775
+ "pending": RepositoryStatus.PENDING,
776
+ "complete": RepositoryStatus.COMPLETE,
777
+ "processing": RepositoryStatus.PROCESSING,
778
+ "error": RepositoryStatus.ERROR,
779
+ "skipped": RepositoryStatus.SKIPPED,
780
+ }
781
+ status = status_map.get(status_str.lower(), RepositoryStatus.PENDING)
782
+
783
+ if repo_name not in self.repositories:
784
+ self.repositories[repo_name] = RepositoryInfo(
785
+ name=repo_name,
786
+ status=status,
787
+ )
788
+ else:
789
+ # Update existing status if needed
790
+ self.repositories[repo_name].status = status
791
+
792
+ # Update statistics
793
+ self.statistics.total_repositories = len(self.repositories)
794
+
795
+ # Set initial phase
796
+ if not self.statistics.current_phase or self.statistics.current_phase == "Initializing":
797
+ self.statistics.current_phase = (
798
+ f"Ready to process {len(self.repositories)} repositories"
799
+ )
800
+
801
+ # Force immediate update to show all repositories
802
+ self._update_all_panels()
803
+
804
+ def set_phase(self, phase: str):
805
+ """Set the current processing phase with immediate display update."""
806
+ with self._lock:
807
+ self.statistics.current_phase = phase
808
+ # Force immediate update to show phase change
809
+ self._update_all_panels()
810
+
811
+ @contextmanager
812
+ def progress_context(self, total_items: int = 100, description: str = "Processing"):
813
+ """Context manager for progress display."""
814
+ try:
815
+ self.start(total_items, description)
816
+ yield self
817
+ finally:
818
+ self.stop()
819
+
820
+ # Compatibility methods for CLI interface
821
+ def show_header(self):
822
+ """Display header - compatibility method for CLI."""
823
+ # The header is shown when start() is called, so we just need to print it
824
+ header_panel = self._create_header_panel()
825
+ self.console.print(header_panel)
826
+
827
+ def add_progress_task(self, task_id: str, description: str, total: int):
828
+ """Add a progress task - compatibility method."""
829
+ if task_id == "repos" or task_id == "main":
830
+ # Handle both "repos" and "main" for overall progress
831
+ if not self._live:
832
+ # Not in live mode, just print
833
+ self.console.print(f"[cyan]{description}[/cyan] (0/{total})")
834
+ return
835
+ # If Live display not started yet, start it now
836
+ if not self._live:
837
+ # Don't clear console - let Rich Live handle the screen management
838
+ self.start(total_items=total, description=description)
839
+ else:
840
+ # Update the existing overall progress description and total
841
+ if self.overall_task_id is not None:
842
+ self.overall_progress.update(
843
+ self.overall_task_id, description=description, total=total
844
+ )
845
+ elif task_id == "qualitative":
846
+ # Create a new task for qualitative analysis
847
+ with self._lock:
848
+ # Store task IDs in a dictionary for tracking
849
+ if not hasattr(self, "_task_ids"):
850
+ self._task_ids = {}
851
+ # Only add task if overall_progress is available
852
+ if self._live:
853
+ self._task_ids[task_id] = self.overall_progress.add_task(
854
+ description, total=total
855
+ )
856
+
857
+ def update_progress_task(
858
+ self,
859
+ task_id: str,
860
+ description: Optional[str] = None,
861
+ advance: int = 0,
862
+ completed: Optional[int] = None,
863
+ ):
864
+ """Update a progress task - compatibility method."""
865
+ # Handle simple mode
866
+ if self._live == "simple" and description:
867
+ self.console.print(f"[cyan]→ {description}[/cyan]")
868
+ return
869
+ if task_id == "repos" or task_id == "main":
870
+ # Update overall progress (handle both "repos" and "main" for compatibility)
871
+ if description:
872
+ self.update_overall(completed or 0, description)
873
+ elif advance:
874
+ if self.overall_task_id is not None:
875
+ self.overall_progress.advance(self.overall_task_id, advance)
876
+ elif hasattr(self, "_task_ids") and task_id in self._task_ids:
877
+ # Update specific task
878
+ update_kwargs = {}
879
+ if description:
880
+ update_kwargs["description"] = description
881
+ if completed is not None:
882
+ update_kwargs["completed"] = completed
883
+ if advance:
884
+ self.overall_progress.advance(self._task_ids[task_id], advance)
885
+ elif update_kwargs:
886
+ self.overall_progress.update(self._task_ids[task_id], **update_kwargs)
887
+
888
+ def complete_progress_task(self, task_id: str, description: str):
889
+ """Complete a progress task - compatibility method."""
890
+ if task_id == "repos":
891
+ # Mark overall task as complete
892
+ if self.overall_task_id is not None:
893
+ total = self.overall_progress.tasks[0].total if self.overall_progress.tasks else 100
894
+ self.overall_progress.update(
895
+ self.overall_task_id, description=description, completed=total
896
+ )
897
+ elif hasattr(self, "_task_ids") and task_id in self._task_ids:
898
+ # Complete specific task
899
+ task = None
900
+ for t in self.overall_progress.tasks:
901
+ if t.id == self._task_ids[task_id]:
902
+ task = t
903
+ break
904
+ if task:
905
+ self.overall_progress.update(
906
+ self._task_ids[task_id], description=description, completed=task.total
907
+ )
908
+
909
+ def print_status(self, message: str, style: str = "info"):
910
+ """Print a status message - compatibility method."""
911
+ styles = {"info": "cyan", "success": "green", "warning": "yellow", "error": "red"}
912
+ self.console.print(
913
+ f"[{styles.get(style, 'white')}]{message}[/{styles.get(style, 'white')}]"
914
+ )
915
+
916
+ def show_configuration_status(
917
+ self,
918
+ config_file,
919
+ github_org=None,
920
+ github_token_valid=False,
921
+ jira_configured=False,
922
+ jira_valid=False,
923
+ analysis_weeks=4,
924
+ **kwargs,
925
+ ):
926
+ """Display configuration status in a Rich format."""
927
+ table = Table(title="Configuration", box=box.ROUNDED)
928
+ table.add_column("Setting", style="cyan")
929
+ table.add_column("Value", style="white")
930
+
931
+ table.add_row("Config File", str(config_file))
932
+
933
+ if github_org:
934
+ table.add_row("GitHub Organization", github_org)
935
+ status = "✓ Valid" if github_token_valid else "✗ No token"
936
+ table.add_row("GitHub Token", status)
937
+
938
+ if jira_configured:
939
+ status = "✓ Valid" if jira_valid else "✗ Invalid"
940
+ table.add_row("JIRA Integration", status)
941
+
942
+ table.add_row("Analysis Period", f"{analysis_weeks} weeks")
943
+
944
+ # Add any additional kwargs passed
945
+ for key, value in kwargs.items():
946
+ formatted_key = key.replace("_", " ").title()
947
+ table.add_row(formatted_key, str(value))
948
+
949
+ self.console.print(table)
950
+
951
+ def show_repository_discovery(self, repositories):
952
+ """Display discovered repositories in a Rich format."""
953
+ table = Table(
954
+ title="📚 Discovered Repositories", box=box.ROUNDED, show_lines=True, highlight=True
955
+ )
956
+ table.add_column("#", style="dim", width=4, justify="right")
957
+ table.add_column("Repository", style="bold cyan", no_wrap=False)
958
+ table.add_column("Status", style="green", width=12)
959
+ table.add_column("GitHub", style="dim white", no_wrap=False)
960
+
961
+ for idx, repo in enumerate(repositories, 1):
962
+ name = repo.get("name", "Unknown")
963
+ status = repo.get("status", "Ready")
964
+ github_repo = repo.get("github_repo", "")
965
+
966
+ # Style the status based on its value
967
+ if "Local" in status or "exists" in status.lower():
968
+ status_style = "[green]" + status + "[/green]"
969
+ elif "Remote" in status or "clone" in status.lower():
970
+ status_style = "[yellow]" + status + "[/yellow]"
971
+ else:
972
+ status_style = status
973
+
974
+ table.add_row(str(idx), name, status_style, github_repo or "")
975
+
976
+ self.console.print(table)
977
+ self.console.print(f"\n[dim]Total repositories: {len(repositories)}[/dim]\n")
978
+
979
+ def show_error(self, message: str, show_debug_hint: bool = True):
980
+ """Display an error message in Rich format."""
981
+ error_panel = Panel(
982
+ Text(message, style="red"), title="[red]Error[/red]", border_style="red", padding=(1, 2)
983
+ )
984
+ self.console.print(error_panel)
985
+
986
+ if show_debug_hint:
987
+ self.console.print("[dim]Tip: Set GITFLOW_DEBUG=1 for more detailed output[/dim]")
988
+
989
+ def show_warning(self, message: str):
990
+ """Display a warning message in Rich format."""
991
+ warning_panel = Panel(
992
+ Text(message, style="yellow"),
993
+ title="[yellow]Warning[/yellow]",
994
+ border_style="yellow",
995
+ padding=(1, 2),
996
+ )
997
+ self.console.print(warning_panel)
998
+
999
+ def show_qualitative_stats(self, stats):
1000
+ """Display qualitative analysis statistics in Rich format."""
1001
+ table = Table(title="Qualitative Analysis Statistics", box=box.ROUNDED)
1002
+ table.add_column("Metric", style="cyan")
1003
+ table.add_column("Value", style="white")
1004
+
1005
+ if isinstance(stats, dict):
1006
+ for key, value in stats.items():
1007
+ # Format the key to be more readable
1008
+ formatted_key = key.replace("_", " ").title()
1009
+ formatted_value = str(value)
1010
+ table.add_row(formatted_key, formatted_value)
1011
+
1012
+ self.console.print(table)
1013
+
1014
+ def show_analysis_summary(self, commits, developers, tickets, prs=None, untracked=None):
1015
+ """Display analysis summary in Rich format."""
1016
+ summary = Table(title="Analysis Summary", box=box.ROUNDED)
1017
+ summary.add_column("Metric", style="cyan", width=30)
1018
+ summary.add_column("Count", style="green", width=20)
1019
+
1020
+ summary.add_row("Total Commits", str(commits))
1021
+ summary.add_row("Unique Developers", str(developers))
1022
+ summary.add_row("Tracked Tickets", str(tickets))
1023
+
1024
+ if prs is not None:
1025
+ summary.add_row("Pull Requests", str(prs))
1026
+
1027
+ if untracked is not None:
1028
+ summary.add_row("Untracked Commits", str(untracked))
1029
+
1030
+ self.console.print(summary)
1031
+
1032
+ def show_dora_metrics(self, metrics):
1033
+ """Display DORA metrics in Rich format."""
1034
+ if not metrics:
1035
+ return
1036
+
1037
+ table = Table(title="DORA Metrics", box=box.ROUNDED)
1038
+ table.add_column("Metric", style="cyan")
1039
+ table.add_column("Value", style="white")
1040
+ table.add_column("Rating", style="green")
1041
+
1042
+ # Format and display each DORA metric
1043
+ metric_names = {
1044
+ "deployment_frequency": "Deployment Frequency",
1045
+ "lead_time_for_changes": "Lead Time for Changes",
1046
+ "mean_time_to_recovery": "Mean Time to Recovery",
1047
+ "change_failure_rate": "Change Failure Rate",
1048
+ }
1049
+
1050
+ for key, name in metric_names.items():
1051
+ if key in metrics:
1052
+ value = metrics[key].get("value", "N/A")
1053
+ rating = metrics[key].get("rating", "")
1054
+ table.add_row(name, str(value), rating)
1055
+
1056
+ self.console.print(table)
1057
+
1058
+ def show_reports_generated(self, output_dir, reports):
1059
+ """Display generated reports information in Rich format."""
1060
+ table = Table(title=f"Reports Generated in {output_dir}", box=box.ROUNDED)
1061
+ table.add_column("Report Type", style="cyan")
1062
+ table.add_column("Filename", style="white")
1063
+
1064
+ for report in reports:
1065
+ if isinstance(report, dict):
1066
+ report_type = report.get("type", "Unknown")
1067
+ filename = report.get("filename", "N/A")
1068
+ else:
1069
+ # Handle simple string format
1070
+ report_type = "Report"
1071
+ filename = str(report)
1072
+
1073
+ table.add_row(report_type, filename)
1074
+
1075
+ self.console.print(table)
1076
+
1077
+ def show_llm_cost_summary(self, cost_stats):
1078
+ """Display LLM cost summary in Rich format."""
1079
+ if not cost_stats:
1080
+ return
1081
+
1082
+ table = Table(title="LLM Usage & Cost Summary", box=box.ROUNDED)
1083
+ table.add_column("Model", style="cyan")
1084
+ table.add_column("Requests", style="white")
1085
+ table.add_column("Tokens", style="white")
1086
+ table.add_column("Cost", style="green")
1087
+
1088
+ if isinstance(cost_stats, dict):
1089
+ for model, stats in cost_stats.items():
1090
+ requests = stats.get("requests", 0)
1091
+ tokens = stats.get("tokens", 0)
1092
+ cost = stats.get("cost", 0.0)
1093
+ table.add_row(model, str(requests), str(tokens), f"${cost:.4f}")
1094
+
1095
+ self.console.print(table)
1096
+
1097
+ def start_live_display(self):
1098
+ """Start live display - compatibility wrapper for start()."""
1099
+ if not self.overall_task_id:
1100
+ self.start(total_items=100, description="Processing")
1101
+
1102
+ def stop_live_display(self):
1103
+ """Stop live display - compatibility wrapper for stop()."""
1104
+ self.stop()
1105
+
1106
+
1107
+ class SimpleProgressDisplay:
1108
+ """Fallback progress display using tqdm when Rich is not available."""
1109
+
1110
+ def __init__(self, version: str = "1.3.11", update_frequency: float = 0.5):
1111
+ """Initialize simple progress display."""
1112
+ from tqdm import tqdm
1113
+
1114
+ self.tqdm = tqdm
1115
+ self.version = version
1116
+ self.overall_progress = None
1117
+ self.repo_progress = None
1118
+ self.repositories = {}
1119
+ self.statistics = ProgressStatistics()
1120
+
1121
+ def start(self, total_items: int = 100, description: str = "Analyzing repositories"):
1122
+ """Start progress display."""
1123
+ self.overall_progress = self.tqdm(
1124
+ total=total_items,
1125
+ desc=description,
1126
+ unit="items",
1127
+ )
1128
+ self.statistics.start_time = datetime.now()
1129
+
1130
+ def stop(self):
1131
+ """Stop progress display."""
1132
+ if self.overall_progress:
1133
+ self.overall_progress.close()
1134
+ if self.repo_progress:
1135
+ self.repo_progress.close()
1136
+
1137
+ def update_overall(self, completed: int, description: Optional[str] = None):
1138
+ """Update overall progress."""
1139
+ if self.overall_progress:
1140
+ self.overall_progress.n = completed
1141
+ if description:
1142
+ self.overall_progress.set_description(description)
1143
+ self.overall_progress.refresh()
1144
+
1145
+ def start_repository(self, repo_name: str, total_commits: int = 0):
1146
+ """Start processing a repository."""
1147
+ if self.repo_progress:
1148
+ self.repo_progress.close()
1149
+
1150
+ self.repositories[repo_name] = RepositoryInfo(
1151
+ name=repo_name,
1152
+ status=RepositoryStatus.PROCESSING,
1153
+ total_commits=total_commits,
1154
+ start_time=datetime.now(),
1155
+ )
1156
+
1157
+ # Enhanced description to show what's happening
1158
+ action = "Analyzing" if total_commits > 0 else "Fetching"
1159
+ desc = f"{action} repository: {repo_name}"
1160
+
1161
+ self.repo_progress = self.tqdm(
1162
+ total=total_commits if total_commits > 0 else 100,
1163
+ desc=desc,
1164
+ unit="commits",
1165
+ leave=False,
1166
+ )
1167
+
1168
+ def update_repository(self, repo_name: str, commits: int, speed: float = 0.0):
1169
+ """Update repository progress."""
1170
+ if self.repo_progress and repo_name in self.repositories:
1171
+ self.repo_progress.n = commits
1172
+ self.repo_progress.set_postfix(speed=f"{speed:.1f} c/s")
1173
+ self.repo_progress.refresh()
1174
+ self.repositories[repo_name].commits = commits
1175
+
1176
+ def finish_repository(
1177
+ self, repo_name: str, success: bool = True, error_message: Optional[str] = None
1178
+ ):
1179
+ """Finish processing a repository."""
1180
+ if repo_name in self.repositories:
1181
+ repo_info = self.repositories[repo_name]
1182
+ repo_info.status = RepositoryStatus.COMPLETE if success else RepositoryStatus.ERROR
1183
+ repo_info.error_message = error_message
1184
+ if repo_info.start_time:
1185
+ repo_info.processing_time = (datetime.now() - repo_info.start_time).total_seconds()
1186
+
1187
+ if self.repo_progress:
1188
+ self.repo_progress.close()
1189
+ self.repo_progress = None
1190
+
1191
+ def update_statistics(self, **kwargs):
1192
+ """Update statistics."""
1193
+ for key, value in kwargs.items():
1194
+ if hasattr(self.statistics, key):
1195
+ setattr(self.statistics, key, value)
1196
+
1197
+ def initialize_repositories(self, repository_list: list):
1198
+ """Initialize all repositories with their status.
1199
+
1200
+ Args:
1201
+ repository_list: List of repositories to be processed.
1202
+ """
1203
+ # Pre-populate all repositories with their status
1204
+ for repo in repository_list:
1205
+ repo_name = repo.get("name", "Unknown")
1206
+ status_str = repo.get("status", "pending")
1207
+
1208
+ # Map status string to enum
1209
+ status_map = {
1210
+ "pending": RepositoryStatus.PENDING,
1211
+ "complete": RepositoryStatus.COMPLETE,
1212
+ "processing": RepositoryStatus.PROCESSING,
1213
+ "error": RepositoryStatus.ERROR,
1214
+ "skipped": RepositoryStatus.SKIPPED,
1215
+ }
1216
+ status = status_map.get(status_str.lower(), RepositoryStatus.PENDING)
1217
+
1218
+ if repo_name not in self.repositories:
1219
+ self.repositories[repo_name] = RepositoryInfo(
1220
+ name=repo_name,
1221
+ status=status,
1222
+ )
1223
+ else:
1224
+ # Update existing status if needed
1225
+ self.repositories[repo_name].status = status
1226
+ self.statistics.total_repositories = len(self.repositories)
1227
+
1228
+ def set_phase(self, phase: str):
1229
+ """Set the current processing phase."""
1230
+ self.statistics.current_phase = phase
1231
+ if self.overall_progress:
1232
+ self.overall_progress.set_description(f"{phase}")
1233
+
1234
+ @contextmanager
1235
+ def progress_context(self, total_items: int = 100, description: str = "Processing"):
1236
+ """Context manager for progress display."""
1237
+ try:
1238
+ self.start(total_items, description)
1239
+ yield self
1240
+ finally:
1241
+ self.stop()
1242
+
1243
+ # Compatibility methods for CLI interface
1244
+ def show_header(self):
1245
+ """Display header - compatibility method for CLI."""
1246
+ print(f"\n{'='*60}")
1247
+ print(f"GitFlow Analytics v{self.version}")
1248
+ print(f"{'='*60}\n")
1249
+
1250
+ def start_live_display(self):
1251
+ """Start live display - compatibility wrapper for start()."""
1252
+ if not self.overall_progress:
1253
+ self.start(total_items=100, description="Processing")
1254
+
1255
+ def stop_live_display(self):
1256
+ """Stop live display - compatibility wrapper for stop()."""
1257
+ self.stop()
1258
+
1259
+ def add_progress_task(self, task_id: str, description: str, total: int):
1260
+ """Add a progress task - compatibility method."""
1261
+ # Store task information for later use
1262
+ if not hasattr(self, "_tasks"):
1263
+ self._tasks = {}
1264
+ self._tasks[task_id] = {"description": description, "total": total, "progress": None}
1265
+
1266
+ if task_id == "repos":
1267
+ # Update overall progress
1268
+ if self.overall_progress:
1269
+ self.overall_progress.total = total
1270
+ self.overall_progress.set_description(description)
1271
+ elif task_id == "qualitative":
1272
+ # For qualitative, we might create a separate progress bar
1273
+ from tqdm import tqdm
1274
+
1275
+ self._tasks[task_id]["progress"] = tqdm(
1276
+ total=total, desc=description, unit="items", leave=False
1277
+ )
1278
+
1279
+ def update_progress_task(
1280
+ self,
1281
+ task_id: str,
1282
+ description: Optional[str] = None,
1283
+ advance: int = 0,
1284
+ completed: Optional[int] = None,
1285
+ ):
1286
+ """Update a progress task - compatibility method."""
1287
+ if task_id == "repos" and self.overall_progress:
1288
+ if description:
1289
+ self.overall_progress.set_description(description)
1290
+ if advance:
1291
+ self.overall_progress.update(advance)
1292
+ if completed is not None:
1293
+ self.overall_progress.n = completed
1294
+ self.overall_progress.refresh()
1295
+ elif hasattr(self, "_tasks") and task_id in self._tasks:
1296
+ task = self._tasks[task_id].get("progress")
1297
+ if task:
1298
+ if description:
1299
+ task.set_description(description)
1300
+ if advance:
1301
+ task.update(advance)
1302
+ if completed is not None:
1303
+ task.n = completed
1304
+ task.refresh()
1305
+
1306
+ def complete_progress_task(self, task_id: str, description: str):
1307
+ """Complete a progress task - compatibility method."""
1308
+ if task_id == "repos" and self.overall_progress:
1309
+ self.overall_progress.set_description(description)
1310
+ self.overall_progress.n = self.overall_progress.total
1311
+ self.overall_progress.refresh()
1312
+ elif hasattr(self, "_tasks") and task_id in self._tasks:
1313
+ task = self._tasks[task_id].get("progress")
1314
+ if task:
1315
+ task.set_description(description)
1316
+ task.n = task.total
1317
+ task.close()
1318
+ self._tasks[task_id]["progress"] = None
1319
+
1320
+ def print_status(self, message: str, style: str = "info"):
1321
+ """Print a status message - compatibility method."""
1322
+ # Simple console print with basic styling
1323
+ prefix = {"info": "ℹ️ ", "success": "✅ ", "warning": "⚠️ ", "error": "❌ "}.get(style, "")
1324
+ print(f"{prefix}{message}")
1325
+
1326
+ def show_configuration_status(
1327
+ self,
1328
+ config_file,
1329
+ github_org=None,
1330
+ github_token_valid=False,
1331
+ jira_configured=False,
1332
+ jira_valid=False,
1333
+ analysis_weeks=4,
1334
+ **kwargs,
1335
+ ):
1336
+ """Display configuration status in simple format."""
1337
+ print("\n=== Configuration ===")
1338
+ print(f"Config File: {config_file}")
1339
+
1340
+ if github_org:
1341
+ print(f"GitHub Organization: {github_org}")
1342
+ status = "✓ Valid" if github_token_valid else "✗ No token"
1343
+ print(f"GitHub Token: {status}")
1344
+
1345
+ if jira_configured:
1346
+ status = "✓ Valid" if jira_valid else "✗ Invalid"
1347
+ print(f"JIRA Integration: {status}")
1348
+
1349
+ print(f"Analysis Period: {analysis_weeks} weeks")
1350
+
1351
+ # Add any additional kwargs passed
1352
+ for key, value in kwargs.items():
1353
+ formatted_key = key.replace("_", " ").title()
1354
+ print(f"{formatted_key}: {value}")
1355
+
1356
+ print("==================\n")
1357
+
1358
+ def show_repository_discovery(self, repositories):
1359
+ """Display discovered repositories in simple format."""
1360
+ print("\n📚 === Discovered Repositories ===")
1361
+ for idx, repo in enumerate(repositories, 1):
1362
+ name = repo.get("name", "Unknown")
1363
+ status = repo.get("status", "Ready")
1364
+ github_repo = repo.get("github_repo", "")
1365
+
1366
+ # Format the output line
1367
+ if github_repo:
1368
+ print(f" {idx:2}. {name:30} {status:12} ({github_repo})")
1369
+ else:
1370
+ print(f" {idx:2}. {name:30} {status}")
1371
+ print(f"\nTotal repositories: {len(repositories)}")
1372
+ print("============================\n")
1373
+
1374
+ def show_error(self, message: str, show_debug_hint: bool = True):
1375
+ """Display an error message in simple format."""
1376
+ print(f"\n❌ ERROR: {message}")
1377
+ if show_debug_hint:
1378
+ print("Tip: Set GITFLOW_DEBUG=1 for more detailed output")
1379
+ print("")
1380
+
1381
+ def show_warning(self, message: str):
1382
+ """Display a warning message in simple format."""
1383
+ print(f"\n⚠️ WARNING: {message}\n")
1384
+
1385
+ def show_qualitative_stats(self, stats):
1386
+ """Display qualitative analysis statistics in simple format."""
1387
+ print("\n=== Qualitative Analysis Statistics ===")
1388
+ if isinstance(stats, dict):
1389
+ for key, value in stats.items():
1390
+ formatted_key = key.replace("_", " ").title()
1391
+ print(f" {formatted_key}: {value}")
1392
+ print("=====================================\n")
1393
+
1394
+ def show_analysis_summary(self, commits, developers, tickets, prs=None, untracked=None):
1395
+ """Display analysis summary in simple format."""
1396
+ print("\n=== Analysis Summary ===")
1397
+ print(f" Total Commits: {commits}")
1398
+ print(f" Unique Developers: {developers}")
1399
+ print(f" Tracked Tickets: {tickets}")
1400
+ if prs is not None:
1401
+ print(f" Pull Requests: {prs}")
1402
+ if untracked is not None:
1403
+ print(f" Untracked Commits: {untracked}")
1404
+ print("======================\n")
1405
+
1406
+ def show_dora_metrics(self, metrics):
1407
+ """Display DORA metrics in simple format."""
1408
+ if not metrics:
1409
+ return
1410
+
1411
+ print("\n=== DORA Metrics ===")
1412
+ metric_names = {
1413
+ "deployment_frequency": "Deployment Frequency",
1414
+ "lead_time_for_changes": "Lead Time for Changes",
1415
+ "mean_time_to_recovery": "Mean Time to Recovery",
1416
+ "change_failure_rate": "Change Failure Rate",
1417
+ }
1418
+
1419
+ for key, name in metric_names.items():
1420
+ if key in metrics:
1421
+ value = metrics[key].get("value", "N/A")
1422
+ rating = metrics[key].get("rating", "")
1423
+ print(f" {name}: {value} {f'({rating})' if rating else ''}")
1424
+ print("==================\n")
1425
+
1426
+ def show_reports_generated(self, output_dir, reports):
1427
+ """Display generated reports information in simple format."""
1428
+ print(f"\n=== Reports Generated in {output_dir} ===")
1429
+ for report in reports:
1430
+ if isinstance(report, dict):
1431
+ report_type = report.get("type", "Unknown")
1432
+ filename = report.get("filename", "N/A")
1433
+ print(f" {report_type}: {filename}")
1434
+ else:
1435
+ print(f" Report: {report}")
1436
+ print("=====================================\n")
1437
+
1438
+ def show_llm_cost_summary(self, cost_stats):
1439
+ """Display LLM cost summary in simple format."""
1440
+ if not cost_stats:
1441
+ return
1442
+
1443
+ print("\n=== LLM Usage & Cost Summary ===")
1444
+ if isinstance(cost_stats, dict):
1445
+ for model, stats in cost_stats.items():
1446
+ requests = stats.get("requests", 0)
1447
+ tokens = stats.get("tokens", 0)
1448
+ cost = stats.get("cost", 0.0)
1449
+ print(f" {model}:")
1450
+ print(f" Requests: {requests}")
1451
+ print(f" Tokens: {tokens}")
1452
+ print(f" Cost: ${cost:.4f}")
1453
+ print("==============================\n")
1454
+
1455
+
1456
+ def create_progress_display(
1457
+ style: str = "auto", version: str = "1.3.11", update_frequency: float = 0.5
1458
+ ) -> Any:
1459
+ """
1460
+ Create a progress display based on configuration.
1461
+
1462
+ Args:
1463
+ style: Display style ("rich", "simple", or "auto")
1464
+ version: GitFlow Analytics version
1465
+ update_frequency: Update frequency in seconds
1466
+
1467
+ Returns:
1468
+ Progress display instance
1469
+ """
1470
+ if style == "rich" or (style == "auto" and RICH_AVAILABLE):
1471
+ try:
1472
+ return RichProgressDisplay(version, update_frequency)
1473
+ except Exception:
1474
+ # Fall back to simple if Rich fails
1475
+ pass
1476
+
1477
+ return SimpleProgressDisplay(version, update_frequency)