gitflow-analytics 1.0.1__py3-none-any.whl → 1.3.6__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 (119) hide show
  1. gitflow_analytics/__init__.py +11 -11
  2. gitflow_analytics/_version.py +2 -2
  3. gitflow_analytics/classification/__init__.py +31 -0
  4. gitflow_analytics/classification/batch_classifier.py +752 -0
  5. gitflow_analytics/classification/classifier.py +464 -0
  6. gitflow_analytics/classification/feature_extractor.py +725 -0
  7. gitflow_analytics/classification/linguist_analyzer.py +574 -0
  8. gitflow_analytics/classification/model.py +455 -0
  9. gitflow_analytics/cli.py +4490 -378
  10. gitflow_analytics/cli_rich.py +503 -0
  11. gitflow_analytics/config/__init__.py +43 -0
  12. gitflow_analytics/config/errors.py +261 -0
  13. gitflow_analytics/config/loader.py +904 -0
  14. gitflow_analytics/config/profiles.py +264 -0
  15. gitflow_analytics/config/repository.py +124 -0
  16. gitflow_analytics/config/schema.py +441 -0
  17. gitflow_analytics/config/validator.py +154 -0
  18. gitflow_analytics/config.py +44 -398
  19. gitflow_analytics/core/analyzer.py +1320 -172
  20. gitflow_analytics/core/branch_mapper.py +132 -132
  21. gitflow_analytics/core/cache.py +1554 -175
  22. gitflow_analytics/core/data_fetcher.py +1193 -0
  23. gitflow_analytics/core/identity.py +571 -185
  24. gitflow_analytics/core/metrics_storage.py +526 -0
  25. gitflow_analytics/core/progress.py +372 -0
  26. gitflow_analytics/core/schema_version.py +269 -0
  27. gitflow_analytics/extractors/base.py +13 -11
  28. gitflow_analytics/extractors/ml_tickets.py +1100 -0
  29. gitflow_analytics/extractors/story_points.py +77 -59
  30. gitflow_analytics/extractors/tickets.py +841 -89
  31. gitflow_analytics/identity_llm/__init__.py +6 -0
  32. gitflow_analytics/identity_llm/analysis_pass.py +231 -0
  33. gitflow_analytics/identity_llm/analyzer.py +464 -0
  34. gitflow_analytics/identity_llm/models.py +76 -0
  35. gitflow_analytics/integrations/github_integration.py +258 -87
  36. gitflow_analytics/integrations/jira_integration.py +572 -123
  37. gitflow_analytics/integrations/orchestrator.py +206 -82
  38. gitflow_analytics/metrics/activity_scoring.py +322 -0
  39. gitflow_analytics/metrics/branch_health.py +470 -0
  40. gitflow_analytics/metrics/dora.py +542 -179
  41. gitflow_analytics/models/database.py +986 -59
  42. gitflow_analytics/pm_framework/__init__.py +115 -0
  43. gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
  44. gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
  45. gitflow_analytics/pm_framework/base.py +406 -0
  46. gitflow_analytics/pm_framework/models.py +211 -0
  47. gitflow_analytics/pm_framework/orchestrator.py +652 -0
  48. gitflow_analytics/pm_framework/registry.py +333 -0
  49. gitflow_analytics/qualitative/__init__.py +29 -0
  50. gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
  51. gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
  52. gitflow_analytics/qualitative/classifiers/change_type.py +742 -0
  53. gitflow_analytics/qualitative/classifiers/domain_classifier.py +506 -0
  54. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +535 -0
  55. gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
  56. gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
  57. gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
  58. gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
  59. gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
  60. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
  61. gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
  62. gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
  63. gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
  64. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +438 -0
  65. gitflow_analytics/qualitative/core/__init__.py +13 -0
  66. gitflow_analytics/qualitative/core/llm_fallback.py +657 -0
  67. gitflow_analytics/qualitative/core/nlp_engine.py +382 -0
  68. gitflow_analytics/qualitative/core/pattern_cache.py +479 -0
  69. gitflow_analytics/qualitative/core/processor.py +673 -0
  70. gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
  71. gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
  72. gitflow_analytics/qualitative/models/__init__.py +25 -0
  73. gitflow_analytics/qualitative/models/schemas.py +306 -0
  74. gitflow_analytics/qualitative/utils/__init__.py +13 -0
  75. gitflow_analytics/qualitative/utils/batch_processor.py +339 -0
  76. gitflow_analytics/qualitative/utils/cost_tracker.py +345 -0
  77. gitflow_analytics/qualitative/utils/metrics.py +361 -0
  78. gitflow_analytics/qualitative/utils/text_processing.py +285 -0
  79. gitflow_analytics/reports/__init__.py +100 -0
  80. gitflow_analytics/reports/analytics_writer.py +550 -18
  81. gitflow_analytics/reports/base.py +648 -0
  82. gitflow_analytics/reports/branch_health_writer.py +322 -0
  83. gitflow_analytics/reports/classification_writer.py +924 -0
  84. gitflow_analytics/reports/cli_integration.py +427 -0
  85. gitflow_analytics/reports/csv_writer.py +1700 -216
  86. gitflow_analytics/reports/data_models.py +504 -0
  87. gitflow_analytics/reports/database_report_generator.py +427 -0
  88. gitflow_analytics/reports/example_usage.py +344 -0
  89. gitflow_analytics/reports/factory.py +499 -0
  90. gitflow_analytics/reports/formatters.py +698 -0
  91. gitflow_analytics/reports/html_generator.py +1116 -0
  92. gitflow_analytics/reports/interfaces.py +489 -0
  93. gitflow_analytics/reports/json_exporter.py +2770 -0
  94. gitflow_analytics/reports/narrative_writer.py +2289 -158
  95. gitflow_analytics/reports/story_point_correlation.py +1144 -0
  96. gitflow_analytics/reports/weekly_trends_writer.py +389 -0
  97. gitflow_analytics/training/__init__.py +5 -0
  98. gitflow_analytics/training/model_loader.py +377 -0
  99. gitflow_analytics/training/pipeline.py +550 -0
  100. gitflow_analytics/tui/__init__.py +5 -0
  101. gitflow_analytics/tui/app.py +724 -0
  102. gitflow_analytics/tui/screens/__init__.py +8 -0
  103. gitflow_analytics/tui/screens/analysis_progress_screen.py +496 -0
  104. gitflow_analytics/tui/screens/configuration_screen.py +523 -0
  105. gitflow_analytics/tui/screens/loading_screen.py +348 -0
  106. gitflow_analytics/tui/screens/main_screen.py +321 -0
  107. gitflow_analytics/tui/screens/results_screen.py +722 -0
  108. gitflow_analytics/tui/widgets/__init__.py +7 -0
  109. gitflow_analytics/tui/widgets/data_table.py +255 -0
  110. gitflow_analytics/tui/widgets/export_modal.py +301 -0
  111. gitflow_analytics/tui/widgets/progress_widget.py +187 -0
  112. gitflow_analytics-1.3.6.dist-info/METADATA +1015 -0
  113. gitflow_analytics-1.3.6.dist-info/RECORD +122 -0
  114. gitflow_analytics-1.0.1.dist-info/METADATA +0 -463
  115. gitflow_analytics-1.0.1.dist-info/RECORD +0 -31
  116. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/WHEEL +0 -0
  117. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
  118. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
  119. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,8 @@
