gitflow-analytics 1.0.1__py3-none-any.whl → 1.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitflow_analytics/__init__.py +11 -11
- gitflow_analytics/_version.py +2 -2
- gitflow_analytics/cli.py +612 -258
- gitflow_analytics/cli_rich.py +353 -0
- gitflow_analytics/config.py +251 -141
- gitflow_analytics/core/analyzer.py +140 -103
- gitflow_analytics/core/branch_mapper.py +132 -132
- gitflow_analytics/core/cache.py +240 -169
- gitflow_analytics/core/identity.py +210 -173
- gitflow_analytics/extractors/base.py +13 -11
- gitflow_analytics/extractors/story_points.py +70 -59
- gitflow_analytics/extractors/tickets.py +101 -87
- gitflow_analytics/integrations/github_integration.py +84 -77
- gitflow_analytics/integrations/jira_integration.py +116 -104
- gitflow_analytics/integrations/orchestrator.py +86 -85
- gitflow_analytics/metrics/dora.py +181 -177
- gitflow_analytics/models/database.py +190 -53
- gitflow_analytics/qualitative/__init__.py +30 -0
- gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
- gitflow_analytics/qualitative/classifiers/change_type.py +468 -0
- gitflow_analytics/qualitative/classifiers/domain_classifier.py +399 -0
- gitflow_analytics/qualitative/classifiers/intent_analyzer.py +436 -0
- gitflow_analytics/qualitative/classifiers/risk_analyzer.py +412 -0
- gitflow_analytics/qualitative/core/__init__.py +13 -0
- gitflow_analytics/qualitative/core/llm_fallback.py +653 -0
- gitflow_analytics/qualitative/core/nlp_engine.py +373 -0
- gitflow_analytics/qualitative/core/pattern_cache.py +457 -0
- gitflow_analytics/qualitative/core/processor.py +540 -0
- gitflow_analytics/qualitative/models/__init__.py +25 -0
- gitflow_analytics/qualitative/models/schemas.py +272 -0
- gitflow_analytics/qualitative/utils/__init__.py +13 -0
- gitflow_analytics/qualitative/utils/batch_processor.py +326 -0
- gitflow_analytics/qualitative/utils/cost_tracker.py +343 -0
- gitflow_analytics/qualitative/utils/metrics.py +347 -0
- gitflow_analytics/qualitative/utils/text_processing.py +243 -0
- gitflow_analytics/reports/analytics_writer.py +11 -4
- gitflow_analytics/reports/csv_writer.py +51 -31
- gitflow_analytics/reports/narrative_writer.py +16 -14
- gitflow_analytics/tui/__init__.py +5 -0
- gitflow_analytics/tui/app.py +721 -0
- gitflow_analytics/tui/screens/__init__.py +8 -0
- gitflow_analytics/tui/screens/analysis_progress_screen.py +487 -0
- gitflow_analytics/tui/screens/configuration_screen.py +547 -0
- gitflow_analytics/tui/screens/loading_screen.py +358 -0
- gitflow_analytics/tui/screens/main_screen.py +304 -0
- gitflow_analytics/tui/screens/results_screen.py +698 -0
- gitflow_analytics/tui/widgets/__init__.py +7 -0
- gitflow_analytics/tui/widgets/data_table.py +257 -0
- gitflow_analytics/tui/widgets/export_modal.py +301 -0
- gitflow_analytics/tui/widgets/progress_widget.py +192 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/METADATA +31 -4
- gitflow_analytics-1.0.3.dist-info/RECORD +62 -0
- gitflow_analytics-1.0.1.dist-info/RECORD +0 -31
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
"""Main TUI application for GitFlow Analytics."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from textual.app import App, ComposeResult
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
|
|
10
|
+
from .screens.main_screen import MainScreen
|
|
11
|
+
from .screens.configuration_screen import ConfigurationScreen
|
|
12
|
+
from .screens.analysis_progress_screen import AnalysisProgressScreen
|
|
13
|
+
from .screens.results_screen import ResultsScreen
|
|
14
|
+
from .screens.loading_screen import InitializationLoadingScreen
|
|
15
|
+
from gitflow_analytics.config import ConfigLoader, Config
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GitFlowAnalyticsApp(App):
|
|
19
|
+
"""
|
|
20
|
+
Main Terminal User Interface application for GitFlow Analytics.
|
|
21
|
+
|
|
22
|
+
WHY: Provides a comprehensive TUI that guides users through the entire
|
|
23
|
+
analytics workflow from configuration to results analysis. Designed to
|
|
24
|
+
be more user-friendly than command-line interface while maintaining
|
|
25
|
+
the power and flexibility of the core analysis engine.
|
|
26
|
+
|
|
27
|
+
DESIGN DECISION: Uses a screen-based navigation model where each major
|
|
28
|
+
workflow step (configuration, analysis, results) has its own dedicated
|
|
29
|
+
screen. This provides clear context separation and allows for complex
|
|
30
|
+
interactions within each workflow step.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
TITLE = "GitFlow Analytics"
|
|
34
|
+
SUB_TITLE = "Developer Productivity Analysis"
|
|
35
|
+
|
|
36
|
+
CSS = """
|
|
37
|
+
/* Global styles */
|
|
38
|
+
.screen-title {
|
|
39
|
+
text-align: center;
|
|
40
|
+
text-style: bold;
|
|
41
|
+
color: $accent;
|
|
42
|
+
margin: 1;
|
|
43
|
+
padding: 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.section-title {
|
|
47
|
+
text-style: bold;
|
|
48
|
+
color: $secondary;
|
|
49
|
+
margin: 1 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.subsection-title {
|
|
53
|
+
text-style: bold;
|
|
54
|
+
color: $warning;
|
|
55
|
+
margin: 1 0 0 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.help-text {
|
|
59
|
+
color: $text-muted;
|
|
60
|
+
text-style: italic;
|
|
61
|
+
margin: 0 0 1 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.info-message {
|
|
65
|
+
color: $warning;
|
|
66
|
+
text-style: italic;
|
|
67
|
+
margin: 1 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Panel styles */
|
|
71
|
+
.status-panel {
|
|
72
|
+
border: solid $primary;
|
|
73
|
+
margin: 1;
|
|
74
|
+
padding: 1;
|
|
75
|
+
height: auto;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.actions-panel {
|
|
79
|
+
border: solid $secondary;
|
|
80
|
+
margin: 1;
|
|
81
|
+
padding: 1;
|
|
82
|
+
height: auto;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.stats-panel {
|
|
86
|
+
border: solid $accent;
|
|
87
|
+
margin: 1;
|
|
88
|
+
padding: 1;
|
|
89
|
+
height: auto;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.progress-panel {
|
|
93
|
+
height: 8;
|
|
94
|
+
border: solid $primary;
|
|
95
|
+
margin: 1;
|
|
96
|
+
padding: 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.log-panel {
|
|
100
|
+
border: solid $accent;
|
|
101
|
+
margin: 1;
|
|
102
|
+
padding: 1;
|
|
103
|
+
min-height: 10;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.panel-title {
|
|
107
|
+
text-style: bold;
|
|
108
|
+
color: $primary;
|
|
109
|
+
margin-bottom: 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Form styles */
|
|
113
|
+
.form-row {
|
|
114
|
+
height: 3;
|
|
115
|
+
margin: 1 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.form-label {
|
|
119
|
+
width: 25;
|
|
120
|
+
padding: 1 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.form-input {
|
|
124
|
+
width: 1fr;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Button styles */
|
|
128
|
+
.button-bar {
|
|
129
|
+
dock: bottom;
|
|
130
|
+
height: 3;
|
|
131
|
+
align: center middle;
|
|
132
|
+
margin: 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.action-bar {
|
|
136
|
+
height: 3;
|
|
137
|
+
align: center middle;
|
|
138
|
+
margin: 1 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Modal styles */
|
|
142
|
+
#config-modal {
|
|
143
|
+
background: $surface;
|
|
144
|
+
border: thick $primary;
|
|
145
|
+
width: 90%;
|
|
146
|
+
height: 85%;
|
|
147
|
+
padding: 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.modal-title {
|
|
151
|
+
text-align: center;
|
|
152
|
+
text-style: bold;
|
|
153
|
+
color: $primary;
|
|
154
|
+
margin-bottom: 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Validation styles */
|
|
158
|
+
.validation-error {
|
|
159
|
+
color: $error;
|
|
160
|
+
text-style: bold;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.validation-success {
|
|
164
|
+
color: $success;
|
|
165
|
+
text-style: bold;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Table styles */
|
|
169
|
+
EnhancedDataTable {
|
|
170
|
+
height: auto;
|
|
171
|
+
min-height: 20;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
EnhancedDataTable > .datatable--header {
|
|
175
|
+
background: $primary 10%;
|
|
176
|
+
color: $primary;
|
|
177
|
+
text-style: bold;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
EnhancedDataTable > .datatable--row-hover {
|
|
181
|
+
background: $accent 20%;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
EnhancedDataTable > .datatable--row-cursor {
|
|
185
|
+
background: $primary 30%;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Progress widget styles */
|
|
189
|
+
AnalysisProgressWidget {
|
|
190
|
+
height: auto;
|
|
191
|
+
border: solid $primary;
|
|
192
|
+
margin: 1;
|
|
193
|
+
padding: 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.progress-title {
|
|
197
|
+
text-style: bold;
|
|
198
|
+
color: $primary;
|
|
199
|
+
margin-bottom: 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.progress-status {
|
|
203
|
+
color: $text;
|
|
204
|
+
margin-top: 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.progress-eta {
|
|
208
|
+
color: $accent;
|
|
209
|
+
text-style: italic;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Loading screen styles */
|
|
213
|
+
#loading-container {
|
|
214
|
+
height: 100%;
|
|
215
|
+
align: center middle;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#loading-content {
|
|
219
|
+
width: 80%;
|
|
220
|
+
max-width: 100;
|
|
221
|
+
align: center middle;
|
|
222
|
+
margin: 2;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#main-spinner {
|
|
226
|
+
margin: 1;
|
|
227
|
+
align: center middle;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#loading-steps {
|
|
231
|
+
margin-top: 1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#loading-steps > Static {
|
|
235
|
+
margin: 0 0 0 2;
|
|
236
|
+
padding: 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* Utility classes */
|
|
240
|
+
.hidden {
|
|
241
|
+
display: none;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.center {
|
|
245
|
+
text-align: center;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.bold {
|
|
249
|
+
text-style: bold;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.italic {
|
|
253
|
+
text-style: italic;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.error {
|
|
257
|
+
color: $error;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.success {
|
|
261
|
+
color: $success;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.warning {
|
|
265
|
+
color: $warning;
|
|
266
|
+
}
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
BINDINGS = [
|
|
270
|
+
Binding("ctrl+q", "quit", "Quit", priority=True),
|
|
271
|
+
Binding("ctrl+c", "quit", "Quit", priority=True),
|
|
272
|
+
Binding("f1", "help", "Help"),
|
|
273
|
+
Binding("ctrl+d", "toggle_dark", "Toggle Dark Mode"),
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
def __init__(self) -> None:
|
|
277
|
+
super().__init__()
|
|
278
|
+
self.config: Optional[Config] = None
|
|
279
|
+
self.config_path: Optional[Path] = None
|
|
280
|
+
self.initialization_complete = False
|
|
281
|
+
self._nlp_engine = None
|
|
282
|
+
|
|
283
|
+
def compose(self) -> ComposeResult:
|
|
284
|
+
"""
|
|
285
|
+
Compose the main application. We don't yield screens here during startup
|
|
286
|
+
to avoid screen stack management issues.
|
|
287
|
+
|
|
288
|
+
WHY: Instead of yielding screens in compose, we handle the initial screen
|
|
289
|
+
setup in on_mount to ensure proper screen stack management and avoid
|
|
290
|
+
IndexError when transitioning between screens.
|
|
291
|
+
"""
|
|
292
|
+
# Don't yield screens here - handle in on_mount for proper screen management
|
|
293
|
+
# Return empty to avoid NoneType iteration error
|
|
294
|
+
return iter([])
|
|
295
|
+
|
|
296
|
+
def on_mount(self) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Handle application startup with loading screen.
|
|
299
|
+
|
|
300
|
+
WHY: Shows loading screen immediately while heavy initialization
|
|
301
|
+
happens in the background, providing better user experience.
|
|
302
|
+
Using push_screen instead of compose to avoid screen stack issues.
|
|
303
|
+
"""
|
|
304
|
+
# Set up application title
|
|
305
|
+
self.title = "GitFlow Analytics"
|
|
306
|
+
self.sub_title = "Developer Productivity Analysis"
|
|
307
|
+
|
|
308
|
+
# Start with loading screen during initialization
|
|
309
|
+
if not self.initialization_complete:
|
|
310
|
+
loading_screen = InitializationLoadingScreen(
|
|
311
|
+
config_loader_func=self._load_default_config,
|
|
312
|
+
nlp_init_func=self._initialize_nlp_engine,
|
|
313
|
+
loading_message="Initializing GitFlow Analytics...",
|
|
314
|
+
id="loading-screen"
|
|
315
|
+
)
|
|
316
|
+
self.push_screen(loading_screen)
|
|
317
|
+
else:
|
|
318
|
+
# Show main screen if already initialized
|
|
319
|
+
main_screen = MainScreen(self.config, self.config_path, id="main-screen")
|
|
320
|
+
self.push_screen(main_screen)
|
|
321
|
+
|
|
322
|
+
def _load_default_config(self) -> Optional[tuple]:
|
|
323
|
+
"""
|
|
324
|
+
Attempt to load configuration from default locations.
|
|
325
|
+
|
|
326
|
+
WHY: Provides automatic configuration discovery to reduce setup
|
|
327
|
+
friction for users who have configurations in standard locations.
|
|
328
|
+
This version is designed to be called from the loading screen.
|
|
329
|
+
|
|
330
|
+
@return: Tuple of (config, config_path) if found, None otherwise
|
|
331
|
+
"""
|
|
332
|
+
default_config_paths = [
|
|
333
|
+
Path("config.yaml"),
|
|
334
|
+
Path("gitflow.yaml"),
|
|
335
|
+
Path(".gitflow/config.yaml"),
|
|
336
|
+
Path("~/.gitflow/config.yaml").expanduser(),
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
for config_path in default_config_paths:
|
|
340
|
+
if config_path.exists():
|
|
341
|
+
try:
|
|
342
|
+
config = ConfigLoader.load(config_path)
|
|
343
|
+
return (config, config_path)
|
|
344
|
+
|
|
345
|
+
except Exception as e:
|
|
346
|
+
# Log error but continue trying other paths
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
def _initialize_nlp_engine(self, config: Optional[Config] = None) -> Optional[object]:
|
|
352
|
+
"""
|
|
353
|
+
Initialize the NLP engine for qualitative analysis.
|
|
354
|
+
|
|
355
|
+
WHY: spaCy model loading can be slow and should happen during
|
|
356
|
+
the loading screen to provide user feedback. This method handles
|
|
357
|
+
the heavy NLP initialization work.
|
|
358
|
+
|
|
359
|
+
@param config: Configuration object with qualitative settings
|
|
360
|
+
@return: Initialized NLP engine object or None if not needed/failed
|
|
361
|
+
"""
|
|
362
|
+
if not config or not getattr(config, 'qualitative', None):
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
if not config.qualitative.enabled:
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
# Import here to avoid slow imports at module level
|
|
370
|
+
from gitflow_analytics.qualitative.core.nlp_engine import NLPEngine
|
|
371
|
+
|
|
372
|
+
# Initialize the NLP engine (this loads spaCy models)
|
|
373
|
+
nlp_engine = NLPEngine(config.qualitative.nlp)
|
|
374
|
+
|
|
375
|
+
# Validate the setup
|
|
376
|
+
is_valid, issues = nlp_engine.validate_setup()
|
|
377
|
+
if not is_valid:
|
|
378
|
+
# Return None if validation fails, but don't raise exception
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
return nlp_engine
|
|
382
|
+
|
|
383
|
+
except ImportError:
|
|
384
|
+
# Qualitative analysis dependencies not available
|
|
385
|
+
return None
|
|
386
|
+
except Exception:
|
|
387
|
+
# Other initialization errors
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
def on_initialization_loading_screen_initialization_complete(
|
|
391
|
+
self, message: InitializationLoadingScreen.InitializationComplete
|
|
392
|
+
) -> None:
|
|
393
|
+
"""
|
|
394
|
+
Handle completion of initialization loading.
|
|
395
|
+
|
|
396
|
+
WHY: When the loading screen completes initialization, we need to
|
|
397
|
+
update the app state and transition to the main screen. Using pop_screen
|
|
398
|
+
and push_screen for proper screen stack management.
|
|
399
|
+
"""
|
|
400
|
+
initialization_data = message.data
|
|
401
|
+
|
|
402
|
+
# Update app state with loaded configuration
|
|
403
|
+
config_result = initialization_data.get('config')
|
|
404
|
+
if config_result:
|
|
405
|
+
self.config, self.config_path = config_result
|
|
406
|
+
|
|
407
|
+
# Store NLP engine for later use
|
|
408
|
+
self._nlp_engine = initialization_data.get('nlp')
|
|
409
|
+
|
|
410
|
+
# Mark initialization as complete
|
|
411
|
+
self.initialization_complete = True
|
|
412
|
+
|
|
413
|
+
# Transition to main screen by popping loading screen and pushing main screen
|
|
414
|
+
try:
|
|
415
|
+
self.pop_screen() # Remove loading screen
|
|
416
|
+
except:
|
|
417
|
+
pass # Ignore if no screen to pop
|
|
418
|
+
|
|
419
|
+
main_screen = MainScreen(self.config, self.config_path, id="main-screen")
|
|
420
|
+
self.push_screen(main_screen)
|
|
421
|
+
|
|
422
|
+
# Show success notification if config was loaded
|
|
423
|
+
if self.config and self.config_path:
|
|
424
|
+
self.notify(f"Loaded configuration from {self.config_path}", severity="info")
|
|
425
|
+
|
|
426
|
+
def on_initialization_loading_screen_loading_cancelled(
|
|
427
|
+
self, message: InitializationLoadingScreen.LoadingCancelled
|
|
428
|
+
) -> None:
|
|
429
|
+
"""
|
|
430
|
+
Handle cancellation of initialization loading.
|
|
431
|
+
|
|
432
|
+
WHY: If user cancels loading, we should still show the main screen
|
|
433
|
+
but without the loaded configuration. Using pop_screen and push_screen
|
|
434
|
+
for proper screen stack management.
|
|
435
|
+
"""
|
|
436
|
+
# Mark initialization as complete (even if cancelled)
|
|
437
|
+
self.initialization_complete = True
|
|
438
|
+
|
|
439
|
+
# Transition to main screen without configuration
|
|
440
|
+
try:
|
|
441
|
+
self.pop_screen() # Remove loading screen
|
|
442
|
+
except:
|
|
443
|
+
pass # Ignore if no screen to pop
|
|
444
|
+
|
|
445
|
+
main_screen = MainScreen(None, None, id="main-screen")
|
|
446
|
+
self.push_screen(main_screen)
|
|
447
|
+
|
|
448
|
+
self.notify("Initialization cancelled. You can load configuration manually.", severity="warning")
|
|
449
|
+
|
|
450
|
+
def on_main_screen_new_analysis_requested(self, message: MainScreen.NewAnalysisRequested) -> None:
|
|
451
|
+
"""Handle new analysis request from main screen."""
|
|
452
|
+
if not self.config:
|
|
453
|
+
self.notify("Please load or create a configuration first", severity="error")
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
# Launch analysis progress screen
|
|
457
|
+
analysis_screen = AnalysisProgressScreen(
|
|
458
|
+
config=self.config,
|
|
459
|
+
weeks=12, # TODO: Get from config or user preference
|
|
460
|
+
enable_qualitative=getattr(self.config, 'qualitative', None) and self.config.qualitative.enabled
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
self.push_screen(analysis_screen)
|
|
464
|
+
|
|
465
|
+
def on_main_screen_configuration_requested(self, message: MainScreen.ConfigurationRequested) -> None:
|
|
466
|
+
"""Handle configuration request from main screen."""
|
|
467
|
+
config_screen = ConfigurationScreen(
|
|
468
|
+
config_path=self.config_path,
|
|
469
|
+
config=self.config
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def handle_config_result(config: Optional[Config]) -> None:
|
|
473
|
+
if config:
|
|
474
|
+
self.config = config
|
|
475
|
+
# TODO: Set config_path if provided
|
|
476
|
+
|
|
477
|
+
# Update main screen
|
|
478
|
+
main_screen = self.query_one("#main-screen", MainScreen)
|
|
479
|
+
main_screen.update_config(self.config, self.config_path)
|
|
480
|
+
|
|
481
|
+
self.notify("Configuration updated successfully", severity="success")
|
|
482
|
+
|
|
483
|
+
self.push_screen(config_screen, handle_config_result)
|
|
484
|
+
|
|
485
|
+
def on_main_screen_cache_status_requested(self, message: MainScreen.CacheStatusRequested) -> None:
|
|
486
|
+
"""Handle cache status request from main screen."""
|
|
487
|
+
if not self.config:
|
|
488
|
+
self.notify("No configuration loaded", severity="error")
|
|
489
|
+
return
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
from gitflow_analytics.core.cache import GitAnalysisCache
|
|
493
|
+
|
|
494
|
+
cache = GitAnalysisCache(self.config.cache.directory)
|
|
495
|
+
stats = cache.get_cache_stats()
|
|
496
|
+
|
|
497
|
+
# Calculate cache size
|
|
498
|
+
import os
|
|
499
|
+
cache_size = 0
|
|
500
|
+
try:
|
|
501
|
+
for root, _dirs, files in os.walk(self.config.cache.directory):
|
|
502
|
+
for f in files:
|
|
503
|
+
cache_size += os.path.getsize(os.path.join(root, f))
|
|
504
|
+
cache_size_mb = cache_size / 1024 / 1024
|
|
505
|
+
except:
|
|
506
|
+
cache_size_mb = 0
|
|
507
|
+
|
|
508
|
+
message_text = f"""Cache Statistics:
|
|
509
|
+
• Location: {self.config.cache.directory}
|
|
510
|
+
• Cached commits: {stats['cached_commits']:,}
|
|
511
|
+
• Cached PRs: {stats['cached_prs']:,}
|
|
512
|
+
• Cached issues: {stats['cached_issues']:,}
|
|
513
|
+
• Stale entries: {stats['stale_commits']:,}
|
|
514
|
+
• Cache size: {cache_size_mb:.1f} MB
|
|
515
|
+
• TTL: {self.config.cache.ttl_hours} hours"""
|
|
516
|
+
|
|
517
|
+
self.notify(message_text, severity="info")
|
|
518
|
+
|
|
519
|
+
except Exception as e:
|
|
520
|
+
self.notify(f"Failed to get cache statistics: {e}", severity="error")
|
|
521
|
+
|
|
522
|
+
def on_main_screen_identity_management_requested(self, message: MainScreen.IdentityManagementRequested) -> None:
|
|
523
|
+
"""Handle identity management request from main screen."""
|
|
524
|
+
if not self.config:
|
|
525
|
+
self.notify("No configuration loaded", severity="error")
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
from gitflow_analytics.core.identity import DeveloperIdentityResolver
|
|
530
|
+
|
|
531
|
+
identity_resolver = DeveloperIdentityResolver(
|
|
532
|
+
self.config.cache.directory / "identities.db"
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
developers = identity_resolver.get_developer_stats()
|
|
536
|
+
|
|
537
|
+
if not developers:
|
|
538
|
+
self.notify("No developer identities found. Run analysis first.", severity="info")
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
# Show top developers
|
|
542
|
+
top_devs = sorted(developers, key=lambda d: d['total_commits'], reverse=True)[:10]
|
|
543
|
+
|
|
544
|
+
dev_list = []
|
|
545
|
+
for dev in top_devs:
|
|
546
|
+
dev_list.append(f"• {dev['primary_name']}: {dev['total_commits']} commits, {dev['alias_count']} aliases")
|
|
547
|
+
|
|
548
|
+
message_text = f"""Developer Identity Statistics:
|
|
549
|
+
• Total unique developers: {len(developers)}
|
|
550
|
+
• Manual mappings: {len(self.config.analysis.manual_identity_mappings) if self.config.analysis.manual_identity_mappings else 0}
|
|
551
|
+
|
|
552
|
+
Top Contributors:
|
|
553
|
+
{chr(10).join(dev_list)}
|
|
554
|
+
|
|
555
|
+
Use the CLI 'merge-identity' command to merge duplicate identities."""
|
|
556
|
+
|
|
557
|
+
self.notify(message_text, severity="info")
|
|
558
|
+
|
|
559
|
+
except Exception as e:
|
|
560
|
+
self.notify(f"Failed to get identity information: {e}", severity="error")
|
|
561
|
+
|
|
562
|
+
def on_main_screen_help_requested(self, message: MainScreen.HelpRequested) -> None:
|
|
563
|
+
"""Handle help request from main screen."""
|
|
564
|
+
help_text = """GitFlow Analytics - Terminal UI Help
|
|
565
|
+
|
|
566
|
+
🚀 Getting Started:
|
|
567
|
+
1. Load or create a configuration file (API keys, repositories)
|
|
568
|
+
2. Configure analysis settings (time period, options)
|
|
569
|
+
3. Run analysis to process your repositories
|
|
570
|
+
4. Explore results and export reports
|
|
571
|
+
|
|
572
|
+
⌨️ Key Bindings:
|
|
573
|
+
• Ctrl+Q / Ctrl+C: Quit application
|
|
574
|
+
• F1: Show this help
|
|
575
|
+
• Ctrl+D: Toggle dark/light mode
|
|
576
|
+
• Escape: Go back/cancel current action
|
|
577
|
+
|
|
578
|
+
📁 Configuration:
|
|
579
|
+
• GitHub Personal Access Token required
|
|
580
|
+
• OpenRouter API key for qualitative analysis
|
|
581
|
+
• JIRA optional for enhanced ticket tracking
|
|
582
|
+
|
|
583
|
+
🔧 Analysis Features:
|
|
584
|
+
• Git commit analysis with developer identity resolution
|
|
585
|
+
• Pull request metrics and timing analysis
|
|
586
|
+
• Qualitative analysis using AI for commit categorization
|
|
587
|
+
• DORA metrics calculation
|
|
588
|
+
• Story point tracking from commit messages
|
|
589
|
+
|
|
590
|
+
📊 Export Options:
|
|
591
|
+
• CSV reports for spreadsheet analysis
|
|
592
|
+
• JSON export for API integration
|
|
593
|
+
• Markdown reports for documentation
|
|
594
|
+
|
|
595
|
+
💡 Tips:
|
|
596
|
+
• Use organization auto-discovery for multiple repositories
|
|
597
|
+
• Enable qualitative analysis for deeper insights
|
|
598
|
+
• Review identity mappings for accurate attribution
|
|
599
|
+
|
|
600
|
+
For more information: https://github.com/bobmatnyc/gitflow-analytics"""
|
|
601
|
+
|
|
602
|
+
self.notify(help_text, severity="info")
|
|
603
|
+
|
|
604
|
+
def action_quit(self) -> None:
|
|
605
|
+
"""
|
|
606
|
+
Quit the application with confirmation if analysis is running.
|
|
607
|
+
|
|
608
|
+
WHY: Provides safe exit that checks for running operations to
|
|
609
|
+
prevent data loss or corruption from incomplete analysis.
|
|
610
|
+
"""
|
|
611
|
+
# Check if analysis is running
|
|
612
|
+
try:
|
|
613
|
+
analysis_screen = self.query("AnalysisProgressScreen")
|
|
614
|
+
if analysis_screen:
|
|
615
|
+
# TODO: Show confirmation dialog
|
|
616
|
+
pass
|
|
617
|
+
except:
|
|
618
|
+
pass
|
|
619
|
+
|
|
620
|
+
self.exit()
|
|
621
|
+
|
|
622
|
+
def action_help(self) -> None:
|
|
623
|
+
"""Show application help."""
|
|
624
|
+
# Trigger help from main screen if available
|
|
625
|
+
try:
|
|
626
|
+
main_screen = self.query_one("#main-screen", MainScreen)
|
|
627
|
+
main_screen.action_help()
|
|
628
|
+
except:
|
|
629
|
+
# Fallback to direct help
|
|
630
|
+
self.on_main_screen_help_requested(MainScreen.HelpRequested())
|
|
631
|
+
|
|
632
|
+
def action_toggle_dark(self) -> None:
|
|
633
|
+
"""
|
|
634
|
+
Toggle between dark and light themes.
|
|
635
|
+
|
|
636
|
+
WHY: Provides theme flexibility for different user preferences
|
|
637
|
+
and working environments (bright vs dim lighting conditions).
|
|
638
|
+
"""
|
|
639
|
+
self.dark = not self.dark
|
|
640
|
+
theme = "dark" if self.dark else "light"
|
|
641
|
+
self.notify(f"Switched to {theme} theme", severity="info")
|
|
642
|
+
|
|
643
|
+
def get_current_config(self) -> Optional[Config]:
|
|
644
|
+
"""Get the currently loaded configuration."""
|
|
645
|
+
return self.config
|
|
646
|
+
|
|
647
|
+
def get_current_config_path(self) -> Optional[Path]:
|
|
648
|
+
"""Get the path of the currently loaded configuration."""
|
|
649
|
+
return self.config_path
|
|
650
|
+
|
|
651
|
+
def get_nlp_engine(self) -> Optional[object]:
|
|
652
|
+
"""
|
|
653
|
+
Get the initialized NLP engine for qualitative analysis.
|
|
654
|
+
|
|
655
|
+
WHY: Provides access to the pre-loaded NLP engine to avoid
|
|
656
|
+
re-initialization overhead during analysis operations.
|
|
657
|
+
|
|
658
|
+
@return: Initialized NLP engine or None if not available
|
|
659
|
+
"""
|
|
660
|
+
return self._nlp_engine
|
|
661
|
+
|
|
662
|
+
def update_config(self, config: Config, config_path: Optional[Path] = None) -> None:
|
|
663
|
+
"""
|
|
664
|
+
Update the application configuration and refresh relevant screens.
|
|
665
|
+
|
|
666
|
+
WHY: Provides centralized configuration updates that can be called
|
|
667
|
+
from any screen to ensure all parts of the application stay in sync.
|
|
668
|
+
"""
|
|
669
|
+
self.config = config
|
|
670
|
+
self.config_path = config_path
|
|
671
|
+
|
|
672
|
+
# Update main screen if visible
|
|
673
|
+
try:
|
|
674
|
+
main_screen = self.query_one("#main-screen", MainScreen)
|
|
675
|
+
main_screen.update_config(config, config_path)
|
|
676
|
+
except:
|
|
677
|
+
pass
|
|
678
|
+
|
|
679
|
+
async def run_analysis_async(
|
|
680
|
+
self,
|
|
681
|
+
weeks: int = 12,
|
|
682
|
+
enable_qualitative: bool = True
|
|
683
|
+
) -> Optional[dict]:
|
|
684
|
+
"""
|
|
685
|
+
Run analysis asynchronously and return results.
|
|
686
|
+
|
|
687
|
+
WHY: Provides programmatic access to analysis functionality
|
|
688
|
+
for integration with other systems or automated workflows.
|
|
689
|
+
|
|
690
|
+
@param weeks: Number of weeks to analyze
|
|
691
|
+
@param enable_qualitative: Whether to enable qualitative analysis
|
|
692
|
+
@return: Analysis results dictionary or None if failed
|
|
693
|
+
"""
|
|
694
|
+
if not self.config:
|
|
695
|
+
raise ValueError("No configuration loaded")
|
|
696
|
+
|
|
697
|
+
# Create analysis screen
|
|
698
|
+
analysis_screen = AnalysisProgressScreen(
|
|
699
|
+
config=self.config,
|
|
700
|
+
weeks=weeks,
|
|
701
|
+
enable_qualitative=enable_qualitative
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
# Run analysis (this would need to be implemented properly)
|
|
705
|
+
# For now, return None to indicate not implemented
|
|
706
|
+
return None
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def main() -> None:
|
|
710
|
+
"""
|
|
711
|
+
Main entry point for the TUI application.
|
|
712
|
+
|
|
713
|
+
WHY: Provides a clean entry point that can be called from the CLI
|
|
714
|
+
or used as a standalone application launcher.
|
|
715
|
+
"""
|
|
716
|
+
app = GitFlowAnalyticsApp()
|
|
717
|
+
app.run()
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
if __name__ == "__main__":
|
|
721
|
+
main()
|