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