gitflow-analytics 1.0.3__py3-none-any.whl → 1.3.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitflow_analytics/_version.py +1 -1
- gitflow_analytics/classification/__init__.py +31 -0
- gitflow_analytics/classification/batch_classifier.py +752 -0
- gitflow_analytics/classification/classifier.py +464 -0
- gitflow_analytics/classification/feature_extractor.py +725 -0
- gitflow_analytics/classification/linguist_analyzer.py +574 -0
- gitflow_analytics/classification/model.py +455 -0
- gitflow_analytics/cli.py +4158 -350
- gitflow_analytics/cli_rich.py +198 -48
- gitflow_analytics/config/__init__.py +43 -0
- gitflow_analytics/config/errors.py +261 -0
- gitflow_analytics/config/loader.py +905 -0
- gitflow_analytics/config/profiles.py +264 -0
- gitflow_analytics/config/repository.py +124 -0
- gitflow_analytics/config/schema.py +444 -0
- gitflow_analytics/config/validator.py +154 -0
- gitflow_analytics/config.py +44 -508
- gitflow_analytics/core/analyzer.py +1209 -98
- gitflow_analytics/core/cache.py +1337 -29
- gitflow_analytics/core/data_fetcher.py +1285 -0
- gitflow_analytics/core/identity.py +363 -14
- gitflow_analytics/core/metrics_storage.py +526 -0
- gitflow_analytics/core/progress.py +372 -0
- gitflow_analytics/core/schema_version.py +269 -0
- gitflow_analytics/extractors/ml_tickets.py +1100 -0
- gitflow_analytics/extractors/story_points.py +8 -1
- gitflow_analytics/extractors/tickets.py +749 -11
- gitflow_analytics/identity_llm/__init__.py +6 -0
- gitflow_analytics/identity_llm/analysis_pass.py +231 -0
- gitflow_analytics/identity_llm/analyzer.py +464 -0
- gitflow_analytics/identity_llm/models.py +76 -0
- gitflow_analytics/integrations/github_integration.py +175 -11
- gitflow_analytics/integrations/jira_integration.py +461 -24
- gitflow_analytics/integrations/orchestrator.py +124 -1
- gitflow_analytics/metrics/activity_scoring.py +322 -0
- gitflow_analytics/metrics/branch_health.py +470 -0
- gitflow_analytics/metrics/dora.py +379 -20
- gitflow_analytics/models/database.py +843 -53
- gitflow_analytics/pm_framework/__init__.py +115 -0
- gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
- gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
- gitflow_analytics/pm_framework/base.py +406 -0
- gitflow_analytics/pm_framework/models.py +211 -0
- gitflow_analytics/pm_framework/orchestrator.py +652 -0
- gitflow_analytics/pm_framework/registry.py +333 -0
- gitflow_analytics/qualitative/__init__.py +9 -10
- gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
- gitflow_analytics/qualitative/classifiers/__init__.py +3 -3
- gitflow_analytics/qualitative/classifiers/change_type.py +518 -244
- gitflow_analytics/qualitative/classifiers/domain_classifier.py +272 -165
- gitflow_analytics/qualitative/classifiers/intent_analyzer.py +321 -222
- gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
- gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
- gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
- gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
- gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
- gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
- gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
- gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
- gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
- gitflow_analytics/qualitative/classifiers/risk_analyzer.py +215 -189
- gitflow_analytics/qualitative/core/__init__.py +4 -4
- gitflow_analytics/qualitative/core/llm_fallback.py +239 -235
- gitflow_analytics/qualitative/core/nlp_engine.py +157 -148
- gitflow_analytics/qualitative/core/pattern_cache.py +214 -192
- gitflow_analytics/qualitative/core/processor.py +381 -248
- gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
- gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
- gitflow_analytics/qualitative/models/__init__.py +7 -7
- gitflow_analytics/qualitative/models/schemas.py +155 -121
- gitflow_analytics/qualitative/utils/__init__.py +4 -4
- gitflow_analytics/qualitative/utils/batch_processor.py +136 -123
- gitflow_analytics/qualitative/utils/cost_tracker.py +142 -140
- gitflow_analytics/qualitative/utils/metrics.py +172 -158
- gitflow_analytics/qualitative/utils/text_processing.py +146 -104
- gitflow_analytics/reports/__init__.py +100 -0
- gitflow_analytics/reports/analytics_writer.py +539 -14
- gitflow_analytics/reports/base.py +648 -0
- gitflow_analytics/reports/branch_health_writer.py +322 -0
- gitflow_analytics/reports/classification_writer.py +924 -0
- gitflow_analytics/reports/cli_integration.py +427 -0
- gitflow_analytics/reports/csv_writer.py +1676 -212
- gitflow_analytics/reports/data_models.py +504 -0
- gitflow_analytics/reports/database_report_generator.py +427 -0
- gitflow_analytics/reports/example_usage.py +344 -0
- gitflow_analytics/reports/factory.py +499 -0
- gitflow_analytics/reports/formatters.py +698 -0
- gitflow_analytics/reports/html_generator.py +1116 -0
- gitflow_analytics/reports/interfaces.py +489 -0
- gitflow_analytics/reports/json_exporter.py +2770 -0
- gitflow_analytics/reports/narrative_writer.py +2287 -158
- gitflow_analytics/reports/story_point_correlation.py +1144 -0
- gitflow_analytics/reports/weekly_trends_writer.py +389 -0
- gitflow_analytics/training/__init__.py +5 -0
- gitflow_analytics/training/model_loader.py +377 -0
- gitflow_analytics/training/pipeline.py +550 -0
- gitflow_analytics/tui/__init__.py +1 -1
- gitflow_analytics/tui/app.py +129 -126
- gitflow_analytics/tui/screens/__init__.py +3 -3
- gitflow_analytics/tui/screens/analysis_progress_screen.py +188 -179
- gitflow_analytics/tui/screens/configuration_screen.py +154 -178
- gitflow_analytics/tui/screens/loading_screen.py +100 -110
- gitflow_analytics/tui/screens/main_screen.py +89 -72
- gitflow_analytics/tui/screens/results_screen.py +305 -281
- gitflow_analytics/tui/widgets/__init__.py +2 -2
- gitflow_analytics/tui/widgets/data_table.py +67 -69
- gitflow_analytics/tui/widgets/export_modal.py +76 -76
- gitflow_analytics/tui/widgets/progress_widget.py +41 -46
- gitflow_analytics-1.3.11.dist-info/METADATA +1015 -0
- gitflow_analytics-1.3.11.dist-info/RECORD +122 -0
- gitflow_analytics-1.0.3.dist-info/METADATA +0 -490
- gitflow_analytics-1.0.3.dist-info/RECORD +0 -62
- {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.11.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.11.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.11.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.11.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
|
|
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
|
-
"
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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 /
|
|
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,
|
|
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,
|
|
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) ->
|
|
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(
|
|
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:
|
|
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[
|
|
285
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
331
|
-
log.write_line(
|
|
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:
|
|
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[
|
|
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:
|
|
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,
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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,
|
|
434
|
-
original[
|
|
435
|
-
original[
|
|
436
|
-
original[
|
|
437
|
-
original[
|
|
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 =
|
|
461
|
-
|
|
462
|
-
|
|
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:
|
|
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")
|