gitflow-analytics 3.6.1__py3-none-any.whl → 3.7.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/__init__.py +8 -12
- gitflow_analytics/_version.py +1 -1
- gitflow_analytics/cli.py +156 -175
- gitflow_analytics/cli_wizards/install_wizard.py +5 -5
- gitflow_analytics/core/cache.py +3 -3
- gitflow_analytics/models/database.py +279 -45
- gitflow_analytics/security/reports/__init__.py +5 -0
- gitflow_analytics/security/reports/security_report.py +358 -0
- {gitflow_analytics-3.6.1.dist-info → gitflow_analytics-3.7.0.dist-info}/METADATA +2 -4
- {gitflow_analytics-3.6.1.dist-info → gitflow_analytics-3.7.0.dist-info}/RECORD +14 -25
- gitflow_analytics/tui/__init__.py +0 -5
- gitflow_analytics/tui/app.py +0 -726
- gitflow_analytics/tui/progress_adapter.py +0 -313
- gitflow_analytics/tui/screens/__init__.py +0 -8
- gitflow_analytics/tui/screens/analysis_progress_screen.py +0 -857
- gitflow_analytics/tui/screens/configuration_screen.py +0 -523
- gitflow_analytics/tui/screens/loading_screen.py +0 -348
- gitflow_analytics/tui/screens/main_screen.py +0 -321
- gitflow_analytics/tui/screens/results_screen.py +0 -735
- gitflow_analytics/tui/widgets/__init__.py +0 -7
- gitflow_analytics/tui/widgets/data_table.py +0 -255
- gitflow_analytics/tui/widgets/export_modal.py +0 -301
- gitflow_analytics/tui/widgets/progress_widget.py +0 -187
- {gitflow_analytics-3.6.1.dist-info → gitflow_analytics-3.7.0.dist-info}/WHEEL +0 -0
- {gitflow_analytics-3.6.1.dist-info → gitflow_analytics-3.7.0.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-3.6.1.dist-info → gitflow_analytics-3.7.0.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-3.6.1.dist-info → gitflow_analytics-3.7.0.dist-info}/top_level.txt +0 -0
|
@@ -1,735 +0,0 @@
|
|
|
1
|
-
"""Results screen for GitFlow Analytics TUI."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Any, Optional
|
|
7
|
-
|
|
8
|
-
from rich.table import Table
|
|
9
|
-
from textual.binding import Binding
|
|
10
|
-
from textual.containers import Container, Horizontal, ScrollableContainer, Vertical
|
|
11
|
-
from textual.screen import Screen
|
|
12
|
-
from textual.widgets import (
|
|
13
|
-
Button,
|
|
14
|
-
Footer,
|
|
15
|
-
Header,
|
|
16
|
-
Label,
|
|
17
|
-
RadioButton,
|
|
18
|
-
Rule,
|
|
19
|
-
Static,
|
|
20
|
-
TabbedContent,
|
|
21
|
-
TabPane,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
from gitflow_analytics.config import Config
|
|
25
|
-
|
|
26
|
-
from ..widgets.data_table import EnhancedDataTable
|
|
27
|
-
from ..widgets.export_modal import ExportModal
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class ResultsScreen(Screen):
|
|
31
|
-
"""
|
|
32
|
-
Screen displaying comprehensive analysis results with interactive exploration.
|
|
33
|
-
|
|
34
|
-
WHY: Analysis results are complex and multi-dimensional, requiring an
|
|
35
|
-
interactive interface that allows users to explore different aspects of
|
|
36
|
-
the data. Tabbed layout organizes information logically while providing
|
|
37
|
-
powerful data exploration capabilities.
|
|
38
|
-
|
|
39
|
-
DESIGN DECISION: Uses tabbed interface to separate different result categories
|
|
40
|
-
while providing consistent export functionality across all views. Interactive
|
|
41
|
-
tables allow users to sort, filter, and drill down into specific data points.
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
BINDINGS = [
|
|
45
|
-
Binding("escape", "back", "Back to Main"),
|
|
46
|
-
Binding("ctrl+s", "export", "Export Results"),
|
|
47
|
-
Binding("ctrl+f", "filter", "Filter Data"),
|
|
48
|
-
Binding("r", "refresh", "Refresh View"),
|
|
49
|
-
Binding("ctrl+e", "export_current", "Export Current View"),
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
def __init__(
|
|
53
|
-
self,
|
|
54
|
-
commits: list[dict],
|
|
55
|
-
prs: list[dict],
|
|
56
|
-
developers: list[dict],
|
|
57
|
-
config: Config,
|
|
58
|
-
*,
|
|
59
|
-
name: Optional[str] = None,
|
|
60
|
-
id: Optional[str] = None,
|
|
61
|
-
) -> None:
|
|
62
|
-
super().__init__(name=name, id=id)
|
|
63
|
-
self.commits = commits
|
|
64
|
-
self.prs = prs
|
|
65
|
-
self.developers = developers
|
|
66
|
-
self.config = config
|
|
67
|
-
self.current_tab = "summary"
|
|
68
|
-
|
|
69
|
-
def compose(self):
|
|
70
|
-
"""Compose the results screen."""
|
|
71
|
-
yield Header()
|
|
72
|
-
|
|
73
|
-
with Container(id="results-container"):
|
|
74
|
-
yield Label("GitFlow Analytics - Results", classes="screen-title")
|
|
75
|
-
|
|
76
|
-
with TabbedContent(initial="summary"):
|
|
77
|
-
# Summary Tab
|
|
78
|
-
with TabPane("Summary", id="summary"):
|
|
79
|
-
yield self._create_summary_panel()
|
|
80
|
-
|
|
81
|
-
# Developers Tab
|
|
82
|
-
with TabPane("Developers", id="developers"):
|
|
83
|
-
yield self._create_developers_panel()
|
|
84
|
-
|
|
85
|
-
# Commits Tab
|
|
86
|
-
with TabPane("Commits", id="commits"):
|
|
87
|
-
yield self._create_commits_panel()
|
|
88
|
-
|
|
89
|
-
# Pull Requests Tab (if available)
|
|
90
|
-
if self.prs:
|
|
91
|
-
with TabPane("Pull Requests", id="pull-requests"):
|
|
92
|
-
yield self._create_prs_panel()
|
|
93
|
-
|
|
94
|
-
# Qualitative Insights Tab (if available)
|
|
95
|
-
if self._has_qualitative_data():
|
|
96
|
-
with TabPane("Qualitative Insights", id="qualitative"):
|
|
97
|
-
yield self._create_qualitative_panel()
|
|
98
|
-
|
|
99
|
-
# Export Tab
|
|
100
|
-
with TabPane("Export", id="export"):
|
|
101
|
-
yield self._create_export_panel()
|
|
102
|
-
|
|
103
|
-
yield Footer()
|
|
104
|
-
|
|
105
|
-
def _create_summary_panel(self) -> ScrollableContainer:
|
|
106
|
-
"""
|
|
107
|
-
Create comprehensive summary statistics panel.
|
|
108
|
-
|
|
109
|
-
WHY: Provides high-level overview of all analysis results in a single view,
|
|
110
|
-
allowing users to quickly understand the overall scope and key metrics
|
|
111
|
-
without diving into detailed data tables.
|
|
112
|
-
"""
|
|
113
|
-
# Build all widgets first, then add to container
|
|
114
|
-
widgets = []
|
|
115
|
-
|
|
116
|
-
# Key metrics section
|
|
117
|
-
widgets.append(Label("Analysis Summary", classes="section-title"))
|
|
118
|
-
|
|
119
|
-
# Create summary table
|
|
120
|
-
summary_table = Table(show_header=False, show_edge=False, pad_edge=False)
|
|
121
|
-
summary_table.add_column("Metric", style="bold cyan", width=25)
|
|
122
|
-
summary_table.add_column("Value", style="green", width=15)
|
|
123
|
-
summary_table.add_column("Details", style="dim", width=40)
|
|
124
|
-
|
|
125
|
-
# Calculate key metrics
|
|
126
|
-
total_commits = len(self.commits)
|
|
127
|
-
total_prs = len(self.prs)
|
|
128
|
-
total_developers = len(self.developers)
|
|
129
|
-
|
|
130
|
-
# Time range
|
|
131
|
-
if self.commits:
|
|
132
|
-
dates = [c.get("timestamp") for c in self.commits if c.get("timestamp")]
|
|
133
|
-
if dates:
|
|
134
|
-
min_date = min(dates).strftime("%Y-%m-%d")
|
|
135
|
-
max_date = max(dates).strftime("%Y-%m-%d")
|
|
136
|
-
date_range = f"{min_date} to {max_date}"
|
|
137
|
-
else:
|
|
138
|
-
date_range = "Unknown"
|
|
139
|
-
else:
|
|
140
|
-
date_range = "No data"
|
|
141
|
-
|
|
142
|
-
# Story points
|
|
143
|
-
total_story_points = sum(c.get("story_points", 0) or 0 for c in self.commits)
|
|
144
|
-
|
|
145
|
-
# Ticket coverage
|
|
146
|
-
commits_with_tickets = sum(1 for c in self.commits if c.get("ticket_references"))
|
|
147
|
-
ticket_coverage = (commits_with_tickets / total_commits * 100) if total_commits > 0 else 0
|
|
148
|
-
|
|
149
|
-
# Add metrics to table
|
|
150
|
-
summary_table.add_row(
|
|
151
|
-
"Total Commits", f"{total_commits:,}", "All commits in analysis period"
|
|
152
|
-
)
|
|
153
|
-
summary_table.add_row("Total Pull Requests", f"{total_prs:,}", "Detected pull requests")
|
|
154
|
-
summary_table.add_row(
|
|
155
|
-
"Active Developers", f"{total_developers:,}", "Unique developer identities"
|
|
156
|
-
)
|
|
157
|
-
summary_table.add_row("Analysis Period", date_range, "Date range of analyzed commits")
|
|
158
|
-
summary_table.add_row(
|
|
159
|
-
"Story Points", f"{total_story_points:,}", "Total story points completed"
|
|
160
|
-
)
|
|
161
|
-
summary_table.add_row(
|
|
162
|
-
"Ticket Coverage", f"{ticket_coverage:.1f}%", "Commits with ticket references"
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
from rich.panel import Panel
|
|
166
|
-
|
|
167
|
-
widgets.append(Static(Panel(summary_table, title="Key Metrics", border_style="blue")))
|
|
168
|
-
|
|
169
|
-
# Top contributors section
|
|
170
|
-
widgets.append(Rule())
|
|
171
|
-
widgets.append(Label("Top Contributors", classes="section-title"))
|
|
172
|
-
|
|
173
|
-
if self.developers:
|
|
174
|
-
top_devs = sorted(
|
|
175
|
-
self.developers, key=lambda d: d.get("total_commits", 0), reverse=True
|
|
176
|
-
)[:10]
|
|
177
|
-
|
|
178
|
-
contrib_table = Table(show_header=True, header_style="bold magenta")
|
|
179
|
-
contrib_table.add_column("Developer", width=25)
|
|
180
|
-
contrib_table.add_column("Commits", justify="right", width=10)
|
|
181
|
-
contrib_table.add_column("Story Points", justify="right", width=12)
|
|
182
|
-
contrib_table.add_column("Avg Points/Commit", justify="right", width=15)
|
|
183
|
-
|
|
184
|
-
for dev in top_devs:
|
|
185
|
-
commits = dev.get("total_commits", 0)
|
|
186
|
-
points = dev.get("total_story_points", 0)
|
|
187
|
-
avg_points = points / commits if commits > 0 else 0
|
|
188
|
-
|
|
189
|
-
contrib_table.add_row(
|
|
190
|
-
dev.get("primary_name", "Unknown")[:23],
|
|
191
|
-
f"{commits:,}",
|
|
192
|
-
f"{points:,}",
|
|
193
|
-
f"{avg_points:.1f}",
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
widgets.append(
|
|
197
|
-
Static(Panel(contrib_table, title="Developer Activity", border_style="green"))
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
# Qualitative insights summary (if available)
|
|
201
|
-
if self._has_qualitative_data():
|
|
202
|
-
widgets.append(Rule())
|
|
203
|
-
widgets.append(Label("Qualitative Analysis Summary", classes="section-title"))
|
|
204
|
-
widgets.append(Static(self._create_qualitative_summary()))
|
|
205
|
-
|
|
206
|
-
# Create container with all widgets
|
|
207
|
-
return ScrollableContainer(*widgets)
|
|
208
|
-
|
|
209
|
-
def _create_developers_panel(self) -> Container:
|
|
210
|
-
"""Create interactive developers data panel."""
|
|
211
|
-
# Create enhanced data table
|
|
212
|
-
developers_table = EnhancedDataTable(data=self.developers, id="developers-table")
|
|
213
|
-
|
|
214
|
-
# Create container with all widgets
|
|
215
|
-
return Container(
|
|
216
|
-
Label("Developer Statistics", classes="section-title"),
|
|
217
|
-
Static(
|
|
218
|
-
f"Showing {len(self.developers)} unique developers. Click column headers to sort.",
|
|
219
|
-
classes="help-text",
|
|
220
|
-
),
|
|
221
|
-
developers_table,
|
|
222
|
-
Horizontal(
|
|
223
|
-
Button("Export Developers", id="export-developers"),
|
|
224
|
-
Button("Show Identity Details", id="show-identities"),
|
|
225
|
-
classes="action-bar",
|
|
226
|
-
),
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
def _create_commits_panel(self) -> Container:
|
|
230
|
-
"""Create interactive commits data panel."""
|
|
231
|
-
# Build widgets list
|
|
232
|
-
widgets = []
|
|
233
|
-
|
|
234
|
-
widgets.append(Label("Commit Analysis", classes="section-title"))
|
|
235
|
-
widgets.append(
|
|
236
|
-
Static(
|
|
237
|
-
f"Showing {len(self.commits)} commits. Click column headers to sort.",
|
|
238
|
-
classes="help-text",
|
|
239
|
-
)
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
# Create commits table with meaningful columns
|
|
243
|
-
commits_table = Table(show_header=True, header_style="bold magenta", expand=True)
|
|
244
|
-
commits_table.add_column("Date", width=12)
|
|
245
|
-
commits_table.add_column("Developer", width=20)
|
|
246
|
-
commits_table.add_column("Message", width=50)
|
|
247
|
-
commits_table.add_column("Points", width=8, justify="right")
|
|
248
|
-
commits_table.add_column("Tickets", width=15)
|
|
249
|
-
commits_table.add_column("Files", width=8, justify="right")
|
|
250
|
-
|
|
251
|
-
# Add rows (limit to recent 100 for performance)
|
|
252
|
-
for commit in sorted(self.commits, key=lambda c: c.get("timestamp", ""), reverse=True)[
|
|
253
|
-
:100
|
|
254
|
-
]:
|
|
255
|
-
timestamp = commit.get("timestamp", "")
|
|
256
|
-
date_str = timestamp.strftime("%Y-%m-%d") if timestamp else "Unknown"
|
|
257
|
-
|
|
258
|
-
developer = commit.get("author_name", "Unknown")[:18]
|
|
259
|
-
message = commit.get("message", "")[:48]
|
|
260
|
-
story_points = commit.get("story_points", 0) or 0
|
|
261
|
-
tickets = ", ".join(commit.get("ticket_references", []))[:13]
|
|
262
|
-
files = commit.get("files_changed", 0)
|
|
263
|
-
|
|
264
|
-
commits_table.add_row(
|
|
265
|
-
date_str,
|
|
266
|
-
developer,
|
|
267
|
-
message,
|
|
268
|
-
str(story_points) if story_points > 0 else "",
|
|
269
|
-
tickets,
|
|
270
|
-
str(files) if files > 0 else "",
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
# Create enhanced data table
|
|
274
|
-
commits_table_widget = EnhancedDataTable(data=self.commits[:100], id="commits-table")
|
|
275
|
-
|
|
276
|
-
widgets.append(commits_table_widget)
|
|
277
|
-
|
|
278
|
-
# Action buttons
|
|
279
|
-
widgets.append(
|
|
280
|
-
Horizontal(
|
|
281
|
-
Button("Export Commits", id="export-commits"),
|
|
282
|
-
Button("Show Untracked", id="show-untracked"),
|
|
283
|
-
classes="action-bar",
|
|
284
|
-
)
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
return Container(*widgets)
|
|
288
|
-
|
|
289
|
-
def _create_prs_panel(self) -> Container:
|
|
290
|
-
"""Create pull requests data panel."""
|
|
291
|
-
widgets = []
|
|
292
|
-
|
|
293
|
-
widgets.append(Label("Pull Request Analysis", classes="section-title"))
|
|
294
|
-
widgets.append(Static(f"Showing {len(self.prs)} pull requests.", classes="help-text"))
|
|
295
|
-
|
|
296
|
-
# Create PR table
|
|
297
|
-
pr_table = Table(show_header=True, header_style="bold magenta", expand=True)
|
|
298
|
-
pr_table.add_column("PR #", width=8)
|
|
299
|
-
pr_table.add_column("Title", width=40)
|
|
300
|
-
pr_table.add_column("Author", width=20)
|
|
301
|
-
pr_table.add_column("Status", width=10)
|
|
302
|
-
pr_table.add_column("Created", width=12)
|
|
303
|
-
pr_table.add_column("Commits", width=8, justify="right")
|
|
304
|
-
|
|
305
|
-
# Add rows
|
|
306
|
-
for pr in self.prs[:100]: # Limit for performance
|
|
307
|
-
pr_number = pr.get("number", "")
|
|
308
|
-
title = pr.get("title", "")[:38]
|
|
309
|
-
author = pr.get("author", "Unknown")[:18]
|
|
310
|
-
status = pr.get("state", "unknown")
|
|
311
|
-
created = pr.get("created_at", "")
|
|
312
|
-
if created:
|
|
313
|
-
created_str = (
|
|
314
|
-
created.strftime("%Y-%m-%d")
|
|
315
|
-
if hasattr(created, "strftime")
|
|
316
|
-
else str(created)[:10]
|
|
317
|
-
)
|
|
318
|
-
else:
|
|
319
|
-
created_str = "Unknown"
|
|
320
|
-
commit_count = pr.get("commit_count", 0)
|
|
321
|
-
|
|
322
|
-
pr_table.add_row(
|
|
323
|
-
str(pr_number),
|
|
324
|
-
title,
|
|
325
|
-
author,
|
|
326
|
-
status,
|
|
327
|
-
created_str,
|
|
328
|
-
str(commit_count) if commit_count > 0 else "",
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
# Create enhanced data table
|
|
332
|
-
prs_table = EnhancedDataTable(data=self.prs, id="prs-table")
|
|
333
|
-
|
|
334
|
-
widgets.append(prs_table)
|
|
335
|
-
|
|
336
|
-
# Action buttons
|
|
337
|
-
widgets.append(Horizontal(Button("Export PRs", id="export-prs"), classes="action-bar"))
|
|
338
|
-
|
|
339
|
-
return Container(*widgets)
|
|
340
|
-
|
|
341
|
-
def _create_qualitative_panel(self) -> ScrollableContainer:
|
|
342
|
-
"""Create qualitative analysis panel with enhanced insights."""
|
|
343
|
-
widgets = []
|
|
344
|
-
|
|
345
|
-
widgets.append(Label("Qualitative Analysis Results", classes="section-title"))
|
|
346
|
-
|
|
347
|
-
if self._has_qualitative_data():
|
|
348
|
-
widgets.append(Static("AI-powered insights from commit analysis", classes="help-text"))
|
|
349
|
-
widgets.append(
|
|
350
|
-
Static(
|
|
351
|
-
"Note: Qualitative analysis is based on commit messages and may not reflect "
|
|
352
|
-
"actual implementation details.",
|
|
353
|
-
classes="warning-text",
|
|
354
|
-
)
|
|
355
|
-
)
|
|
356
|
-
else:
|
|
357
|
-
widgets.append(
|
|
358
|
-
Static(
|
|
359
|
-
"No qualitative analysis data available. Enable OpenAI integration to generate insights.",
|
|
360
|
-
classes="warning-text",
|
|
361
|
-
)
|
|
362
|
-
)
|
|
363
|
-
return ScrollableContainer(*widgets)
|
|
364
|
-
|
|
365
|
-
# Get qualitative data
|
|
366
|
-
qual_data = self._get_qualitative_data()
|
|
367
|
-
summary_data = qual_data.get("summary", {})
|
|
368
|
-
|
|
369
|
-
# Change Type Distribution
|
|
370
|
-
widgets.append(Label("Change Type Distribution", classes="subsection-title"))
|
|
371
|
-
|
|
372
|
-
change_table = Table(show_header=True, header_style="bold cyan")
|
|
373
|
-
change_table.add_column("Type", width=20)
|
|
374
|
-
change_table.add_column("Count", width=10, justify="right")
|
|
375
|
-
change_table.add_column("Percentage", width=12, justify="right")
|
|
376
|
-
|
|
377
|
-
change_types = summary_data.get("change_types", {})
|
|
378
|
-
total_changes = sum(change_types.values())
|
|
379
|
-
|
|
380
|
-
for change_type, count in sorted(change_types.items(), key=lambda x: x[1], reverse=True):
|
|
381
|
-
percentage = (count / total_changes * 100) if total_changes > 0 else 0
|
|
382
|
-
change_table.add_row(change_type.title(), str(count), f"{percentage:.1f}%")
|
|
383
|
-
|
|
384
|
-
from rich.panel import Panel
|
|
385
|
-
|
|
386
|
-
widgets.append(Static(Panel(change_table, title="Change Types", border_style="cyan")))
|
|
387
|
-
|
|
388
|
-
# Risk Level Distribution
|
|
389
|
-
widgets.append(Rule())
|
|
390
|
-
widgets.append(Label("Risk Level Distribution", classes="subsection-title"))
|
|
391
|
-
|
|
392
|
-
risk_table = Table(show_header=True, header_style="bold red")
|
|
393
|
-
risk_table.add_column("Risk Level", width=20)
|
|
394
|
-
risk_table.add_column("Count", width=10, justify="right")
|
|
395
|
-
risk_table.add_column("Percentage", width=12, justify="right")
|
|
396
|
-
|
|
397
|
-
risk_levels = summary_data.get("risk_levels", {})
|
|
398
|
-
for level, count in risk_levels.items():
|
|
399
|
-
percentage = (count / total_changes * 100) if total_changes > 0 else 0
|
|
400
|
-
risk_table.add_row(level.title(), str(count), f"{percentage:.1f}%")
|
|
401
|
-
|
|
402
|
-
widgets.append(Static(Panel(risk_table, title="Risk Levels", border_style="red")))
|
|
403
|
-
|
|
404
|
-
# Business Domain Activity
|
|
405
|
-
widgets.append(Rule())
|
|
406
|
-
widgets.append(Label("Business Domain Activity", classes="subsection-title"))
|
|
407
|
-
|
|
408
|
-
domain_table = Table(show_header=True, header_style="bold green")
|
|
409
|
-
domain_table.add_column("Domain", width=20)
|
|
410
|
-
domain_table.add_column("Commits", width=10, justify="right")
|
|
411
|
-
domain_table.add_column("Focus", width=12, justify="right")
|
|
412
|
-
|
|
413
|
-
domains = summary_data.get("business_domains", {})
|
|
414
|
-
for domain, count in sorted(domains.items(), key=lambda x: x[1], reverse=True)[:10]:
|
|
415
|
-
percentage = (count / total_changes * 100) if total_changes > 0 else 0
|
|
416
|
-
domain_table.add_row(domain.title(), str(count), f"{percentage:.1f}%")
|
|
417
|
-
|
|
418
|
-
widgets.append(Static(Panel(domain_table, title="Business Domains", border_style="green")))
|
|
419
|
-
|
|
420
|
-
# Analysis Confidence
|
|
421
|
-
if "confidence_scores" in summary_data:
|
|
422
|
-
widgets.append(Rule())
|
|
423
|
-
widgets.append(Label("Analysis Confidence", classes="subsection-title"))
|
|
424
|
-
|
|
425
|
-
conf_scores = summary_data["confidence_scores"]
|
|
426
|
-
conf_table = Table(show_header=False, show_edge=False)
|
|
427
|
-
conf_table.add_column("Metric", width=25)
|
|
428
|
-
conf_table.add_column("Score", width=15)
|
|
429
|
-
|
|
430
|
-
conf_table.add_row("Average Confidence", f"{conf_scores.get('average', 0):.1f}%")
|
|
431
|
-
conf_table.add_row(
|
|
432
|
-
"High Confidence", f"{conf_scores.get('high_confidence_pct', 0):.1f}%"
|
|
433
|
-
)
|
|
434
|
-
conf_table.add_row("Low Confidence", f"{conf_scores.get('low_confidence_pct', 0):.1f}%")
|
|
435
|
-
|
|
436
|
-
widgets.append(
|
|
437
|
-
Static(Panel(conf_table, title="ML Model Confidence", border_style="yellow"))
|
|
438
|
-
)
|
|
439
|
-
|
|
440
|
-
return ScrollableContainer(*widgets)
|
|
441
|
-
|
|
442
|
-
def _create_export_panel(self) -> Container:
|
|
443
|
-
"""Create export options panel."""
|
|
444
|
-
widgets = []
|
|
445
|
-
|
|
446
|
-
widgets.append(Label("Export Analysis Results", classes="section-title"))
|
|
447
|
-
widgets.append(
|
|
448
|
-
Static(
|
|
449
|
-
"Select export format and click export to save results.",
|
|
450
|
-
classes="help-text",
|
|
451
|
-
)
|
|
452
|
-
)
|
|
453
|
-
|
|
454
|
-
# Export options
|
|
455
|
-
export_options = Vertical(
|
|
456
|
-
RadioButton("CSV Reports (Multiple Files)", id="export-csv", value=True),
|
|
457
|
-
RadioButton("JSON (Complete Data)", id="export-json"),
|
|
458
|
-
RadioButton("Markdown Report", id="export-markdown"),
|
|
459
|
-
Rule(),
|
|
460
|
-
Button("Export Now", variant="primary", id="export-now"),
|
|
461
|
-
Label("", id="export-path-label"),
|
|
462
|
-
id="export-options",
|
|
463
|
-
)
|
|
464
|
-
|
|
465
|
-
widgets.append(export_options)
|
|
466
|
-
widgets.append(Rule())
|
|
467
|
-
widgets.append(Static("", id="export-status"))
|
|
468
|
-
|
|
469
|
-
return Container(*widgets)
|
|
470
|
-
|
|
471
|
-
def _has_qualitative_data(self) -> bool:
|
|
472
|
-
"""Check if qualitative analysis data is available."""
|
|
473
|
-
return any("change_type" in commit for commit in self.commits)
|
|
474
|
-
|
|
475
|
-
def _create_qualitative_summary(self) -> str:
|
|
476
|
-
"""Create a text summary of qualitative insights."""
|
|
477
|
-
if not self._has_qualitative_data():
|
|
478
|
-
return "No qualitative data available"
|
|
479
|
-
|
|
480
|
-
# Count change types and risk levels
|
|
481
|
-
change_types = {}
|
|
482
|
-
risk_levels = {}
|
|
483
|
-
|
|
484
|
-
for commit in self.commits:
|
|
485
|
-
if "change_type" in commit:
|
|
486
|
-
change_type = commit.get("change_type", "unknown")
|
|
487
|
-
change_types[change_type] = change_types.get(change_type, 0) + 1
|
|
488
|
-
|
|
489
|
-
risk_level = commit.get("risk_level", "unknown")
|
|
490
|
-
risk_levels[risk_level] = risk_levels.get(risk_level, 0) + 1
|
|
491
|
-
|
|
492
|
-
# Find most common values
|
|
493
|
-
top_change_type = (
|
|
494
|
-
max(change_types.items(), key=lambda x: x[1]) if change_types else ("unknown", 0)
|
|
495
|
-
)
|
|
496
|
-
top_risk_level = (
|
|
497
|
-
max(risk_levels.items(), key=lambda x: x[1]) if risk_levels else ("unknown", 0)
|
|
498
|
-
)
|
|
499
|
-
|
|
500
|
-
total_analyzed = sum(change_types.values())
|
|
501
|
-
|
|
502
|
-
return f"""Qualitative Analysis Summary:
|
|
503
|
-
• Total commits analyzed: {total_analyzed:,}
|
|
504
|
-
• Most common change type: {top_change_type[0]} ({top_change_type[1]} commits)
|
|
505
|
-
• Most common risk level: {top_risk_level[0]} ({top_risk_level[1]} commits)
|
|
506
|
-
• Coverage: {(total_analyzed/len(self.commits)*100):.1f}% of all commits"""
|
|
507
|
-
|
|
508
|
-
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
509
|
-
"""Handle button press events."""
|
|
510
|
-
export_actions = {
|
|
511
|
-
"export-summary-csv": lambda: self._export_data("summary", "csv"),
|
|
512
|
-
"export-developers-csv": lambda: self._export_data("developers", "csv"),
|
|
513
|
-
"export-commits-csv": lambda: self._export_data("commits", "csv"),
|
|
514
|
-
"export-prs-csv": lambda: self._export_data("prs", "csv"),
|
|
515
|
-
"export-qualitative-csv": lambda: self._export_data("qualitative", "csv"),
|
|
516
|
-
"export-json": lambda: self._export_data("complete", "json"),
|
|
517
|
-
"export-markdown": lambda: self._export_data("report", "markdown"),
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
action = export_actions.get(event.button.id)
|
|
521
|
-
if action:
|
|
522
|
-
action()
|
|
523
|
-
else:
|
|
524
|
-
# Handle other button actions
|
|
525
|
-
if event.button.id == "show-identities":
|
|
526
|
-
self._show_identity_details()
|
|
527
|
-
elif event.button.id == "show-pr-metrics":
|
|
528
|
-
self._show_pr_metrics()
|
|
529
|
-
|
|
530
|
-
def _export_data(self, data_type: str, format_type: str) -> None:
|
|
531
|
-
"""
|
|
532
|
-
Export specific data type in specified format.
|
|
533
|
-
|
|
534
|
-
WHY: Provides flexible export functionality that allows users to
|
|
535
|
-
export exactly the data they need in their preferred format.
|
|
536
|
-
"""
|
|
537
|
-
try:
|
|
538
|
-
# Determine data and filename based on type
|
|
539
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
540
|
-
|
|
541
|
-
if data_type == "summary":
|
|
542
|
-
data = self._prepare_summary_data()
|
|
543
|
-
elif data_type == "developers":
|
|
544
|
-
data = self.developers
|
|
545
|
-
elif data_type == "commits":
|
|
546
|
-
data = self.commits
|
|
547
|
-
elif data_type == "prs":
|
|
548
|
-
data = self.prs
|
|
549
|
-
elif data_type == "qualitative":
|
|
550
|
-
data = self._prepare_qualitative_data()
|
|
551
|
-
elif data_type == "complete":
|
|
552
|
-
data = {
|
|
553
|
-
"commits": self.commits,
|
|
554
|
-
"prs": self.prs,
|
|
555
|
-
"developers": self.developers,
|
|
556
|
-
"config": self.config.__dict__ if hasattr(self.config, "__dict__") else {},
|
|
557
|
-
}
|
|
558
|
-
else:
|
|
559
|
-
self.notify("Unknown export type", severity="error")
|
|
560
|
-
return
|
|
561
|
-
|
|
562
|
-
# Show export modal
|
|
563
|
-
export_modal = ExportModal(
|
|
564
|
-
available_formats=[format_type.upper()],
|
|
565
|
-
default_path=Path("./reports"),
|
|
566
|
-
data_info={
|
|
567
|
-
"type": data_type,
|
|
568
|
-
"row_count": len(data) if isinstance(data, list) else "N/A",
|
|
569
|
-
"timestamp": timestamp,
|
|
570
|
-
},
|
|
571
|
-
)
|
|
572
|
-
|
|
573
|
-
def handle_export(config):
|
|
574
|
-
if config:
|
|
575
|
-
self._perform_export(data, config, format_type)
|
|
576
|
-
|
|
577
|
-
self.app.push_screen(export_modal, handle_export)
|
|
578
|
-
|
|
579
|
-
except Exception as e:
|
|
580
|
-
self.notify(f"Export preparation failed: {e}", severity="error")
|
|
581
|
-
|
|
582
|
-
def _perform_export(self, data: Any, export_config: dict[str, Any], format_type: str) -> None:
|
|
583
|
-
"""Perform the actual export operation."""
|
|
584
|
-
try:
|
|
585
|
-
export_path = export_config["path"]
|
|
586
|
-
|
|
587
|
-
if format_type == "csv":
|
|
588
|
-
self._export_to_csv(data, export_path, export_config)
|
|
589
|
-
elif format_type == "json":
|
|
590
|
-
self._export_to_json(data, export_path, export_config)
|
|
591
|
-
elif format_type == "markdown":
|
|
592
|
-
self._export_to_markdown(data, export_path, export_config)
|
|
593
|
-
|
|
594
|
-
self.notify(f"Successfully exported to {export_path}", severity="success")
|
|
595
|
-
|
|
596
|
-
except Exception as e:
|
|
597
|
-
self.notify(f"Export failed: {e}", severity="error")
|
|
598
|
-
|
|
599
|
-
def _export_to_csv(self, data: list[dict], path: Path, config: dict[str, Any]) -> None:
|
|
600
|
-
"""Export data to CSV format."""
|
|
601
|
-
import csv
|
|
602
|
-
|
|
603
|
-
if not data:
|
|
604
|
-
return
|
|
605
|
-
|
|
606
|
-
# Ensure parent directory exists
|
|
607
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
608
|
-
|
|
609
|
-
with open(path, "w", newline="", encoding="utf-8") as csvfile:
|
|
610
|
-
fieldnames = list(data[0].keys())
|
|
611
|
-
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
612
|
-
|
|
613
|
-
if config.get("include_headers", True):
|
|
614
|
-
writer.writeheader()
|
|
615
|
-
|
|
616
|
-
for row in data:
|
|
617
|
-
# Anonymize if requested
|
|
618
|
-
if config.get("anonymize", False):
|
|
619
|
-
row = self._anonymize_row(row)
|
|
620
|
-
writer.writerow(row)
|
|
621
|
-
|
|
622
|
-
def _export_to_json(self, data: Any, path: Path, config: dict[str, Any]) -> None:
|
|
623
|
-
"""Export data to JSON format."""
|
|
624
|
-
# Ensure parent directory exists
|
|
625
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
626
|
-
|
|
627
|
-
# Anonymize if requested
|
|
628
|
-
if config.get("anonymize", False):
|
|
629
|
-
data = self._anonymize_data(data)
|
|
630
|
-
|
|
631
|
-
with open(path, "w", encoding="utf-8") as jsonfile:
|
|
632
|
-
json.dump(data, jsonfile, indent=2, default=str)
|
|
633
|
-
|
|
634
|
-
def _export_to_markdown(self, data: Any, path: Path, config: dict[str, Any]) -> None:
|
|
635
|
-
"""Export data as markdown report."""
|
|
636
|
-
self.notify("Markdown export not yet implemented", severity="info")
|
|
637
|
-
# TODO: Implement markdown report generation
|
|
638
|
-
|
|
639
|
-
def _prepare_summary_data(self) -> list[dict]:
|
|
640
|
-
"""Prepare summary statistics for export."""
|
|
641
|
-
return [
|
|
642
|
-
{"metric": "Total Commits", "value": len(self.commits)},
|
|
643
|
-
{"metric": "Total PRs", "value": len(self.prs)},
|
|
644
|
-
{"metric": "Active Developers", "value": len(self.developers)},
|
|
645
|
-
{
|
|
646
|
-
"metric": "Total Story Points",
|
|
647
|
-
"value": sum(c.get("story_points", 0) or 0 for c in self.commits),
|
|
648
|
-
},
|
|
649
|
-
]
|
|
650
|
-
|
|
651
|
-
def _prepare_qualitative_data(self) -> list[dict]:
|
|
652
|
-
"""Prepare qualitative analysis data for export."""
|
|
653
|
-
qualitative_commits = []
|
|
654
|
-
for commit in self.commits:
|
|
655
|
-
if "change_type" in commit:
|
|
656
|
-
qual_commit = {
|
|
657
|
-
"commit_hash": commit.get("hash"),
|
|
658
|
-
"author": commit.get("author_name"),
|
|
659
|
-
"message": commit.get("message"),
|
|
660
|
-
"change_type": commit.get("change_type"),
|
|
661
|
-
"business_domain": commit.get("business_domain"),
|
|
662
|
-
"risk_level": commit.get("risk_level"),
|
|
663
|
-
"confidence_score": commit.get("confidence_score"),
|
|
664
|
-
}
|
|
665
|
-
qualitative_commits.append(qual_commit)
|
|
666
|
-
return qualitative_commits
|
|
667
|
-
|
|
668
|
-
def _anonymize_row(self, row: dict) -> dict:
|
|
669
|
-
"""Anonymize sensitive data in a row."""
|
|
670
|
-
# Simple anonymization - replace names with hashed versions
|
|
671
|
-
anonymized = row.copy()
|
|
672
|
-
|
|
673
|
-
# Fields to anonymize
|
|
674
|
-
sensitive_fields = ["author_name", "author_email", "primary_name", "primary_email"]
|
|
675
|
-
|
|
676
|
-
for field in sensitive_fields:
|
|
677
|
-
if field in anonymized and anonymized[field]:
|
|
678
|
-
# Simple hash-based anonymization
|
|
679
|
-
import hashlib
|
|
680
|
-
|
|
681
|
-
hash_value = hashlib.md5(str(anonymized[field]).encode()).hexdigest()[:8]
|
|
682
|
-
anonymized[field] = f"User_{hash_value}"
|
|
683
|
-
|
|
684
|
-
return anonymized
|
|
685
|
-
|
|
686
|
-
def _anonymize_data(self, data: Any) -> Any:
|
|
687
|
-
"""Anonymize data structure recursively."""
|
|
688
|
-
if isinstance(data, list):
|
|
689
|
-
return [self._anonymize_data(item) for item in data]
|
|
690
|
-
elif isinstance(data, dict):
|
|
691
|
-
return {key: self._anonymize_data(value) for key, value in data.items()}
|
|
692
|
-
else:
|
|
693
|
-
return data
|
|
694
|
-
|
|
695
|
-
def _show_identity_details(self) -> None:
|
|
696
|
-
"""Show detailed developer identity information."""
|
|
697
|
-
self.notify("Identity details view not yet implemented", severity="info")
|
|
698
|
-
|
|
699
|
-
def _show_pr_metrics(self) -> None:
|
|
700
|
-
"""Show detailed pull request metrics."""
|
|
701
|
-
self.notify("PR metrics view not yet implemented", severity="info")
|
|
702
|
-
|
|
703
|
-
def action_back(self) -> None:
|
|
704
|
-
"""Go back to main screen."""
|
|
705
|
-
self.app.pop_screen()
|
|
706
|
-
|
|
707
|
-
def action_export(self) -> None:
|
|
708
|
-
"""Show export options."""
|
|
709
|
-
# Switch to export tab
|
|
710
|
-
tabbed_content = self.query_one(TabbedContent)
|
|
711
|
-
tabbed_content.active = "export"
|
|
712
|
-
|
|
713
|
-
def action_filter(self) -> None:
|
|
714
|
-
"""Show filter options for current tab."""
|
|
715
|
-
self.notify("Filtering functionality not yet implemented", severity="info")
|
|
716
|
-
|
|
717
|
-
def action_refresh(self) -> None:
|
|
718
|
-
"""Refresh current view."""
|
|
719
|
-
self.refresh()
|
|
720
|
-
|
|
721
|
-
def action_export_current(self) -> None:
|
|
722
|
-
"""Export data from currently active tab."""
|
|
723
|
-
tabbed_content = self.query_one(TabbedContent)
|
|
724
|
-
current_tab = tabbed_content.active
|
|
725
|
-
|
|
726
|
-
if current_tab == "developers":
|
|
727
|
-
self._export_data("developers", "csv")
|
|
728
|
-
elif current_tab == "commits":
|
|
729
|
-
self._export_data("commits", "csv")
|
|
730
|
-
elif current_tab == "pull-requests":
|
|
731
|
-
self._export_data("prs", "csv")
|
|
732
|
-
elif current_tab == "qualitative":
|
|
733
|
-
self._export_data("qualitative", "csv")
|
|
734
|
-
else:
|
|
735
|
-
self._export_data("summary", "csv")
|