1
+ """TUI screens for GitFlow Analytics."""
2
+
3
+ from .analysis_progress_screen import AnalysisProgressScreen
4
+ from .configuration_screen import ConfigurationScreen
5
+ from .main_screen import MainScreen
6
+ from .results_screen import ResultsScreen
7
+
8
+ __all__ = ["MainScreen", "ConfigurationScreen", "AnalysisProgressScreen", "ResultsScreen"]
@@ -0,0 +1,496 @@
1
+ """Analysis progress screen for GitFlow Analytics TUI."""
2
+
3
+ import asyncio
4
+ import time
5
+ from datetime import datetime, timedelta, timezone
6
+ from pathlib import Path
7
+ from typing import Any, Optional
8
+
9
+ from rich.pretty import Pretty
10
+ from textual.binding import Binding
11
+ from textual.containers import Container, Vertical
12
+ from textual.screen import Screen
13
+ from textual.widgets import Footer, Header, Label, Log
14
+
15
+ from gitflow_analytics.config import Config
16
+ from gitflow_analytics.core.analyzer import GitAnalyzer
17
+ from gitflow_analytics.core.cache import GitAnalysisCache
18
+ from gitflow_analytics.core.identity import DeveloperIdentityResolver
19
+ from gitflow_analytics.integrations.orchestrator import IntegrationOrchestrator
20
+
21
+ from ..widgets.progress_widget import AnalysisProgressWidget
22
+
23
+
24
+ class AnalysisProgressScreen(Screen):
25
+ """
26
+ Screen showing real-time analysis progress with detailed status updates.
27
+
28
+ WHY: Long-running analysis operations require comprehensive progress feedback
29
+ to keep users informed and allow them to monitor the process. This screen
30
+ provides real-time updates on all phases of analysis.
31
+
32
+ DESIGN DECISION: Uses multiple progress widgets to show different phases
33
+ independently, allowing users to understand which part of the analysis is
34
+ currently running and estimated completion times for each phase.
35
+ """
36
+
37
+ BINDINGS = [
38
+ Binding("ctrl+c", "cancel", "Cancel Analysis"),
39
+ Binding("escape", "back", "Back to Main"),
40
+ Binding("ctrl+l", "toggle_log", "Toggle Log"),
41
+ ]
42
+
43
+ def __init__(
44
+ self,
45
+ config: Config,
46
+ weeks: int = 12,
47
+ enable_qualitative: bool = True,
48
+ *,
49
+ name: Optional[str] = None,
50
+ id: Optional[str] = None,
51
+ ) -> None:
52
+ super().__init__(name=name, id=id)
53
+ self.config = config
54
+ self.weeks = weeks
55
+ self.enable_qualitative = enable_qualitative
56
+ self.analysis_task: Optional[asyncio.Task] = None
57
+ self.analysis_results = {}
58
+ self.start_time = time.time()
59
+
60
+ def compose(self):
61
+ """Compose the analysis progress screen."""
62
+ yield Header()
63
+
64
+ with Container(id="progress-container"):
65
+ yield Label("GitFlow Analytics - Analysis in Progress", classes="screen-title")
66
+
67
+ # Progress panels for different phases
68
+ with Vertical(id="progress-panels"):
69
+ yield AnalysisProgressWidget("Overall Progress", total=100.0, id="overall-progress")
70
+
71
+ yield AnalysisProgressWidget("Repository Analysis", total=100.0, id="repo-progress")
72
+
73
+ yield AnalysisProgressWidget(
74
+ "Integration Data", total=100.0, id="integration-progress"
75
+ )
76
+
77
+ if self.enable_qualitative:
78
+ yield AnalysisProgressWidget(
79
+ "Qualitative Analysis", total=100.0, id="qual-progress"
80
+ )
81
+
82
+ # Live statistics panel
83
+ with Container(classes="stats-panel"):
84
+ yield Label("Live Statistics", classes="panel-title")
85
+ yield Pretty({}, id="live-stats")
86
+
87
+ # Analysis log
88
+ with Container(classes="log-panel"):
89
+ yield Label("Analysis Log", classes="panel-title")
90
+ yield Log(auto_scroll=True, id="analysis-log")
91
+
92
+ yield Footer()
93
+
94
+ def on_mount(self) -> None:
95
+ """Start analysis when screen mounts."""
96
+ self.analysis_task = asyncio.create_task(self._run_analysis())
97
+
98
+ async def _run_analysis(self) -> None:
99
+ """
100
+ Run the complete analysis pipeline with progress updates.
101
+
102
+ WHY: Implements the full analysis workflow with detailed progress tracking
103
+ and error handling, ensuring users receive comprehensive feedback about
104
+ the analysis process.
105
+ """
106
+ log = self.query_one("#analysis-log", Log)
107
+ overall_progress = self.query_one("#overall-progress", AnalysisProgressWidget)
108
+
109
+ try:
110
+ log.write_line("🚀 Starting GitFlow Analytics...")
111
+
112
+ # Phase 1: Initialize components (10%)
113
+ overall_progress.update_progress(5, "Initializing components...")
114
+ await self._initialize_components(log)
115
+ overall_progress.update_progress(10, "Components initialized")
116
+
117
+ # Phase 2: Repository discovery (20%)
118
+ overall_progress.update_progress(10, "Discovering repositories...")
119
+ repositories = await self._discover_repositories(log)
120
+ overall_progress.update_progress(20, f"Found {len(repositories)} repositories")
121
+
122
+ # Phase 3: Repository analysis (50%)
123
+ overall_progress.update_progress(20, "Analyzing repositories...")
124
+ commits, prs = await self._analyze_repositories(repositories, log)
125
+ overall_progress.update_progress(50, f"Analyzed {len(commits)} commits")
126
+
127
+ # Phase 4: Integration enrichment (70%)
128
+ overall_progress.update_progress(50, "Enriching with integration data...")
129
+ await self._enrich_with_integrations(repositories, commits, log)
130
+ overall_progress.update_progress(70, "Integration data complete")
131
+
132
+ # Phase 5: Identity resolution (80%)
133
+ overall_progress.update_progress(70, "Resolving developer identities...")
134
+ developer_stats = await self._resolve_identities(commits, log)
135
+ overall_progress.update_progress(80, f"Identified {len(developer_stats)} developers")
136
+
137
+ # Phase 6: Qualitative analysis (95%)
138
+ if self.enable_qualitative:
139
+ overall_progress.update_progress(80, "Running qualitative analysis...")
140
+ await self._run_qualitative_analysis(commits, log)
141
+ overall_progress.update_progress(95, "Qualitative analysis complete")
142
+
143
+ # Phase 7: Finalization (100%)
144
+ overall_progress.update_progress(95, "Finalizing results...")
145
+ self.analysis_results = {
146
+ "commits": commits,
147
+ "prs": prs,
148
+ "developers": developer_stats,
149
+ "repositories": repositories,
150
+ }
151
+
152
+ overall_progress.complete("Analysis complete!")
153
+
154
+ total_time = time.time() - self.start_time
155
+ log.write_line(f"🎉 Analysis completed in {total_time:.1f} seconds!")
156
+ log.write_line(f" - Total commits: {len(commits):,}")
157
+ log.write_line(f" - Total PRs: {len(prs):,}")
158
+ log.write_line(f" - Active developers: {len(developer_stats):,}")
159
+
160
+ # Switch to results screen after brief pause
161
+ await asyncio.sleep(2)
162
+ from .results_screen import ResultsScreen
163
+
164
+ self.app.push_screen(
165
+ ResultsScreen(
166
+ commits=commits, prs=prs, developers=developer_stats, config=self.config
167
+ )
168
+ )
169
+
170
+ except asyncio.CancelledError:
171
+ log.write_line("❌ Analysis cancelled by user")
172
+ overall_progress.update_progress(0, "Cancelled")
173
+ except Exception as e:
174
+ log.write_line(f"❌ Analysis failed: {e}")
175
+ overall_progress.update_progress(0, f"Error: {str(e)[:50]}...")
176
+ self.notify(f"Analysis failed: {e}", severity="error")
177
+
178
+ async def _initialize_components(self, log: Log) -> None:
179
+ """Initialize analysis components."""
180
+ log.write_line("📋 Initializing cache...")
181
+
182
+ self.cache = GitAnalysisCache(
183
+ self.config.cache.directory, ttl_hours=self.config.cache.ttl_hours
184
+ )
185
+
186
+ log.write_line("👥 Initializing identity resolver...")
187
+ self.identity_resolver = DeveloperIdentityResolver(
188
+ self.config.cache.directory / "identities.db",
189
+ similarity_threshold=self.config.analysis.similarity_threshold,
190
+ manual_mappings=self.config.analysis.manual_identity_mappings,
191
+ )
192
+
193
+ log.write_line("🔍 Initializing analyzer...")
194
+ self.analyzer = GitAnalyzer(
195
+ self.cache,
196
+ branch_mapping_rules=self.config.analysis.branch_mapping_rules,
197
+ allowed_ticket_platforms=getattr(self.config.analysis, "ticket_platforms", None),
198
+ exclude_paths=self.config.analysis.exclude_paths,
199
+ story_point_patterns=self.config.analysis.story_point_patterns,
200
+ )
201
+
202
+ log.write_line("🔗 Initializing integrations...")
203
+ self.orchestrator = IntegrationOrchestrator(self.config, self.cache)
204
+
205
+ # Check if we have pre-loaded NLP engine from startup
206
+ if hasattr(self.app, "get_nlp_engine") and self.app.get_nlp_engine():
207
+ log.write_line("✅ NLP engine already loaded from startup")
208
+ elif self.enable_qualitative:
209
+ log.write_line("⚠️ NLP engine will be loaded during qualitative analysis phase")
210
+
211
+ # Small delay to show progress
212
+ await asyncio.sleep(0.5)
213
+
214
+ async def _discover_repositories(self, log: Log) -> list:
215
+ """Discover repositories to analyze."""
216
+ repositories = self.config.repositories
217
+
218
+ if self.config.github.organization and not repositories:
219
+ log.write_line(
220
+ f"🔍 Discovering repositories from organization: {self.config.github.organization}"
221
+ )
222
+
223
+ try:
224
+ # Use config directory for cloned repos
225
+ config_dir = Path.cwd() # TODO: Get actual config directory
226
+ repos_dir = config_dir / "repos"
227
+
228
+ discovered_repos = self.config.discover_organization_repositories(
229
+ clone_base_path=repos_dir
230
+ )
231
+ repositories = discovered_repos
232
+
233
+ for repo in repositories:
234
+ log.write_line(f" 📁 {repo.name} ({repo.github_repo})")
235
+
236
+ except Exception as e:
237
+ log.write_line(f" ❌ Repository discovery failed: {e}")
238
+ raise
239
+
240
+ await asyncio.sleep(0.5) # Brief pause for UI updates
241
+ return repositories
242
+
243
+ async def _analyze_repositories(self, repositories: list, log: Log) -> tuple:
244
+ """Analyze all repositories and return commits and PRs."""
245
+ repo_progress = self.query_one("#repo-progress", AnalysisProgressWidget)
246
+
247
+ all_commits = []
248
+ all_prs = []
249
+
250
+ # Analysis period (timezone-aware to match commit timestamps)
251
+ end_date = datetime.now(timezone.utc)
252
+ start_date = end_date - timedelta(weeks=self.weeks)
253
+
254
+ for i, repo_config in enumerate(repositories):
255
+ progress = (i / len(repositories)) * 100
256
+ repo_progress.update_progress(progress, f"Analyzing {repo_config.name}...")
257
+
258
+ log.write_line(f"📁 Analyzing {repo_config.name}...")
259
+
260
+ try:
261
+ # Clone repository if needed
262
+ if not repo_config.path.exists() and repo_config.github_repo:
263
+ log.write_line(f" 📥 Cloning {repo_config.github_repo}...")
264
+ await self._clone_repository(repo_config, log)
265
+
266
+ # Analyze commits
267
+ commits = self.analyzer.analyze_repository(
268
+ repo_config.path, start_date, repo_config.branch
269
+ )
270
+
271
+ # Add project key and resolve identities
272
+ for commit in commits:
273
+ commit["project_key"] = repo_config.project_key or commit.get(
274
+ "inferred_project", "UNKNOWN"
275
+ )
276
+ commit["canonical_id"] = self.identity_resolver.resolve_developer(
277
+ commit["author_name"], commit["author_email"]
278
+ )
279
+
280
+ all_commits.extend(commits)
281
+ log.write_line(f" ✅ Found {len(commits)} commits")
282
+
283
+ # Update live stats
284
+ await self._update_live_stats(
285
+ {
286
+ "repositories_analyzed": i + 1,
287
+ "total_repositories": len(repositories),
288
+ "total_commits": len(all_commits),
289
+ "current_repo": repo_config.name,
290
+ }
291
+ )
292
+
293
+ # Small delay to allow UI updates
294
+ await asyncio.sleep(0.1)
295
+
296
+ except Exception as e:
297
+ log.write_line(f" ❌ Error analyzing {repo_config.name}: {e}")
298
+ continue
299
+
300
+ repo_progress.complete(f"Completed {len(repositories)} repositories")
301
+ return all_commits, all_prs
302
+
303
+ async def _enrich_with_integrations(self, repositories: list, commits: list, log: Log) -> None:
304
+ """Enrich data with integration sources."""
305
+ integration_progress = self.query_one("#integration-progress", AnalysisProgressWidget)
306
+
307
+ end_date = datetime.now(timezone.utc)
308
+ start_date = end_date - timedelta(weeks=self.weeks)
309
+
310
+ for i, repo_config in enumerate(repositories):
311
+ progress = (i / len(repositories)) * 100
312
+ integration_progress.update_progress(progress, f"Enriching {repo_config.name}...")
313
+
314
+ try:
315
+ # Get repository commits for this repo
316
+ repo_commits = [c for c in commits if c.get("repository") == repo_config.name]
317
+
318
+ enrichment = self.orchestrator.enrich_repository_data(
319
+ repo_config, repo_commits, start_date
320
+ )
321
+
322
+ if enrichment.get("prs"):
323
+ log.write_line(
324
+ f" ✅ Found {len(enrichment['prs'])} pull requests for {repo_config.name}"
325
+ )
326
+
327
+ await asyncio.sleep(0.1)
328
+
329
+ except Exception as e:
330
+ log.write_line(f" ⚠️ Integration enrichment failed for {repo_config.name}: {e}")
331
+ continue
332
+
333
+ integration_progress.complete("Integration enrichment complete")
334
+
335
+ async def _resolve_identities(self, commits: list, log: Log) -> list:
336
+ """Resolve developer identities and return statistics."""
337
+ log.write_line("👥 Updating developer statistics...")
338
+
339
+ # Update commit statistics
340
+ self.identity_resolver.update_commit_stats(commits)
341
+ developer_stats = self.identity_resolver.get_developer_stats()
342
+
343
+ log.write_line(f" ✅ Resolved {len(developer_stats)} unique developer identities")
344
+
345
+ # Show top contributors
346
+ top_devs = sorted(developer_stats, key=lambda d: d["total_commits"], reverse=True)[:5]
347
+ for dev in top_devs:
348
+ log.write_line(f" • {dev['primary_name']}: {dev['total_commits']} commits")
349
+
350
+ await asyncio.sleep(0.5)
351
+ return developer_stats
352
+
353
+ async def _run_qualitative_analysis(self, commits: list, log: Log) -> None:
354
+ """Run qualitative analysis if enabled."""
355
+ if not self.enable_qualitative:
356
+ return
357
+
358
+ qual_progress = self.query_one("#qual-progress", AnalysisProgressWidget)
359
+
360
+ try:
361
+ log.write_line("🧠 Starting qualitative analysis...")
362
+
363
+ # Check if NLP engine is pre-loaded from startup
364
+ nlp_engine = None
365
+ if hasattr(self.app, "get_nlp_engine"):
366
+ nlp_engine = self.app.get_nlp_engine()
367
+
368
+ if nlp_engine:
369
+ log.write_line(" ✅ Using pre-loaded NLP engine")
370
+ qual_processor = None # We'll use the NLP engine directly
371
+ else:
372
+ log.write_line(" ⏳ Initializing qualitative processor...")
373
+ # Import qualitative processor
374
+ from gitflow_analytics.qualitative.core.processor import QualitativeProcessor
375
+
376
+ qual_processor = QualitativeProcessor(self.config.qualitative)
377
+
378
+ # Validate setup
379
+ is_valid, issues = qual_processor.validate_setup()
380
+ if not is_valid:
381
+ log.write_line(" ⚠️ Qualitative analysis setup issues:")
382
+ for issue in issues:
383
+ log.write_line(f" - {issue}")
384
+ return
385
+
386
+ # Process commits in batches
387
+ batch_size = 100
388
+ total_batches = (len(commits) + batch_size - 1) // batch_size
389
+
390
+ for batch_idx in range(total_batches):
391
+ start_idx = batch_idx * batch_size
392
+ end_idx = min(start_idx + batch_size, len(commits))
393
+ batch = commits[start_idx:end_idx]
394
+
395
+ progress = (batch_idx / total_batches) * 100
396
+ qual_progress.update_progress(
397
+ progress, f"Processing batch {batch_idx + 1}/{total_batches}..."
398
+ )
399
+
400
+ # Convert to qualitative format
401
+ qual_batch = []
402
+ for commit in batch:
403
+ qual_commit = {
404
+ "hash": commit.get("hash"),
405
+ "message": commit.get("message"),
406
+ "author_name": commit.get("author_name"),
407
+ "author_email": commit.get("author_email"),
408
+ "timestamp": commit.get("timestamp"),
409
+ "files_changed": commit.get("files_changed", []),
410
+ "insertions": commit.get("insertions", 0),
411
+ "deletions": commit.get("deletions", 0),
412
+ "branch": commit.get("branch", "main"),
413
+ }
414
+ qual_batch.append(qual_commit)
415
+
416
+ # Process batch using pre-loaded NLP engine or processor
417
+ if nlp_engine:
418
+ # Use the pre-loaded NLP engine directly
419
+ results = nlp_engine.process_batch(qual_batch)
420
+ else:
421
+ # Use the qualitative processor
422
+ results = qual_processor.process_commits(qual_batch, show_progress=False)
423
+
424
+ # Update original commits with qualitative data
425
+ for original, enhanced in zip(batch, results):
426
+ if hasattr(enhanced, "change_type"):
427
+ original["change_type"] = enhanced.change_type
428
+ original["business_domain"] = enhanced.business_domain
429
+ original["risk_level"] = enhanced.risk_level
430
+ original["confidence_score"] = enhanced.confidence_score
431
+
432
+ await asyncio.sleep(0.1) # Allow UI updates
433
+
434
+ qual_progress.complete("Qualitative analysis complete")
435
+ log.write_line(" ✅ Qualitative analysis completed")
436
+
437
+ except ImportError:
438
+ log.write_line(" ❌ Qualitative analysis dependencies not available")
439
+ qual_progress.update_progress(0, "Dependencies missing")
440
+ except Exception as e:
441
+ log.write_line(f" ❌ Qualitative analysis failed: {e}")
442
+ qual_progress.update_progress(0, f"Error: {str(e)[:30]}...")
443
+
444
+ async def _clone_repository(self, repo_config, log: Log) -> None:
445
+ """Clone repository if needed."""
446
+ try:
447
+ import git
448
+
449
+ repo_config.path.parent.mkdir(parents=True, exist_ok=True)
450
+
451
+ clone_url = f"https://github.com/{repo_config.github_repo}.git"
452
+ if self.config.github.token:
453
+ clone_url = (
454
+ f"https://{self.config.github.token}@github.com/{repo_config.github_repo}.git"
455
+ )
456
+
457
+ # Try to clone with specified branch, fall back to default if it fails
458
+ try:
459
+ if repo_config.branch:
460
+ git.Repo.clone_from(clone_url, repo_config.path, branch=repo_config.branch)
461
+ else:
462
+ git.Repo.clone_from(clone_url, repo_config.path)
463
+ except git.GitCommandError as e:
464
+ if repo_config.branch and "Remote branch" in str(e) and "not found" in str(e):
465
+ # Branch doesn't exist, try cloning without specifying branch
466
+ log.write_line(
467
+ f" ⚠️ Branch '{repo_config.branch}' not found, using repository default"
468
+ )
469
+ git.Repo.clone_from(clone_url, repo_config.path)
470
+ else:
471
+ raise
472
+ log.write_line(f" ✅ Successfully cloned {repo_config.github_repo}")
473
+
474
+ except Exception as e:
475
+ log.write_line(f" ❌ Failed to clone {repo_config.github_repo}: {e}")
476
+ raise
477
+
478
+ async def _update_live_stats(self, stats: dict[str, Any]) -> None:
479
+ """Update live statistics display."""
480
+ stats_widget = self.query_one("#live-stats", Pretty)
481
+ stats_widget.update(stats)
482
+
483
+ def action_cancel(self) -> None:
484
+ """Cancel the analysis."""
485
+ if self.analysis_task and not self.analysis_task.done():
486
+ self.analysis_task.cancel()
487
+ self.app.pop_screen()
488
+
489
+ def action_back(self) -> None:
490
+ """Go back to main screen."""
491
+ self.action_cancel()
492
+
493
+ def action_toggle_log(self) -> None:
494
+ """Toggle log panel visibility."""
495
+ log_panel = self.query_one(".log-panel")
496
+ log_panel.set_class(not log_panel.has_class("hidden"), "hidden")