gitflow-analytics 1.0.1__py3-none-any.whl → 1.0.3__py3-none-any.whl

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