gitflow-analytics 1.3.11__py3-none-any.whl → 3.3.0__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/batch_classifier.py +156 -4
- gitflow_analytics/cli.py +803 -135
- gitflow_analytics/config/loader.py +39 -1
- gitflow_analytics/config/schema.py +1 -0
- gitflow_analytics/core/cache.py +20 -0
- gitflow_analytics/core/data_fetcher.py +1051 -117
- gitflow_analytics/core/git_auth.py +169 -0
- gitflow_analytics/core/git_timeout_wrapper.py +347 -0
- gitflow_analytics/core/metrics_storage.py +12 -3
- gitflow_analytics/core/progress.py +219 -18
- gitflow_analytics/core/subprocess_git.py +145 -0
- gitflow_analytics/extractors/ml_tickets.py +3 -2
- gitflow_analytics/extractors/tickets.py +93 -8
- gitflow_analytics/integrations/jira_integration.py +1 -1
- gitflow_analytics/integrations/orchestrator.py +47 -29
- gitflow_analytics/metrics/branch_health.py +3 -2
- gitflow_analytics/models/database.py +72 -1
- gitflow_analytics/pm_framework/adapters/jira_adapter.py +12 -5
- gitflow_analytics/pm_framework/orchestrator.py +8 -3
- gitflow_analytics/qualitative/classifiers/llm/openai_client.py +24 -4
- gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +3 -1
- gitflow_analytics/qualitative/core/llm_fallback.py +34 -2
- gitflow_analytics/reports/narrative_writer.py +118 -74
- gitflow_analytics/security/__init__.py +11 -0
- gitflow_analytics/security/config.py +189 -0
- gitflow_analytics/security/extractors/__init__.py +7 -0
- gitflow_analytics/security/extractors/dependency_checker.py +379 -0
- gitflow_analytics/security/extractors/secret_detector.py +197 -0
- gitflow_analytics/security/extractors/vulnerability_scanner.py +333 -0
- gitflow_analytics/security/llm_analyzer.py +347 -0
- gitflow_analytics/security/reports/__init__.py +5 -0
- gitflow_analytics/security/reports/security_report.py +358 -0
- gitflow_analytics/security/security_analyzer.py +414 -0
- gitflow_analytics/tui/app.py +3 -1
- gitflow_analytics/tui/progress_adapter.py +313 -0
- gitflow_analytics/tui/screens/analysis_progress_screen.py +407 -46
- gitflow_analytics/tui/screens/results_screen.py +219 -206
- gitflow_analytics/ui/__init__.py +21 -0
- gitflow_analytics/ui/progress_display.py +1477 -0
- gitflow_analytics/verify_activity.py +697 -0
- {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/METADATA +2 -1
- {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/RECORD +47 -31
- gitflow_analytics/cli_rich.py +0 -503
- {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/top_level.txt +0 -0
|
@@ -13,6 +13,7 @@ DESIGN DECISIONS:
|
|
|
13
13
|
- Testable: Can be globally disabled for testing, with event capture capability
|
|
14
14
|
- Nested support: Handles nested progress contexts with proper positioning
|
|
15
15
|
- Consistent styling: All progress bars follow the same formatting rules
|
|
16
|
+
- Rich integration: Optional Rich library support for enhanced terminal UI
|
|
16
17
|
|
|
17
18
|
USAGE:
|
|
18
19
|
from gitflow_analytics.core.progress import get_progress_service
|
|
@@ -30,10 +31,24 @@ import sys
|
|
|
30
31
|
import threading
|
|
31
32
|
from contextlib import contextmanager
|
|
32
33
|
from dataclasses import dataclass
|
|
33
|
-
from typing import Any, Optional
|
|
34
|
+
from typing import Any, Dict, Optional
|
|
34
35
|
|
|
35
36
|
from tqdm import tqdm
|
|
36
37
|
|
|
38
|
+
# Import UI components if available
|
|
39
|
+
try:
|
|
40
|
+
from ..ui.progress_display import (
|
|
41
|
+
RICH_AVAILABLE,
|
|
42
|
+
RepositoryInfo,
|
|
43
|
+
RepositoryStatus,
|
|
44
|
+
create_progress_display,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
UI_AVAILABLE = True
|
|
48
|
+
except ImportError:
|
|
49
|
+
UI_AVAILABLE = False
|
|
50
|
+
RICH_AVAILABLE = False
|
|
51
|
+
|
|
37
52
|
|
|
38
53
|
@dataclass
|
|
39
54
|
class ProgressContext:
|
|
@@ -73,38 +88,76 @@ class ProgressService:
|
|
|
73
88
|
|
|
74
89
|
This service provides a unified interface for creating and managing progress bars
|
|
75
90
|
throughout the application. It supports nested progress contexts, global disable
|
|
76
|
-
for testing,
|
|
91
|
+
for testing, event capture for verification, and optional Rich terminal UI.
|
|
77
92
|
"""
|
|
78
93
|
|
|
79
|
-
def __init__(self):
|
|
80
|
-
"""Initialize the progress service.
|
|
94
|
+
def __init__(self, display_style: str = "auto", version: str = "1.3.11"):
|
|
95
|
+
"""Initialize the progress service.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
display_style: Display style ("rich", "simple", or "auto")
|
|
99
|
+
version: Version string for display
|
|
100
|
+
"""
|
|
81
101
|
self._enabled = True
|
|
82
102
|
self._lock = threading.Lock()
|
|
83
103
|
self._active_contexts: list[ProgressContext] = []
|
|
84
104
|
self._position_counter = 0
|
|
85
105
|
self._capture_events = False
|
|
86
106
|
self._captured_events: list[ProgressEvent] = []
|
|
107
|
+
self._display_style = display_style
|
|
108
|
+
self._version = version
|
|
109
|
+
|
|
110
|
+
# Rich display components
|
|
111
|
+
self._rich_display = None
|
|
112
|
+
self._repository_contexts: Dict[str, Any] = {}
|
|
113
|
+
self._use_rich = False
|
|
114
|
+
|
|
115
|
+
# Initialize display based on configuration
|
|
116
|
+
self._init_display()
|
|
87
117
|
|
|
88
118
|
# Check environment for testing mode
|
|
119
|
+
# Note: If user explicitly requested rich mode, don't disable it
|
|
89
120
|
self._check_testing_environment()
|
|
90
121
|
|
|
122
|
+
def _init_display(self):
|
|
123
|
+
"""Initialize the appropriate display based on configuration."""
|
|
124
|
+
if UI_AVAILABLE and self._display_style in ("rich", "auto"):
|
|
125
|
+
try:
|
|
126
|
+
self._rich_display = create_progress_display(
|
|
127
|
+
style=self._display_style, version=self._version, update_frequency=0.5
|
|
128
|
+
)
|
|
129
|
+
self._use_rich = self._display_style == "rich" or (
|
|
130
|
+
self._display_style == "auto" and RICH_AVAILABLE
|
|
131
|
+
)
|
|
132
|
+
except Exception:
|
|
133
|
+
# Fall back to tqdm if Rich fails
|
|
134
|
+
self._use_rich = False
|
|
135
|
+
self._rich_display = None
|
|
136
|
+
|
|
91
137
|
def _check_testing_environment(self):
|
|
92
138
|
"""Check if running in a testing environment and disable if needed.
|
|
93
139
|
|
|
94
140
|
WHY: Progress bars interfere with test output and can cause issues in CI/CD.
|
|
95
141
|
This automatically detects common testing scenarios and disables progress.
|
|
96
142
|
"""
|
|
143
|
+
# Don't auto-disable if user explicitly requested rich mode
|
|
144
|
+
explicit_rich = self._display_style == "rich"
|
|
145
|
+
|
|
97
146
|
# Disable in pytest
|
|
98
147
|
if "pytest" in sys.modules:
|
|
99
148
|
self._enabled = False
|
|
149
|
+
self._use_rich = False
|
|
100
150
|
|
|
101
151
|
# Disable if explicitly requested via environment
|
|
102
152
|
if os.environ.get("GITFLOW_DISABLE_PROGRESS", "").lower() in ("1", "true", "yes"):
|
|
103
153
|
self._enabled = False
|
|
154
|
+
self._use_rich = False
|
|
104
155
|
|
|
105
156
|
# Disable if not in a TTY (e.g., CI/CD, piped output)
|
|
106
|
-
if
|
|
157
|
+
# BUT: Keep enabled if user explicitly requested rich mode
|
|
158
|
+
if not sys.stdout.isatty() and not explicit_rich:
|
|
107
159
|
self._enabled = False
|
|
160
|
+
self._use_rich = False
|
|
108
161
|
|
|
109
162
|
def create_progress(
|
|
110
163
|
self,
|
|
@@ -154,16 +207,21 @@ class ProgressService:
|
|
|
154
207
|
|
|
155
208
|
# Create actual progress bar if enabled
|
|
156
209
|
if self._enabled:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
210
|
+
if self._use_rich and self._rich_display:
|
|
211
|
+
# For Rich display, we don't create individual tqdm bars
|
|
212
|
+
# Instead, we'll manage everything through the Rich display
|
|
213
|
+
context.progress_bar = None
|
|
214
|
+
else:
|
|
215
|
+
context.progress_bar = tqdm(
|
|
216
|
+
total=total,
|
|
217
|
+
desc=description,
|
|
218
|
+
unit=unit,
|
|
219
|
+
position=position,
|
|
220
|
+
leave=leave,
|
|
221
|
+
# Consistent styling
|
|
222
|
+
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]",
|
|
223
|
+
dynamic_ncols=True,
|
|
224
|
+
)
|
|
167
225
|
|
|
168
226
|
self._active_contexts.append(context)
|
|
169
227
|
return context
|
|
@@ -196,7 +254,18 @@ class ProgressService:
|
|
|
196
254
|
)
|
|
197
255
|
|
|
198
256
|
# Update actual progress bar if it exists
|
|
199
|
-
if
|
|
257
|
+
if self._use_rich and self._rich_display:
|
|
258
|
+
# Update Rich display based on context type
|
|
259
|
+
if hasattr(context, "repository_name"):
|
|
260
|
+
# Repository-specific progress
|
|
261
|
+
speed = increment / 0.1 if increment > 0 else 0 # Simple speed calculation
|
|
262
|
+
self._rich_display.update_repository(
|
|
263
|
+
context.repository_name, context.current, speed
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
# Overall progress
|
|
267
|
+
self._rich_display.update_overall(context.current, description)
|
|
268
|
+
elif context.progress_bar:
|
|
200
269
|
context.progress_bar.update(increment)
|
|
201
270
|
if description:
|
|
202
271
|
context.progress_bar.set_description(description)
|
|
@@ -334,26 +403,158 @@ class ProgressService:
|
|
|
334
403
|
with self._lock:
|
|
335
404
|
self._captured_events = []
|
|
336
405
|
|
|
406
|
+
# Rich-specific methods
|
|
407
|
+
def start_rich_display(
|
|
408
|
+
self, total_items: int = 100, description: str = "Analyzing repositories"
|
|
409
|
+
):
|
|
410
|
+
"""Start the Rich display if available.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
total_items: Total number of items to process
|
|
414
|
+
description: Description of the overall task
|
|
415
|
+
"""
|
|
416
|
+
if self._use_rich and self._rich_display and self._enabled:
|
|
417
|
+
self._rich_display.start(total_items, description)
|
|
418
|
+
|
|
419
|
+
def stop_rich_display(self):
|
|
420
|
+
"""Stop the Rich display if active."""
|
|
421
|
+
if self._use_rich and self._rich_display:
|
|
422
|
+
self._rich_display.stop()
|
|
423
|
+
|
|
424
|
+
def start_repository(self, repo_name: str, total_commits: int = 0):
|
|
425
|
+
"""Start processing a repository with Rich display.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
repo_name: Name of the repository
|
|
429
|
+
total_commits: Total number of commits to process
|
|
430
|
+
"""
|
|
431
|
+
if self._use_rich and self._rich_display and self._enabled:
|
|
432
|
+
self._rich_display.start_repository(repo_name, total_commits)
|
|
433
|
+
|
|
434
|
+
def finish_repository(
|
|
435
|
+
self, repo_name: str, success: bool = True, error_message: Optional[str] = None
|
|
436
|
+
):
|
|
437
|
+
"""Finish processing a repository with Rich display.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
repo_name: Name of the repository
|
|
441
|
+
success: Whether processing was successful
|
|
442
|
+
error_message: Error message if processing failed
|
|
443
|
+
"""
|
|
444
|
+
if self._use_rich and self._rich_display and self._enabled:
|
|
445
|
+
self._rich_display.finish_repository(repo_name, success, error_message)
|
|
446
|
+
|
|
447
|
+
def update_statistics(self, **kwargs):
|
|
448
|
+
"""Update Rich display statistics.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
**kwargs: Statistics to update (total_commits, total_developers, etc.)
|
|
452
|
+
"""
|
|
453
|
+
if self._use_rich and self._rich_display and self._enabled:
|
|
454
|
+
self._rich_display.update_statistics(**kwargs)
|
|
455
|
+
|
|
456
|
+
def initialize_repositories(self, repository_list: list):
|
|
457
|
+
"""Initialize all repositories with pending status in Rich display.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
repository_list: List of repositories to be processed.
|
|
461
|
+
Each item should have 'name' and optionally 'path' fields.
|
|
462
|
+
"""
|
|
463
|
+
if self._use_rich and self._rich_display and self._enabled:
|
|
464
|
+
self._rich_display.initialize_repositories(repository_list)
|
|
465
|
+
|
|
466
|
+
def set_phase(self, phase: str):
|
|
467
|
+
"""Set the current processing phase for Rich display.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
phase: Description of the current phase
|
|
471
|
+
"""
|
|
472
|
+
if self._use_rich and self._rich_display and self._enabled:
|
|
473
|
+
self._rich_display.set_phase(phase)
|
|
474
|
+
|
|
475
|
+
def create_repository_progress(
|
|
476
|
+
self, repo_name: str, total: int, description: str
|
|
477
|
+
) -> ProgressContext:
|
|
478
|
+
"""Create a progress context specifically for repository processing.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
repo_name: Name of the repository
|
|
482
|
+
total: Total number of items to process
|
|
483
|
+
description: Description of the task
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
ProgressContext with repository information
|
|
487
|
+
"""
|
|
488
|
+
context = self.create_progress(total, description, unit="commits", nested=True)
|
|
489
|
+
# Add repository name to context for Rich display handling
|
|
490
|
+
# Note: We use object.__setattr__ to bypass dataclass frozen status if needed
|
|
491
|
+
object.__setattr__(context, "repository_name", repo_name)
|
|
492
|
+
|
|
493
|
+
if self._use_rich and self._rich_display and self._enabled:
|
|
494
|
+
self.start_repository(repo_name, total)
|
|
495
|
+
|
|
496
|
+
return context
|
|
497
|
+
|
|
337
498
|
|
|
338
499
|
# Global singleton instance
|
|
339
500
|
_progress_service: Optional[ProgressService] = None
|
|
340
501
|
_service_lock = threading.Lock()
|
|
341
502
|
|
|
342
503
|
|
|
343
|
-
def get_progress_service(
|
|
504
|
+
def get_progress_service(
|
|
505
|
+
display_style: Optional[str] = None, version: Optional[str] = None
|
|
506
|
+
) -> ProgressService:
|
|
344
507
|
"""Get the global progress service instance.
|
|
345
508
|
|
|
509
|
+
Args:
|
|
510
|
+
display_style: Optional display style override ("rich", "simple", or "auto")
|
|
511
|
+
version: Optional version string for display
|
|
512
|
+
|
|
346
513
|
Returns:
|
|
347
514
|
The singleton ProgressService instance
|
|
348
515
|
|
|
349
516
|
Thread-safe singleton pattern ensures only one progress service exists.
|
|
517
|
+
If display_style is provided and differs from current style, the service is reconfigured.
|
|
350
518
|
"""
|
|
351
519
|
global _progress_service
|
|
352
520
|
|
|
521
|
+
# Check if we need to reconfigure an existing service
|
|
522
|
+
if _progress_service is not None and display_style is not None:
|
|
523
|
+
with _service_lock:
|
|
524
|
+
# If display style changed, reconfigure the service
|
|
525
|
+
if _progress_service._display_style != display_style:
|
|
526
|
+
# Close any active displays
|
|
527
|
+
if _progress_service._use_rich and _progress_service._rich_display:
|
|
528
|
+
_progress_service.stop_rich_display()
|
|
529
|
+
|
|
530
|
+
# Reconfigure with new display style
|
|
531
|
+
_progress_service._display_style = display_style
|
|
532
|
+
_progress_service._use_rich = False
|
|
533
|
+
_progress_service._rich_display = None
|
|
534
|
+
|
|
535
|
+
# Re-enable if user explicitly requested rich mode
|
|
536
|
+
if display_style == "rich":
|
|
537
|
+
_progress_service._enabled = True
|
|
538
|
+
|
|
539
|
+
# Reinitialize display
|
|
540
|
+
_progress_service._init_display()
|
|
541
|
+
|
|
353
542
|
if _progress_service is None:
|
|
354
543
|
with _service_lock:
|
|
355
544
|
if _progress_service is None:
|
|
356
|
-
|
|
545
|
+
# Get display style from environment or use default
|
|
546
|
+
if display_style is None:
|
|
547
|
+
display_style = os.environ.get("GITFLOW_PROGRESS_STYLE", "auto")
|
|
548
|
+
if version is None:
|
|
549
|
+
# Try to get version from package
|
|
550
|
+
try:
|
|
551
|
+
from .._version import __version__
|
|
552
|
+
|
|
553
|
+
version = __version__
|
|
554
|
+
except ImportError:
|
|
555
|
+
version = "1.3.11"
|
|
556
|
+
|
|
557
|
+
_progress_service = ProgressService(display_style=display_style, version=version)
|
|
357
558
|
|
|
358
559
|
return _progress_service
|
|
359
560
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Subprocess-based Git operations to avoid authentication prompts."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SubprocessGit:
|
|
13
|
+
"""Git operations using subprocess to avoid GitPython authentication issues."""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def get_commits_in_range(
|
|
17
|
+
repo_path: Path, start_date: datetime, end_date: datetime, branch: str = "HEAD"
|
|
18
|
+
) -> list[dict[str, Any]]:
|
|
19
|
+
"""Get commits in date range using git log subprocess.
|
|
20
|
+
|
|
21
|
+
This avoids GitPython's potential authentication triggers by using
|
|
22
|
+
subprocess directly with environment variables that prevent prompts.
|
|
23
|
+
"""
|
|
24
|
+
# Format dates for git log
|
|
25
|
+
start_str = start_date.strftime("%Y-%m-%d")
|
|
26
|
+
end_str = end_date.strftime("%Y-%m-%d")
|
|
27
|
+
|
|
28
|
+
# Build git log command with JSON-like format for easy parsing
|
|
29
|
+
cmd = [
|
|
30
|
+
"git",
|
|
31
|
+
"log",
|
|
32
|
+
f"--since={start_str}",
|
|
33
|
+
f"--until={end_str}",
|
|
34
|
+
"--all", # All branches
|
|
35
|
+
"--no-merges", # Skip merge commits
|
|
36
|
+
"--format=%H|%ae|%an|%at|%s", # hash|email|name|timestamp|subject
|
|
37
|
+
"--numstat", # Include file changes
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# Set environment to prevent any authentication prompts
|
|
41
|
+
env = {
|
|
42
|
+
"GIT_TERMINAL_PROMPT": "0",
|
|
43
|
+
"GIT_ASKPASS": "echo",
|
|
44
|
+
"SSH_ASKPASS": "echo",
|
|
45
|
+
"GCM_INTERACTIVE": "never",
|
|
46
|
+
"DISPLAY": "",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
result = subprocess.run(
|
|
51
|
+
cmd, cwd=repo_path, capture_output=True, text=True, env=env, timeout=30
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if result.returncode != 0:
|
|
55
|
+
logger.warning(f"Git log failed: {result.stderr}")
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
return SubprocessGit._parse_git_log(result.stdout)
|
|
59
|
+
|
|
60
|
+
except subprocess.TimeoutExpired:
|
|
61
|
+
logger.error("Git log timed out - likely authentication issue")
|
|
62
|
+
return []
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"Error running git log: {e}")
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _parse_git_log(output: str) -> list[dict[str, Any]]:
|
|
69
|
+
"""Parse git log output into commit dictionaries."""
|
|
70
|
+
commits = []
|
|
71
|
+
current_commit = None
|
|
72
|
+
|
|
73
|
+
for line in output.split("\n"):
|
|
74
|
+
if not line:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
if "|" in line and not line[0].isdigit():
|
|
78
|
+
# This is a commit line
|
|
79
|
+
if current_commit:
|
|
80
|
+
commits.append(current_commit)
|
|
81
|
+
|
|
82
|
+
parts = line.split("|")
|
|
83
|
+
if len(parts) >= 5:
|
|
84
|
+
current_commit = {
|
|
85
|
+
"hash": parts[0],
|
|
86
|
+
"author_email": parts[1],
|
|
87
|
+
"author_name": parts[2],
|
|
88
|
+
"timestamp": int(parts[3]),
|
|
89
|
+
"message": "|".join(parts[4:]), # Message might contain |
|
|
90
|
+
"files": [],
|
|
91
|
+
}
|
|
92
|
+
elif current_commit and line[0].isdigit():
|
|
93
|
+
# This is a numstat line (additions deletions filename)
|
|
94
|
+
parts = line.split("\t")
|
|
95
|
+
if len(parts) >= 3:
|
|
96
|
+
current_commit["files"].append(
|
|
97
|
+
{"additions": parts[0], "deletions": parts[1], "filename": parts[2]}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Don't forget the last commit
|
|
101
|
+
if current_commit:
|
|
102
|
+
commits.append(current_commit)
|
|
103
|
+
|
|
104
|
+
return commits
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def check_remotes_safe(repo_path: Path) -> bool:
|
|
108
|
+
"""Check if repository has remotes without triggering authentication."""
|
|
109
|
+
cmd = ["git", "remote", "-v"]
|
|
110
|
+
|
|
111
|
+
env = {
|
|
112
|
+
"GIT_TERMINAL_PROMPT": "0",
|
|
113
|
+
"GIT_ASKPASS": "echo",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
result = subprocess.run(
|
|
118
|
+
cmd, cwd=repo_path, capture_output=True, text=True, env=env, timeout=5
|
|
119
|
+
)
|
|
120
|
+
return bool(result.stdout.strip())
|
|
121
|
+
except:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def get_branches_safe(repo_path: Path) -> list[str]:
|
|
126
|
+
"""Get list of branches without triggering authentication."""
|
|
127
|
+
branches = []
|
|
128
|
+
|
|
129
|
+
# Get local branches
|
|
130
|
+
cmd = ["git", "branch", "--format=%(refname:short)"]
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True, timeout=5)
|
|
134
|
+
|
|
135
|
+
if result.returncode == 0:
|
|
136
|
+
branches = [b.strip() for b in result.stdout.split("\n") if b.strip()]
|
|
137
|
+
|
|
138
|
+
except:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
# Default to common branch names if none found
|
|
142
|
+
if not branches:
|
|
143
|
+
branches = ["main", "master", "develop"]
|
|
144
|
+
|
|
145
|
+
return branches
|
|
@@ -673,7 +673,7 @@ class MLTicketExtractor(TicketExtractor):
|
|
|
673
673
|
return mapping.get(ml_category, "other")
|
|
674
674
|
|
|
675
675
|
def analyze_ticket_coverage(
|
|
676
|
-
self, commits: list[dict[str, Any]], prs: list[dict[str, Any]]
|
|
676
|
+
self, commits: list[dict[str, Any]], prs: list[dict[str, Any]], progress_display=None
|
|
677
677
|
) -> dict[str, Any]:
|
|
678
678
|
"""Enhanced ticket coverage analysis with ML categorization insights.
|
|
679
679
|
|
|
@@ -683,12 +683,13 @@ class MLTicketExtractor(TicketExtractor):
|
|
|
683
683
|
Args:
|
|
684
684
|
commits: List of commit data
|
|
685
685
|
prs: List of PR data
|
|
686
|
+
progress_display: Optional progress display for showing analysis progress
|
|
686
687
|
|
|
687
688
|
Returns:
|
|
688
689
|
Enhanced analysis results with ML insights
|
|
689
690
|
"""
|
|
690
691
|
# Get base analysis from parent
|
|
691
|
-
base_analysis = super().analyze_ticket_coverage(commits, prs)
|
|
692
|
+
base_analysis = super().analyze_ticket_coverage(commits, prs, progress_display)
|
|
692
693
|
|
|
693
694
|
if not self.enable_ml:
|
|
694
695
|
# Add indicator that ML was not used
|