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.
- gitflow_analytics/_version.py +1 -1
- gitflow_analytics/classification/batch_classifier.py +156 -4
- gitflow_analytics/cli.py +897 -179
- gitflow_analytics/config/loader.py +40 -1
- gitflow_analytics/config/schema.py +4 -0
- gitflow_analytics/core/cache.py +20 -0
- gitflow_analytics/core/data_fetcher.py +1254 -228
- gitflow_analytics/core/git_auth.py +169 -0
- gitflow_analytics/core/git_timeout_wrapper.py +347 -0
- gitflow_analytics/core/metrics_storage.py +12 -3
- gitflow_analytics/core/progress.py +219 -18
- gitflow_analytics/core/subprocess_git.py +145 -0
- gitflow_analytics/extractors/ml_tickets.py +3 -2
- gitflow_analytics/extractors/tickets.py +93 -8
- gitflow_analytics/integrations/jira_integration.py +1 -1
- gitflow_analytics/integrations/orchestrator.py +47 -29
- gitflow_analytics/metrics/branch_health.py +3 -2
- gitflow_analytics/models/database.py +72 -1
- gitflow_analytics/pm_framework/adapters/jira_adapter.py +12 -5
- gitflow_analytics/pm_framework/orchestrator.py +8 -3
- gitflow_analytics/qualitative/classifiers/llm/openai_client.py +24 -4
- gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +3 -1
- gitflow_analytics/qualitative/core/llm_fallback.py +34 -2
- gitflow_analytics/reports/narrative_writer.py +118 -74
- gitflow_analytics/security/__init__.py +11 -0
- gitflow_analytics/security/config.py +189 -0
- gitflow_analytics/security/extractors/__init__.py +7 -0
- gitflow_analytics/security/extractors/dependency_checker.py +379 -0
- gitflow_analytics/security/extractors/secret_detector.py +197 -0
- gitflow_analytics/security/extractors/vulnerability_scanner.py +333 -0
- gitflow_analytics/security/llm_analyzer.py +347 -0
- gitflow_analytics/security/reports/__init__.py +5 -0
- gitflow_analytics/security/reports/security_report.py +358 -0
- gitflow_analytics/security/security_analyzer.py +414 -0
- gitflow_analytics/tui/app.py +3 -1
- gitflow_analytics/tui/progress_adapter.py +313 -0
- gitflow_analytics/tui/screens/analysis_progress_screen.py +407 -46
- gitflow_analytics/tui/screens/results_screen.py +219 -206
- gitflow_analytics/ui/__init__.py +21 -0
- gitflow_analytics/ui/progress_display.py +1477 -0
- gitflow_analytics/verify_activity.py +697 -0
- {gitflow_analytics-1.3.6.dist-info → gitflow_analytics-3.3.0.dist-info}/METADATA +2 -1
- {gitflow_analytics-1.3.6.dist-info → gitflow_analytics-3.3.0.dist-info}/RECORD +47 -31
- gitflow_analytics/cli_rich.py +0 -503
- {gitflow_analytics-1.3.6.dist-info → gitflow_analytics-3.3.0.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.3.6.dist-info → gitflow_analytics-3.3.0.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.3.6.dist-info → gitflow_analytics-3.3.0.dist-info}/licenses/LICENSE +0 -0
- {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)
|