gitflow-analytics 3.6.1__py3-none-any.whl → 3.7.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/__init__.py +8 -12
- gitflow_analytics/_version.py +1 -1
- gitflow_analytics/cli.py +156 -175
- gitflow_analytics/cli_wizards/install_wizard.py +5 -5
- gitflow_analytics/core/cache.py +3 -3
- gitflow_analytics/models/database.py +279 -45
- gitflow_analytics/security/reports/__init__.py +5 -0
- gitflow_analytics/security/reports/security_report.py +358 -0
- {gitflow_analytics-3.6.1.dist-info → gitflow_analytics-3.7.0.dist-info}/METADATA +2 -4
- {gitflow_analytics-3.6.1.dist-info → gitflow_analytics-3.7.0.dist-info}/RECORD +14 -25
- gitflow_analytics/tui/__init__.py +0 -5
- gitflow_analytics/tui/app.py +0 -726
- gitflow_analytics/tui/progress_adapter.py +0 -313
- gitflow_analytics/tui/screens/__init__.py +0 -8
- gitflow_analytics/tui/screens/analysis_progress_screen.py +0 -857
- gitflow_analytics/tui/screens/configuration_screen.py +0 -523
- gitflow_analytics/tui/screens/loading_screen.py +0 -348
- gitflow_analytics/tui/screens/main_screen.py +0 -321
- gitflow_analytics/tui/screens/results_screen.py +0 -735
- gitflow_analytics/tui/widgets/__init__.py +0 -7
- gitflow_analytics/tui/widgets/data_table.py +0 -255
- gitflow_analytics/tui/widgets/export_modal.py +0 -301
- gitflow_analytics/tui/widgets/progress_widget.py +0 -187
- {gitflow_analytics-3.6.1.dist-info → gitflow_analytics-3.7.0.dist-info}/WHEEL +0 -0
- {gitflow_analytics-3.6.1.dist-info → gitflow_analytics-3.7.0.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-3.6.1.dist-info → gitflow_analytics-3.7.0.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-3.6.1.dist-info → gitflow_analytics-3.7.0.dist-info}/top_level.txt +0 -0
gitflow_analytics/__init__.py
CHANGED
|
@@ -5,20 +5,16 @@ from ._version import __version__, __version_info__
|
|
|
5
5
|
__author__ = "Bob Matyas"
|
|
6
6
|
__email__ = "bobmatnyc@gmail.com"
|
|
7
7
|
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
from .core.
|
|
11
|
-
from .
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
8
|
+
# Heavy imports removed from package __init__ for CLI performance
|
|
9
|
+
# Import these directly when needed in your code:
|
|
10
|
+
# from gitflow_analytics.core.analyzer import GitAnalyzer
|
|
11
|
+
# from gitflow_analytics.core.cache import GitAnalysisCache
|
|
12
|
+
# from gitflow_analytics.core.identity import DeveloperIdentityResolver
|
|
13
|
+
# from gitflow_analytics.extractors.story_points import StoryPointExtractor
|
|
14
|
+
# from gitflow_analytics.extractors.tickets import TicketExtractor
|
|
15
|
+
# from gitflow_analytics.reports.csv_writer import CSVReportGenerator
|
|
14
16
|
|
|
15
17
|
__all__ = [
|
|
16
18
|
"__version__",
|
|
17
19
|
"__version_info__",
|
|
18
|
-
"GitAnalyzer",
|
|
19
|
-
"GitAnalysisCache",
|
|
20
|
-
"DeveloperIdentityResolver",
|
|
21
|
-
"StoryPointExtractor",
|
|
22
|
-
"TicketExtractor",
|
|
23
|
-
"CSVReportGenerator",
|
|
24
20
|
]
|
gitflow_analytics/_version.py
CHANGED
gitflow_analytics/cli.py
CHANGED
|
@@ -14,25 +14,28 @@ from typing import Any, Optional, cast
|
|
|
14
14
|
|
|
15
15
|
import click
|
|
16
16
|
import git
|
|
17
|
-
import pandas as pd
|
|
18
17
|
import yaml
|
|
19
18
|
|
|
20
19
|
from ._version import __version__
|
|
21
20
|
from .config import ConfigLoader
|
|
22
|
-
from .core.analyzer import GitAnalyzer
|
|
23
|
-
from .core.cache import GitAnalysisCache
|
|
24
|
-
from .core.git_auth import preflight_git_authentication
|
|
25
|
-
from .core.identity import DeveloperIdentityResolver
|
|
26
|
-
from .integrations.orchestrator import IntegrationOrchestrator
|
|
27
|
-
from .metrics.dora import DORAMetricsCalculator
|
|
28
|
-
from .reports.analytics_writer import AnalyticsReportGenerator
|
|
29
|
-
from .reports.csv_writer import CSVReportGenerator
|
|
30
|
-
from .reports.json_exporter import ComprehensiveJSONExporter
|
|
31
|
-
from .reports.narrative_writer import NarrativeReportGenerator
|
|
32
|
-
from .reports.weekly_trends_writer import WeeklyTrendsWriter
|
|
33
|
-
from .training.pipeline import CommitClassificationTrainer
|
|
34
21
|
from .ui.progress_display import create_progress_display
|
|
35
22
|
|
|
23
|
+
# Heavy imports are lazy-loaded to improve CLI startup time
|
|
24
|
+
# These imports add 1-2 seconds to startup but are only needed for actual analysis
|
|
25
|
+
# from .core.analyzer import GitAnalyzer
|
|
26
|
+
# from .core.cache import GitAnalysisCache
|
|
27
|
+
# from .core.git_auth import preflight_git_authentication
|
|
28
|
+
# from .core.identity import DeveloperIdentityResolver
|
|
29
|
+
# from .integrations.orchestrator import IntegrationOrchestrator
|
|
30
|
+
# from .metrics.dora import DORAMetricsCalculator
|
|
31
|
+
# from .reports.analytics_writer import AnalyticsReportGenerator
|
|
32
|
+
# from .reports.csv_writer import CSVReportGenerator
|
|
33
|
+
# from .reports.json_exporter import ComprehensiveJSONExporter
|
|
34
|
+
# from .reports.narrative_writer import NarrativeReportGenerator
|
|
35
|
+
# from .reports.weekly_trends_writer import WeeklyTrendsWriter
|
|
36
|
+
# from .training.pipeline import CommitClassificationTrainer
|
|
37
|
+
# import pandas as pd # Only used in one location, lazy loaded
|
|
38
|
+
|
|
36
39
|
logger = logging.getLogger(__name__)
|
|
37
40
|
|
|
38
41
|
|
|
@@ -257,64 +260,33 @@ class ImprovedErrorHandler:
|
|
|
257
260
|
click.echo("\nFor help: gitflow-analytics help", err=True)
|
|
258
261
|
|
|
259
262
|
|
|
260
|
-
class
|
|
263
|
+
class AnalyzeAsDefaultGroup(click.Group):
|
|
261
264
|
"""
|
|
262
|
-
Custom Click group that defaults to
|
|
263
|
-
This allows 'gitflow-analytics -c config.yaml' to
|
|
264
|
-
For explicit CLI analysis, use 'gitflow-analytics analyze -c config.yaml'
|
|
265
|
+
Custom Click group that defaults to analyze when no explicit subcommand is provided.
|
|
266
|
+
This allows 'gitflow-analytics -c config.yaml' to run analysis by default.
|
|
265
267
|
"""
|
|
266
268
|
|
|
267
269
|
def parse_args(self, ctx, args):
|
|
268
|
-
"""Override parse_args to default to
|
|
270
|
+
"""Override parse_args to default to analyze unless explicit subcommand provided."""
|
|
269
271
|
# Check if the first argument is a known subcommand
|
|
270
272
|
if args and args[0] in self.list_commands(ctx):
|
|
271
273
|
return super().parse_args(ctx, args)
|
|
272
274
|
|
|
273
|
-
# Check for global options that should NOT be routed to
|
|
275
|
+
# Check for global options that should NOT be routed to analyze
|
|
274
276
|
global_options = {"--version", "--help", "-h"}
|
|
275
277
|
if args and args[0] in global_options:
|
|
276
278
|
return super().parse_args(ctx, args)
|
|
277
279
|
|
|
278
|
-
#
|
|
279
|
-
|
|
280
|
-
"--no-rich",
|
|
281
|
-
"--generate-csv",
|
|
282
|
-
"--validate-only",
|
|
283
|
-
"--warm-cache",
|
|
284
|
-
"--validate-cache",
|
|
285
|
-
"--qualitative-only",
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
# If user explicitly requests CLI-only features, route to analyze
|
|
289
|
-
if args and any(arg in cli_only_indicators for arg in args):
|
|
280
|
+
# For all other cases (including -c config.yaml), default to analyze
|
|
281
|
+
if args and args[0].startswith("-"):
|
|
290
282
|
new_args = ["analyze"] + args
|
|
291
283
|
return super().parse_args(ctx, new_args)
|
|
292
284
|
|
|
293
|
-
# For all other cases (including -c config.yaml), default to TUI
|
|
294
|
-
if args and args[0].startswith("-"):
|
|
295
|
-
# Check if TUI dependencies are available
|
|
296
|
-
try:
|
|
297
|
-
import importlib.util
|
|
298
|
-
|
|
299
|
-
textual_spec = importlib.util.find_spec("textual")
|
|
300
|
-
# TUI is available - route to TUI
|
|
301
|
-
if textual_spec is not None:
|
|
302
|
-
new_args = ["tui"] + args
|
|
303
|
-
return super().parse_args(ctx, new_args)
|
|
304
|
-
else:
|
|
305
|
-
# TUI not available - fallback to analyze
|
|
306
|
-
new_args = ["analyze"] + args
|
|
307
|
-
return super().parse_args(ctx, new_args)
|
|
308
|
-
except (ImportError, ValueError):
|
|
309
|
-
# TUI not available - fallback to analyze
|
|
310
|
-
new_args = ["analyze"] + args
|
|
311
|
-
return super().parse_args(ctx, new_args)
|
|
312
|
-
|
|
313
285
|
# Otherwise, use default behavior
|
|
314
286
|
return super().parse_args(ctx, args)
|
|
315
287
|
|
|
316
288
|
|
|
317
|
-
@click.group(cls=
|
|
289
|
+
@click.group(cls=AnalyzeAsDefaultGroup, invoke_without_command=True)
|
|
318
290
|
@click.version_option(version=__version__, prog_name="GitFlow Analytics")
|
|
319
291
|
@click.help_option("-h", "--help")
|
|
320
292
|
@click.pass_context
|
|
@@ -374,119 +346,6 @@ def cli(ctx: click.Context) -> None:
|
|
|
374
346
|
ctx.exit(0)
|
|
375
347
|
|
|
376
348
|
|
|
377
|
-
# TUI command - Terminal User Interface
|
|
378
|
-
@cli.command(name="tui")
|
|
379
|
-
@click.option(
|
|
380
|
-
"--config",
|
|
381
|
-
"-c",
|
|
382
|
-
type=click.Path(exists=True, path_type=Path),
|
|
383
|
-
default=None,
|
|
384
|
-
help="Path to YAML configuration file (optional - can be loaded in TUI)",
|
|
385
|
-
)
|
|
386
|
-
@click.option(
|
|
387
|
-
"--weeks",
|
|
388
|
-
"-w",
|
|
389
|
-
type=int,
|
|
390
|
-
default=None,
|
|
391
|
-
help="Number of weeks to analyze (passed to TUI)",
|
|
392
|
-
)
|
|
393
|
-
@click.option(
|
|
394
|
-
"--clear-cache",
|
|
395
|
-
is_flag=True,
|
|
396
|
-
help="Clear cache before analysis (passed to TUI)",
|
|
397
|
-
)
|
|
398
|
-
@click.option(
|
|
399
|
-
"--output",
|
|
400
|
-
"-o",
|
|
401
|
-
type=click.Path(path_type=Path),
|
|
402
|
-
default=None,
|
|
403
|
-
help="Output directory for reports (passed to TUI)",
|
|
404
|
-
)
|
|
405
|
-
def tui_command(
|
|
406
|
-
config: Optional[Path],
|
|
407
|
-
weeks: Optional[int],
|
|
408
|
-
clear_cache: bool,
|
|
409
|
-
output: Optional[Path],
|
|
410
|
-
) -> None:
|
|
411
|
-
"""Launch the Terminal User Interface for GitFlow Analytics.
|
|
412
|
-
|
|
413
|
-
\b
|
|
414
|
-
The TUI provides an interactive interface for:
|
|
415
|
-
- Loading and editing configuration files
|
|
416
|
-
- Running analysis with real-time progress updates
|
|
417
|
-
- Viewing results in an organized, navigable format
|
|
418
|
-
- Exporting reports in various formats
|
|
419
|
-
|
|
420
|
-
\b
|
|
421
|
-
FEATURES:
|
|
422
|
-
• Full-screen terminal interface with keyboard navigation
|
|
423
|
-
• Real-time progress tracking during analysis
|
|
424
|
-
• Interactive configuration management
|
|
425
|
-
• Results browser with filtering and export options
|
|
426
|
-
• Built-in help system and keyboard shortcuts
|
|
427
|
-
|
|
428
|
-
\b
|
|
429
|
-
EXAMPLES:
|
|
430
|
-
# Launch TUI without pre-loading configuration
|
|
431
|
-
gitflow-analytics tui
|
|
432
|
-
|
|
433
|
-
# Launch TUI with a specific configuration file
|
|
434
|
-
gitflow-analytics tui -c config.yaml
|
|
435
|
-
|
|
436
|
-
\b
|
|
437
|
-
KEYBOARD SHORTCUTS:
|
|
438
|
-
• Ctrl+Q / Ctrl+C: Quit application
|
|
439
|
-
• F1: Show help
|
|
440
|
-
• Ctrl+D: Toggle dark/light mode
|
|
441
|
-
• Escape: Go back/cancel current action
|
|
442
|
-
"""
|
|
443
|
-
try:
|
|
444
|
-
# Import TUI components only when needed
|
|
445
|
-
from .tui.app import GitFlowAnalyticsApp
|
|
446
|
-
|
|
447
|
-
# Create and run the TUI application
|
|
448
|
-
app = GitFlowAnalyticsApp()
|
|
449
|
-
|
|
450
|
-
# Pass CLI parameters to TUI
|
|
451
|
-
if weeks is not None:
|
|
452
|
-
app.default_weeks = weeks
|
|
453
|
-
if clear_cache:
|
|
454
|
-
app.clear_cache_on_start = True
|
|
455
|
-
if output:
|
|
456
|
-
app.default_output_dir = output
|
|
457
|
-
|
|
458
|
-
# If config path provided, try to load it
|
|
459
|
-
if config:
|
|
460
|
-
try:
|
|
461
|
-
from .config import ConfigLoader
|
|
462
|
-
|
|
463
|
-
loaded_config = ConfigLoader.load(config)
|
|
464
|
-
app.config = loaded_config
|
|
465
|
-
app.config_path = config
|
|
466
|
-
app.initialization_complete = True
|
|
467
|
-
except Exception as e:
|
|
468
|
-
# Don't fail - let TUI handle config loading
|
|
469
|
-
click.echo(f"⚠️ Could not pre-load config: {e}")
|
|
470
|
-
click.echo(" You can load the configuration within the TUI.")
|
|
471
|
-
|
|
472
|
-
# Run the TUI
|
|
473
|
-
app.run()
|
|
474
|
-
|
|
475
|
-
except ImportError as e:
|
|
476
|
-
click.echo("❌ TUI dependencies not installed.", err=True)
|
|
477
|
-
click.echo(f" Error: {e}", err=True)
|
|
478
|
-
click.echo("\n💡 Install TUI dependencies:", err=True)
|
|
479
|
-
click.echo(" pip install 'gitflow-analytics[tui]'", err=True)
|
|
480
|
-
click.echo(" # or", err=True)
|
|
481
|
-
click.echo(" pip install textual>=0.41.0", err=True)
|
|
482
|
-
sys.exit(1)
|
|
483
|
-
except Exception as e:
|
|
484
|
-
click.echo(f"❌ Failed to launch TUI: {e}", err=True)
|
|
485
|
-
if "--debug" in sys.argv:
|
|
486
|
-
raise
|
|
487
|
-
sys.exit(1)
|
|
488
|
-
|
|
489
|
-
|
|
490
349
|
@cli.command(name="analyze")
|
|
491
350
|
@click.option(
|
|
492
351
|
"--config",
|
|
@@ -534,7 +393,7 @@ def tui_command(
|
|
|
534
393
|
"--no-rich",
|
|
535
394
|
is_flag=True,
|
|
536
395
|
default=True,
|
|
537
|
-
help="Disable rich terminal output (simple
|
|
396
|
+
help="Disable rich terminal output (use simple text progress instead)",
|
|
538
397
|
)
|
|
539
398
|
@click.option(
|
|
540
399
|
"--log",
|
|
@@ -697,8 +556,20 @@ def analyze(
|
|
|
697
556
|
) -> None:
|
|
698
557
|
"""Analyze Git repositories using configuration file."""
|
|
699
558
|
|
|
700
|
-
#
|
|
559
|
+
# Lazy imports: Only load heavy dependencies when actually running analysis
|
|
560
|
+
# This improves CLI startup time from ~2s to <100ms for commands like --help
|
|
561
|
+
from .core.analyzer import GitAnalyzer
|
|
562
|
+
from .core.cache import GitAnalysisCache
|
|
563
|
+
from .core.git_auth import preflight_git_authentication
|
|
564
|
+
from .core.identity import DeveloperIdentityResolver
|
|
701
565
|
from .core.progress import get_progress_service
|
|
566
|
+
from .integrations.orchestrator import IntegrationOrchestrator
|
|
567
|
+
from .metrics.dora import DORAMetricsCalculator
|
|
568
|
+
from .reports.analytics_writer import AnalyticsReportGenerator
|
|
569
|
+
from .reports.csv_writer import CSVReportGenerator
|
|
570
|
+
from .reports.json_exporter import ComprehensiveJSONExporter
|
|
571
|
+
from .reports.narrative_writer import NarrativeReportGenerator
|
|
572
|
+
from .reports.weekly_trends_writer import WeeklyTrendsWriter
|
|
702
573
|
|
|
703
574
|
try:
|
|
704
575
|
from ._version import __version__
|
|
@@ -710,7 +581,7 @@ def analyze(
|
|
|
710
581
|
# Initialize progress service with user's preference
|
|
711
582
|
progress = get_progress_service(display_style=progress_style, version=version)
|
|
712
583
|
|
|
713
|
-
# Initialize display - simple output by default
|
|
584
|
+
# Initialize display - simple output by default for better compatibility
|
|
714
585
|
# Create display - only create if rich output is explicitly enabled (--no-rich=False)
|
|
715
586
|
display = (
|
|
716
587
|
create_progress_display(style="simple" if no_rich else "rich", version=__version__)
|
|
@@ -1448,6 +1319,11 @@ def analyze(
|
|
|
1448
1319
|
else:
|
|
1449
1320
|
click.echo("🔄 Force fetch enabled - analyzing all repositories")
|
|
1450
1321
|
|
|
1322
|
+
# Initialize counters before fetching (used in validation even if skipped)
|
|
1323
|
+
total_commits = 0
|
|
1324
|
+
total_tickets = 0
|
|
1325
|
+
total_developers = set()
|
|
1326
|
+
|
|
1451
1327
|
# Step 1: Fetch data only for repos that need analysis
|
|
1452
1328
|
if repos_needing_analysis:
|
|
1453
1329
|
if display and display._live:
|
|
@@ -1552,11 +1428,6 @@ def analyze(
|
|
|
1552
1428
|
progress.initialize_repositories(all_repo_list)
|
|
1553
1429
|
progress.set_phase("Step 1: Data Fetching")
|
|
1554
1430
|
|
|
1555
|
-
# Fetch data for repositories that need analysis
|
|
1556
|
-
total_commits = 0
|
|
1557
|
-
total_tickets = 0
|
|
1558
|
-
total_developers = set() # Track unique developers
|
|
1559
|
-
|
|
1560
1431
|
# Create top-level progress for all repositories
|
|
1561
1432
|
with progress.progress(
|
|
1562
1433
|
total=len(repos_needing_analysis),
|
|
@@ -1886,6 +1757,102 @@ def analyze(
|
|
|
1886
1757
|
repo_path = Path(repo_config.path)
|
|
1887
1758
|
project_key = repo_config.project_key or repo_path.name
|
|
1888
1759
|
|
|
1760
|
+
# Check if repo exists, clone if needed (critical for organization mode)
|
|
1761
|
+
if not repo_path.exists():
|
|
1762
|
+
if repo_config.github_repo and cfg.github.organization:
|
|
1763
|
+
if display:
|
|
1764
|
+
display.print_status(
|
|
1765
|
+
f" 📥 Cloning {repo_config.github_repo} from GitHub...",
|
|
1766
|
+
"info",
|
|
1767
|
+
)
|
|
1768
|
+
else:
|
|
1769
|
+
click.echo(f" 📥 Cloning {repo_config.github_repo} from GitHub...")
|
|
1770
|
+
try:
|
|
1771
|
+
# Ensure parent directory exists
|
|
1772
|
+
repo_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1773
|
+
|
|
1774
|
+
# Build clone URL with authentication
|
|
1775
|
+
clone_url = f"https://github.com/{repo_config.github_repo}.git"
|
|
1776
|
+
if cfg.github.token:
|
|
1777
|
+
clone_url = f"https://{cfg.github.token}@github.com/{repo_config.github_repo}.git"
|
|
1778
|
+
|
|
1779
|
+
# Clone using subprocess for better control
|
|
1780
|
+
env = os.environ.copy()
|
|
1781
|
+
env["GIT_TERMINAL_PROMPT"] = "0"
|
|
1782
|
+
env["GIT_ASKPASS"] = ""
|
|
1783
|
+
env["GCM_INTERACTIVE"] = "never"
|
|
1784
|
+
|
|
1785
|
+
cmd = ["git", "clone", "--config", "credential.helper="]
|
|
1786
|
+
if repo_config.branch:
|
|
1787
|
+
cmd.extend(["-b", repo_config.branch])
|
|
1788
|
+
cmd.extend([clone_url, str(repo_path)])
|
|
1789
|
+
|
|
1790
|
+
result = subprocess.run(
|
|
1791
|
+
cmd,
|
|
1792
|
+
env=env,
|
|
1793
|
+
capture_output=True,
|
|
1794
|
+
text=True,
|
|
1795
|
+
timeout=30,
|
|
1796
|
+
)
|
|
1797
|
+
|
|
1798
|
+
if result.returncode != 0:
|
|
1799
|
+
error_msg = result.stderr or result.stdout
|
|
1800
|
+
if any(
|
|
1801
|
+
x in error_msg.lower()
|
|
1802
|
+
for x in ["authentication", "permission denied", "401", "403"]
|
|
1803
|
+
):
|
|
1804
|
+
if display:
|
|
1805
|
+
display.print_status(
|
|
1806
|
+
f" ❌ Authentication failed for {repo_config.github_repo}",
|
|
1807
|
+
"error",
|
|
1808
|
+
)
|
|
1809
|
+
else:
|
|
1810
|
+
click.echo(
|
|
1811
|
+
f" ❌ Authentication failed for {repo_config.github_repo}"
|
|
1812
|
+
)
|
|
1813
|
+
continue
|
|
1814
|
+
else:
|
|
1815
|
+
raise subprocess.CalledProcessError(
|
|
1816
|
+
result.returncode, cmd, result.stdout, result.stderr
|
|
1817
|
+
)
|
|
1818
|
+
|
|
1819
|
+
if display:
|
|
1820
|
+
display.print_status(
|
|
1821
|
+
f" ✅ Cloned {repo_config.github_repo}", "success"
|
|
1822
|
+
)
|
|
1823
|
+
else:
|
|
1824
|
+
click.echo(f" ✅ Cloned {repo_config.github_repo}")
|
|
1825
|
+
|
|
1826
|
+
except subprocess.TimeoutExpired:
|
|
1827
|
+
if display:
|
|
1828
|
+
display.print_status(
|
|
1829
|
+
f" ❌ Clone timeout for {repo_config.github_repo}",
|
|
1830
|
+
"error",
|
|
1831
|
+
)
|
|
1832
|
+
else:
|
|
1833
|
+
click.echo(f" ❌ Clone timeout for {repo_config.github_repo}")
|
|
1834
|
+
continue
|
|
1835
|
+
except Exception as e:
|
|
1836
|
+
if display:
|
|
1837
|
+
display.print_status(
|
|
1838
|
+
f" ❌ Failed to clone {repo_config.github_repo}: {e}",
|
|
1839
|
+
"error",
|
|
1840
|
+
)
|
|
1841
|
+
else:
|
|
1842
|
+
click.echo(
|
|
1843
|
+
f" ❌ Failed to clone {repo_config.github_repo}: {e}"
|
|
1844
|
+
)
|
|
1845
|
+
continue
|
|
1846
|
+
else:
|
|
1847
|
+
# No github_repo configured, can't clone
|
|
1848
|
+
if display:
|
|
1849
|
+
display.print_status(
|
|
1850
|
+
f" ❌ Repository not found: {repo_path}", "error"
|
|
1851
|
+
)
|
|
1852
|
+
else:
|
|
1853
|
+
click.echo(f" ❌ Repository not found: {repo_path}")
|
|
1854
|
+
continue
|
|
1855
|
+
|
|
1889
1856
|
# Progress callback for fetch
|
|
1890
1857
|
def progress_callback(message: str):
|
|
1891
1858
|
if display:
|
|
@@ -3567,6 +3534,9 @@ def analyze(
|
|
|
3567
3534
|
logger.debug("Starting narrative report generation")
|
|
3568
3535
|
narrative_gen = NarrativeReportGenerator()
|
|
3569
3536
|
|
|
3537
|
+
# Lazy import pandas - only needed for CSV reading in narrative generation
|
|
3538
|
+
import pandas as pd
|
|
3539
|
+
|
|
3570
3540
|
# Load activity distribution data
|
|
3571
3541
|
logger.debug("Loading activity distribution data")
|
|
3572
3542
|
activity_df = pd.read_csv(activity_report)
|
|
@@ -4111,7 +4081,7 @@ def analyze(
|
|
|
4111
4081
|
"--no-rich",
|
|
4112
4082
|
is_flag=True,
|
|
4113
4083
|
default=True,
|
|
4114
|
-
help="Disable rich terminal output (simple
|
|
4084
|
+
help="Disable rich terminal output (use simple text progress instead)",
|
|
4115
4085
|
)
|
|
4116
4086
|
def fetch(
|
|
4117
4087
|
config: Path,
|
|
@@ -4161,7 +4131,7 @@ def fetch(
|
|
|
4161
4131
|
- Use --clear-cache to force fresh fetch
|
|
4162
4132
|
"""
|
|
4163
4133
|
# Initialize display
|
|
4164
|
-
# Create display - simple output by default
|
|
4134
|
+
# Create display - simple output by default for better compatibility, rich only when explicitly enabled
|
|
4165
4135
|
display = (
|
|
4166
4136
|
create_progress_display(style="simple" if no_rich else "rich", version=__version__)
|
|
4167
4137
|
if not no_rich
|
|
@@ -4186,6 +4156,10 @@ def fetch(
|
|
|
4186
4156
|
logger = logging.getLogger(__name__)
|
|
4187
4157
|
|
|
4188
4158
|
try:
|
|
4159
|
+
# Lazy imports
|
|
4160
|
+
from .core.cache import GitAnalysisCache
|
|
4161
|
+
from .integrations.orchestrator import IntegrationOrchestrator
|
|
4162
|
+
|
|
4189
4163
|
if display:
|
|
4190
4164
|
display.show_header()
|
|
4191
4165
|
|
|
@@ -5604,6 +5578,9 @@ def train(
|
|
|
5604
5578
|
# Initialize trainer
|
|
5605
5579
|
click.echo("🧠 Initializing training pipeline...")
|
|
5606
5580
|
try:
|
|
5581
|
+
# Lazy import - only needed for train command
|
|
5582
|
+
from .training.pipeline import CommitClassificationTrainer
|
|
5583
|
+
|
|
5607
5584
|
trainer = CommitClassificationTrainer(
|
|
5608
5585
|
config=cfg, cache=cache, orchestrator=orchestrator, training_config=training_config
|
|
5609
5586
|
)
|
|
@@ -5939,6 +5916,10 @@ def training_statistics(config: Path) -> None:
|
|
|
5939
5916
|
- Compare different model versions
|
|
5940
5917
|
"""
|
|
5941
5918
|
try:
|
|
5919
|
+
# Lazy imports
|
|
5920
|
+
from .core.cache import GitAnalysisCache
|
|
5921
|
+
from .training.pipeline import CommitClassificationTrainer
|
|
5922
|
+
|
|
5942
5923
|
cfg = ConfigLoader.load(config)
|
|
5943
5924
|
cache = GitAnalysisCache(cfg.cache.directory)
|
|
5944
5925
|
|
|
@@ -117,7 +117,7 @@ class InstallWizard:
|
|
|
117
117
|
return getpass.getpass(prompt)
|
|
118
118
|
else:
|
|
119
119
|
click.echo(f"⚠️ Non-interactive mode detected - {field_name} will be visible", err=True)
|
|
120
|
-
return click.prompt(prompt, hide_input=False)
|
|
120
|
+
return click.prompt(prompt, hide_input=False).strip()
|
|
121
121
|
|
|
122
122
|
def _select_profile(self) -> dict:
|
|
123
123
|
"""Let user select installation profile."""
|
|
@@ -484,7 +484,7 @@ class InstallWizard:
|
|
|
484
484
|
if not click.confirm("Add anyway?", default=False):
|
|
485
485
|
continue
|
|
486
486
|
|
|
487
|
-
repo_name = click.prompt("Repository name", default=path_obj.name)
|
|
487
|
+
repo_name = click.prompt("Repository name", default=path_obj.name).strip()
|
|
488
488
|
|
|
489
489
|
repositories.append({"name": repo_name, "path": str(path_obj)})
|
|
490
490
|
click.echo(f"Added repository #{len(repositories)}\n")
|
|
@@ -850,7 +850,7 @@ class InstallWizard:
|
|
|
850
850
|
"Output directory for reports",
|
|
851
851
|
type=str,
|
|
852
852
|
default="./reports",
|
|
853
|
-
)
|
|
853
|
+
).strip()
|
|
854
854
|
output_path = self._validate_directory_path(output_dir, "Output directory")
|
|
855
855
|
if output_path is not None:
|
|
856
856
|
output_dir = str(output_path)
|
|
@@ -863,7 +863,7 @@ class InstallWizard:
|
|
|
863
863
|
"Cache directory",
|
|
864
864
|
type=str,
|
|
865
865
|
default="./.gitflow-cache",
|
|
866
|
-
)
|
|
866
|
+
).strip()
|
|
867
867
|
cache_path = self._validate_directory_path(cache_dir, "Cache directory")
|
|
868
868
|
if cache_path is not None:
|
|
869
869
|
cache_dir = str(cache_path)
|
|
@@ -928,7 +928,7 @@ class InstallWizard:
|
|
|
928
928
|
# Use existing file
|
|
929
929
|
aliases_path = click.prompt(
|
|
930
930
|
"Path to aliases.yaml (relative to config)", default="../shared/aliases.yaml"
|
|
931
|
-
)
|
|
931
|
+
).strip()
|
|
932
932
|
|
|
933
933
|
# Ensure analysis.identity section exists
|
|
934
934
|
if "identity" not in self.config_data.get("analysis", {}):
|
gitflow_analytics/core/cache.py
CHANGED
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
7
|
from contextlib import contextmanager
|
|
8
|
-
from datetime import datetime, timedelta
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any, Optional, Union
|
|
11
11
|
|
|
@@ -990,7 +990,7 @@ class GitAnalysisCache:
|
|
|
990
990
|
"complexity_delta": commit_data.get("complexity_delta", 0.0),
|
|
991
991
|
"story_points": commit_data.get("story_points"),
|
|
992
992
|
"ticket_references": commit_data.get("ticket_references", []),
|
|
993
|
-
"cached_at": datetime.
|
|
993
|
+
"cached_at": datetime.now(timezone.utc),
|
|
994
994
|
}
|
|
995
995
|
mappings.append(mapping)
|
|
996
996
|
|
|
@@ -1101,7 +1101,7 @@ class GitAnalysisCache:
|
|
|
1101
1101
|
"complexity_delta": commit_data.get("complexity_delta"),
|
|
1102
1102
|
"story_points": commit_data.get("story_points"),
|
|
1103
1103
|
"ticket_references": commit_data.get("ticket_references"),
|
|
1104
|
-
"cached_at": datetime.
|
|
1104
|
+
"cached_at": datetime.now(timezone.utc),
|
|
1105
1105
|
}
|
|
1106
1106
|
|
|
1107
1107
|
# Only include non-None values in update
|