gitflow-analytics 3.6.2__py3-none-any.whl → 3.7.4__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 +323 -203
- gitflow_analytics/cli_wizards/install_wizard.py +5 -5
- gitflow_analytics/config/repository.py +9 -1
- gitflow_analytics/config/schema.py +39 -0
- gitflow_analytics/identity_llm/analysis_pass.py +7 -2
- gitflow_analytics/models/database.py +229 -8
- {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.4.dist-info}/METADATA +2 -4
- {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.4.dist-info}/RECORD +14 -27
- 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.2.dist-info → gitflow_analytics-3.7.4.dist-info}/WHEEL +0 -0
- {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.4.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.4.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.4.dist-info}/top_level.txt +0 -0
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__)
|
|
@@ -1073,7 +944,7 @@ def analyze(
|
|
|
1073
944
|
data_fetcher = GitDataFetcher(
|
|
1074
945
|
cache=cache,
|
|
1075
946
|
branch_mapping_rules=cfg.analysis.branch_mapping_rules,
|
|
1076
|
-
allowed_ticket_platforms=cfg.
|
|
947
|
+
allowed_ticket_platforms=cfg.get_effective_ticket_platforms(),
|
|
1077
948
|
exclude_paths=cfg.analysis.exclude_paths,
|
|
1078
949
|
)
|
|
1079
950
|
|
|
@@ -1264,9 +1135,7 @@ def analyze(
|
|
|
1264
1135
|
analyzer = GitAnalyzer(
|
|
1265
1136
|
cache,
|
|
1266
1137
|
branch_mapping_rules=cfg.analysis.branch_mapping_rules,
|
|
1267
|
-
allowed_ticket_platforms=
|
|
1268
|
-
cfg.analysis, "ticket_platforms", ["jira", "github", "clickup", "linear"]
|
|
1269
|
-
),
|
|
1138
|
+
allowed_ticket_platforms=cfg.get_effective_ticket_platforms(),
|
|
1270
1139
|
exclude_paths=cfg.analysis.exclude_paths,
|
|
1271
1140
|
story_point_patterns=cfg.analysis.story_point_patterns,
|
|
1272
1141
|
ml_categorization_config=ml_config,
|
|
@@ -1293,9 +1162,28 @@ def analyze(
|
|
|
1293
1162
|
# Use a 'repos' directory in the config directory for cloned repositories
|
|
1294
1163
|
config_dir = Path(config).parent if config else Path.cwd()
|
|
1295
1164
|
repos_dir = config_dir / "repos"
|
|
1296
|
-
|
|
1165
|
+
|
|
1166
|
+
# Progress callback for repository discovery
|
|
1167
|
+
def discovery_progress(repo_name, count):
|
|
1168
|
+
if display and display._live:
|
|
1169
|
+
display.update_progress_task(
|
|
1170
|
+
"main",
|
|
1171
|
+
description=f"🔍 Discovering: {repo_name} ({count} repos checked)",
|
|
1172
|
+
completed=15 + min(count % 5, 4), # Show some movement
|
|
1173
|
+
)
|
|
1174
|
+
else:
|
|
1175
|
+
# Simple inline progress - just show count
|
|
1176
|
+
click.echo(f"\r 📦 Checking repositories... {count}", nl=False)
|
|
1177
|
+
|
|
1178
|
+
discovered_repos = cfg.discover_organization_repositories(
|
|
1179
|
+
clone_base_path=repos_dir, progress_callback=discovery_progress
|
|
1180
|
+
)
|
|
1297
1181
|
repositories_to_analyze = discovered_repos
|
|
1298
1182
|
|
|
1183
|
+
# Clear the progress line
|
|
1184
|
+
if not (display and display._live):
|
|
1185
|
+
click.echo("\r" + " " * 60 + "\r", nl=False) # Clear line
|
|
1186
|
+
|
|
1299
1187
|
if display and display._live:
|
|
1300
1188
|
# We're in full-screen mode, update progress and initialize repo list
|
|
1301
1189
|
display.update_progress_task(
|
|
@@ -1832,14 +1720,14 @@ def analyze(
|
|
|
1832
1720
|
"📊 No commits or batches found for date range - proceeding with data fetch"
|
|
1833
1721
|
)
|
|
1834
1722
|
|
|
1835
|
-
# PROCEED WITH
|
|
1723
|
+
# PROCEED WITH INITIAL FETCH if validation didn't pass
|
|
1836
1724
|
if not validation_passed:
|
|
1837
1725
|
if display:
|
|
1838
1726
|
display.print_status(
|
|
1839
|
-
"Data validation failed - running
|
|
1727
|
+
"Data validation failed - running initial data fetch", "warning"
|
|
1840
1728
|
)
|
|
1841
1729
|
else:
|
|
1842
|
-
click.echo("⚠️ Data validation failed - running
|
|
1730
|
+
click.echo("⚠️ Data validation failed - running initial data fetch")
|
|
1843
1731
|
|
|
1844
1732
|
# Force data fetch for all repositories since we have no batches
|
|
1845
1733
|
repos_needing_analysis = repositories_to_analyze
|
|
@@ -1848,12 +1736,12 @@ def analyze(
|
|
|
1848
1736
|
if repos_needing_analysis:
|
|
1849
1737
|
if display:
|
|
1850
1738
|
display.print_status(
|
|
1851
|
-
f"
|
|
1739
|
+
f"Initial fetch: Fetching data for {len(repos_needing_analysis)} repositories...",
|
|
1852
1740
|
"info",
|
|
1853
1741
|
)
|
|
1854
1742
|
else:
|
|
1855
1743
|
click.echo(
|
|
1856
|
-
f"🚨
|
|
1744
|
+
f"🚨 Initial fetch: Fetching data for {len(repos_needing_analysis)} repositories..."
|
|
1857
1745
|
)
|
|
1858
1746
|
click.echo(
|
|
1859
1747
|
" 📋 Reason: Need to ensure commits and batches exist for classification"
|
|
@@ -1886,6 +1774,176 @@ def analyze(
|
|
|
1886
1774
|
repo_path = Path(repo_config.path)
|
|
1887
1775
|
project_key = repo_config.project_key or repo_path.name
|
|
1888
1776
|
|
|
1777
|
+
# Check if repo exists, clone if needed (critical for organization mode)
|
|
1778
|
+
if not repo_path.exists():
|
|
1779
|
+
if repo_config.github_repo and cfg.github.organization:
|
|
1780
|
+
# Retry logic for cloning
|
|
1781
|
+
max_retries = 2
|
|
1782
|
+
retry_count = 0
|
|
1783
|
+
clone_success = False
|
|
1784
|
+
|
|
1785
|
+
while retry_count <= max_retries and not clone_success:
|
|
1786
|
+
if retry_count > 0:
|
|
1787
|
+
if display:
|
|
1788
|
+
display.print_status(
|
|
1789
|
+
f" 🔄 Retry {retry_count}/{max_retries}: {repo_config.github_repo}",
|
|
1790
|
+
"warning",
|
|
1791
|
+
)
|
|
1792
|
+
else:
|
|
1793
|
+
click.echo(
|
|
1794
|
+
f" 🔄 Retry {retry_count}/{max_retries}: {repo_config.github_repo}"
|
|
1795
|
+
)
|
|
1796
|
+
else:
|
|
1797
|
+
if display:
|
|
1798
|
+
display.print_status(
|
|
1799
|
+
f" 📥 Cloning {repo_config.github_repo} from GitHub...",
|
|
1800
|
+
"info",
|
|
1801
|
+
)
|
|
1802
|
+
else:
|
|
1803
|
+
click.echo(
|
|
1804
|
+
f" 📥 Cloning {repo_config.github_repo} from GitHub..."
|
|
1805
|
+
)
|
|
1806
|
+
|
|
1807
|
+
try:
|
|
1808
|
+
# Ensure parent directory exists
|
|
1809
|
+
repo_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1810
|
+
|
|
1811
|
+
# Build clone URL with authentication
|
|
1812
|
+
clone_url = (
|
|
1813
|
+
f"https://github.com/{repo_config.github_repo}.git"
|
|
1814
|
+
)
|
|
1815
|
+
if cfg.github.token:
|
|
1816
|
+
clone_url = f"https://{cfg.github.token}@github.com/{repo_config.github_repo}.git"
|
|
1817
|
+
|
|
1818
|
+
# Clone using subprocess for better control
|
|
1819
|
+
env = os.environ.copy()
|
|
1820
|
+
env["GIT_TERMINAL_PROMPT"] = "0"
|
|
1821
|
+
env["GIT_ASKPASS"] = ""
|
|
1822
|
+
env["GCM_INTERACTIVE"] = "never"
|
|
1823
|
+
env["GIT_PROGRESS"] = "1" # Force progress output
|
|
1824
|
+
|
|
1825
|
+
cmd = [
|
|
1826
|
+
"git",
|
|
1827
|
+
"clone",
|
|
1828
|
+
"--progress",
|
|
1829
|
+
"--config",
|
|
1830
|
+
"credential.helper=",
|
|
1831
|
+
]
|
|
1832
|
+
if repo_config.branch:
|
|
1833
|
+
cmd.extend(["-b", repo_config.branch])
|
|
1834
|
+
cmd.extend([clone_url, str(repo_path)])
|
|
1835
|
+
|
|
1836
|
+
# Track start time for timeout reporting
|
|
1837
|
+
import time
|
|
1838
|
+
|
|
1839
|
+
start_time = time.time()
|
|
1840
|
+
timeout_seconds = 300 # 5 minutes for large repos
|
|
1841
|
+
|
|
1842
|
+
# Run without capturing stderr to show git progress
|
|
1843
|
+
result = subprocess.run(
|
|
1844
|
+
cmd,
|
|
1845
|
+
env=env,
|
|
1846
|
+
stdout=subprocess.PIPE,
|
|
1847
|
+
stderr=None, # Let stderr (progress) flow to terminal
|
|
1848
|
+
text=True,
|
|
1849
|
+
timeout=timeout_seconds,
|
|
1850
|
+
)
|
|
1851
|
+
|
|
1852
|
+
elapsed = time.time() - start_time
|
|
1853
|
+
|
|
1854
|
+
if result.returncode != 0:
|
|
1855
|
+
error_msg = "Clone failed"
|
|
1856
|
+
if any(
|
|
1857
|
+
x in error_msg.lower()
|
|
1858
|
+
for x in [
|
|
1859
|
+
"authentication",
|
|
1860
|
+
"permission denied",
|
|
1861
|
+
"401",
|
|
1862
|
+
"403",
|
|
1863
|
+
]
|
|
1864
|
+
):
|
|
1865
|
+
if display:
|
|
1866
|
+
display.print_status(
|
|
1867
|
+
f" ❌ Authentication failed for {repo_config.github_repo}",
|
|
1868
|
+
"error",
|
|
1869
|
+
)
|
|
1870
|
+
else:
|
|
1871
|
+
click.echo(
|
|
1872
|
+
f" ❌ Authentication failed for {repo_config.github_repo}"
|
|
1873
|
+
)
|
|
1874
|
+
break # Don't retry auth failures
|
|
1875
|
+
else:
|
|
1876
|
+
raise subprocess.CalledProcessError(
|
|
1877
|
+
result.returncode,
|
|
1878
|
+
cmd,
|
|
1879
|
+
result.stdout,
|
|
1880
|
+
result.stderr,
|
|
1881
|
+
)
|
|
1882
|
+
else:
|
|
1883
|
+
clone_success = True
|
|
1884
|
+
if display:
|
|
1885
|
+
display.print_status(
|
|
1886
|
+
f" ✅ Cloned {repo_config.github_repo} ({elapsed:.1f}s)",
|
|
1887
|
+
"success",
|
|
1888
|
+
)
|
|
1889
|
+
else:
|
|
1890
|
+
click.echo(
|
|
1891
|
+
f" ✅ Cloned {repo_config.github_repo} ({elapsed:.1f}s)"
|
|
1892
|
+
)
|
|
1893
|
+
|
|
1894
|
+
except subprocess.TimeoutExpired:
|
|
1895
|
+
retry_count += 1
|
|
1896
|
+
if display:
|
|
1897
|
+
display.print_status(
|
|
1898
|
+
f" ⏱️ Clone timeout after {timeout_seconds}s: {repo_config.github_repo}",
|
|
1899
|
+
"error",
|
|
1900
|
+
)
|
|
1901
|
+
else:
|
|
1902
|
+
click.echo(
|
|
1903
|
+
f" ⏱️ Clone timeout after {timeout_seconds}s: {repo_config.github_repo}"
|
|
1904
|
+
)
|
|
1905
|
+
# Clean up partial clone
|
|
1906
|
+
if repo_path.exists():
|
|
1907
|
+
import shutil
|
|
1908
|
+
|
|
1909
|
+
shutil.rmtree(repo_path, ignore_errors=True)
|
|
1910
|
+
if retry_count > max_retries:
|
|
1911
|
+
if display:
|
|
1912
|
+
display.print_status(
|
|
1913
|
+
f" ❌ Skipping {repo_config.github_repo} after {max_retries} timeouts",
|
|
1914
|
+
"error",
|
|
1915
|
+
)
|
|
1916
|
+
else:
|
|
1917
|
+
click.echo(
|
|
1918
|
+
f" ❌ Skipping {repo_config.github_repo} after {max_retries} timeouts"
|
|
1919
|
+
)
|
|
1920
|
+
break
|
|
1921
|
+
continue # Try again
|
|
1922
|
+
|
|
1923
|
+
except Exception as e:
|
|
1924
|
+
retry_count += 1
|
|
1925
|
+
if display:
|
|
1926
|
+
display.print_status(
|
|
1927
|
+
f" ❌ Clone error: {e}", "error"
|
|
1928
|
+
)
|
|
1929
|
+
else:
|
|
1930
|
+
click.echo(f" ❌ Clone error: {e}")
|
|
1931
|
+
if retry_count > max_retries:
|
|
1932
|
+
break
|
|
1933
|
+
continue # Try again
|
|
1934
|
+
|
|
1935
|
+
if not clone_success:
|
|
1936
|
+
continue # Skip this repo and move to next
|
|
1937
|
+
else:
|
|
1938
|
+
# No github_repo configured, can't clone
|
|
1939
|
+
if display:
|
|
1940
|
+
display.print_status(
|
|
1941
|
+
f" ❌ Repository not found: {repo_path}", "error"
|
|
1942
|
+
)
|
|
1943
|
+
else:
|
|
1944
|
+
click.echo(f" ❌ Repository not found: {repo_path}")
|
|
1945
|
+
continue
|
|
1946
|
+
|
|
1889
1947
|
# Progress callback for fetch
|
|
1890
1948
|
def progress_callback(message: str):
|
|
1891
1949
|
if display:
|
|
@@ -1961,15 +2019,15 @@ def analyze(
|
|
|
1961
2019
|
|
|
1962
2020
|
if display:
|
|
1963
2021
|
display.print_status(
|
|
1964
|
-
f"
|
|
2022
|
+
f"Initial fetch complete: {total_commits} commits, {total_tickets} tickets",
|
|
1965
2023
|
"success",
|
|
1966
2024
|
)
|
|
1967
2025
|
else:
|
|
1968
2026
|
click.echo(
|
|
1969
|
-
f"🚨
|
|
2027
|
+
f"🚨 Initial fetch complete: {total_commits} commits, {total_tickets} tickets"
|
|
1970
2028
|
)
|
|
1971
2029
|
|
|
1972
|
-
# RE-VALIDATE after
|
|
2030
|
+
# RE-VALIDATE after initial fetch
|
|
1973
2031
|
with cache.get_session() as session:
|
|
1974
2032
|
final_commits = (
|
|
1975
2033
|
session.query(CachedCommit)
|
|
@@ -1994,7 +2052,7 @@ def analyze(
|
|
|
1994
2052
|
)
|
|
1995
2053
|
|
|
1996
2054
|
if final_commits == 0:
|
|
1997
|
-
error_msg = "❌ CRITICAL:
|
|
2055
|
+
error_msg = "❌ CRITICAL: Initial fetch completed but still 0 commits stored in database"
|
|
1998
2056
|
if display:
|
|
1999
2057
|
display.print_status(error_msg, "error")
|
|
2000
2058
|
else:
|
|
@@ -2002,7 +2060,9 @@ def analyze(
|
|
|
2002
2060
|
click.echo(
|
|
2003
2061
|
f" 📅 Date range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}"
|
|
2004
2062
|
)
|
|
2005
|
-
click.echo(
|
|
2063
|
+
click.echo(
|
|
2064
|
+
f" 📊 Initial fetch stats: {total_commits} commits reported"
|
|
2065
|
+
)
|
|
2006
2066
|
click.echo(
|
|
2007
2067
|
f" 🗃️ Database result: {final_commits} commits, {final_batches} batches"
|
|
2008
2068
|
)
|
|
@@ -2016,7 +2076,7 @@ def analyze(
|
|
|
2016
2076
|
" - Repository has no commits in the specified time range"
|
|
2017
2077
|
)
|
|
2018
2078
|
raise click.ClickException(
|
|
2019
|
-
"
|
|
2079
|
+
"Initial fetch failed validation - no data available for classification"
|
|
2020
2080
|
)
|
|
2021
2081
|
|
|
2022
2082
|
if display:
|
|
@@ -2375,25 +2435,33 @@ def analyze(
|
|
|
2375
2435
|
env["GIT_TERMINAL_PROMPT"] = "0"
|
|
2376
2436
|
env["GIT_ASKPASS"] = ""
|
|
2377
2437
|
env["GCM_INTERACTIVE"] = "never"
|
|
2438
|
+
env["GIT_PROGRESS"] = "1" # Force progress output
|
|
2378
2439
|
|
|
2379
2440
|
# Build git clone command
|
|
2380
|
-
cmd = [
|
|
2441
|
+
cmd = [
|
|
2442
|
+
"git",
|
|
2443
|
+
"clone",
|
|
2444
|
+
"--progress",
|
|
2445
|
+
"--config",
|
|
2446
|
+
"credential.helper=",
|
|
2447
|
+
]
|
|
2381
2448
|
if repo_config.branch:
|
|
2382
2449
|
cmd.extend(["-b", repo_config.branch])
|
|
2383
2450
|
cmd.extend([clone_url, str(repo_config.path)])
|
|
2384
2451
|
|
|
2385
|
-
# Run with timeout to prevent hanging
|
|
2452
|
+
# Run with timeout to prevent hanging, let progress show on stderr
|
|
2386
2453
|
result = subprocess.run(
|
|
2387
2454
|
cmd,
|
|
2388
2455
|
env=env,
|
|
2389
|
-
|
|
2456
|
+
stdout=subprocess.PIPE,
|
|
2457
|
+
stderr=None, # Let stderr (progress) flow to terminal
|
|
2390
2458
|
text=True,
|
|
2391
|
-
timeout=
|
|
2459
|
+
timeout=120, # Increase timeout for large repos
|
|
2392
2460
|
)
|
|
2393
2461
|
|
|
2394
2462
|
if result.returncode != 0:
|
|
2395
2463
|
raise git.GitCommandError(
|
|
2396
|
-
cmd, result.returncode, stderr=
|
|
2464
|
+
cmd, result.returncode, stderr="Clone failed"
|
|
2397
2465
|
)
|
|
2398
2466
|
except subprocess.TimeoutExpired:
|
|
2399
2467
|
if display:
|
|
@@ -2442,13 +2510,19 @@ def analyze(
|
|
|
2442
2510
|
cmd = [
|
|
2443
2511
|
"git",
|
|
2444
2512
|
"clone",
|
|
2513
|
+
"--progress",
|
|
2445
2514
|
"--config",
|
|
2446
2515
|
"credential.helper=",
|
|
2447
2516
|
clone_url,
|
|
2448
2517
|
str(repo_config.path),
|
|
2449
2518
|
]
|
|
2450
2519
|
result = subprocess.run(
|
|
2451
|
-
cmd,
|
|
2520
|
+
cmd,
|
|
2521
|
+
env=env,
|
|
2522
|
+
stdout=subprocess.PIPE,
|
|
2523
|
+
stderr=None,
|
|
2524
|
+
text=True,
|
|
2525
|
+
timeout=120,
|
|
2452
2526
|
)
|
|
2453
2527
|
if result.returncode != 0:
|
|
2454
2528
|
raise git.GitCommandError(
|
|
@@ -3567,6 +3641,9 @@ def analyze(
|
|
|
3567
3641
|
logger.debug("Starting narrative report generation")
|
|
3568
3642
|
narrative_gen = NarrativeReportGenerator()
|
|
3569
3643
|
|
|
3644
|
+
# Lazy import pandas - only needed for CSV reading in narrative generation
|
|
3645
|
+
import pandas as pd
|
|
3646
|
+
|
|
3570
3647
|
# Load activity distribution data
|
|
3571
3648
|
logger.debug("Loading activity distribution data")
|
|
3572
3649
|
activity_df = pd.read_csv(activity_report)
|
|
@@ -4111,7 +4188,7 @@ def analyze(
|
|
|
4111
4188
|
"--no-rich",
|
|
4112
4189
|
is_flag=True,
|
|
4113
4190
|
default=True,
|
|
4114
|
-
help="Disable rich terminal output (simple
|
|
4191
|
+
help="Disable rich terminal output (use simple text progress instead)",
|
|
4115
4192
|
)
|
|
4116
4193
|
def fetch(
|
|
4117
4194
|
config: Path,
|
|
@@ -4161,7 +4238,7 @@ def fetch(
|
|
|
4161
4238
|
- Use --clear-cache to force fresh fetch
|
|
4162
4239
|
"""
|
|
4163
4240
|
# Initialize display
|
|
4164
|
-
# Create display - simple output by default
|
|
4241
|
+
# Create display - simple output by default for better compatibility, rich only when explicitly enabled
|
|
4165
4242
|
display = (
|
|
4166
4243
|
create_progress_display(style="simple" if no_rich else "rich", version=__version__)
|
|
4167
4244
|
if not no_rich
|
|
@@ -4186,6 +4263,10 @@ def fetch(
|
|
|
4186
4263
|
logger = logging.getLogger(__name__)
|
|
4187
4264
|
|
|
4188
4265
|
try:
|
|
4266
|
+
# Lazy imports
|
|
4267
|
+
from .core.cache import GitAnalysisCache
|
|
4268
|
+
from .integrations.orchestrator import IntegrationOrchestrator
|
|
4269
|
+
|
|
4189
4270
|
if display:
|
|
4190
4271
|
display.show_header()
|
|
4191
4272
|
|
|
@@ -4218,9 +4299,7 @@ def fetch(
|
|
|
4218
4299
|
data_fetcher = GitDataFetcher(
|
|
4219
4300
|
cache=cache,
|
|
4220
4301
|
branch_mapping_rules=getattr(cfg.analysis, "branch_mapping_rules", {}),
|
|
4221
|
-
allowed_ticket_platforms=
|
|
4222
|
-
cfg.analysis, "ticket_platforms", ["jira", "github", "clickup", "linear"]
|
|
4223
|
-
),
|
|
4302
|
+
allowed_ticket_platforms=cfg.get_effective_ticket_platforms(),
|
|
4224
4303
|
exclude_paths=getattr(cfg.analysis, "exclude_paths", None),
|
|
4225
4304
|
)
|
|
4226
4305
|
|
|
@@ -4243,9 +4322,23 @@ def fetch(
|
|
|
4243
4322
|
# Use a 'repos' directory in the config directory for cloned repositories
|
|
4244
4323
|
config_dir = Path(config).parent if config else Path.cwd()
|
|
4245
4324
|
repos_dir = config_dir / "repos"
|
|
4246
|
-
|
|
4325
|
+
|
|
4326
|
+
# Progress callback for repository discovery
|
|
4327
|
+
def discovery_progress(repo_name, count):
|
|
4328
|
+
if display:
|
|
4329
|
+
display.print_status(f" 📦 Checking: {repo_name} ({count})", "info")
|
|
4330
|
+
else:
|
|
4331
|
+
click.echo(f"\r 📦 Checking repositories... {count}", nl=False)
|
|
4332
|
+
|
|
4333
|
+
discovered_repos = cfg.discover_organization_repositories(
|
|
4334
|
+
clone_base_path=repos_dir, progress_callback=discovery_progress
|
|
4335
|
+
)
|
|
4247
4336
|
repositories_to_fetch = discovered_repos
|
|
4248
4337
|
|
|
4338
|
+
# Clear the progress line
|
|
4339
|
+
if not display:
|
|
4340
|
+
click.echo("\r" + " " * 60 + "\r", nl=False) # Clear line
|
|
4341
|
+
|
|
4249
4342
|
if display:
|
|
4250
4343
|
display.print_status(
|
|
4251
4344
|
f"Found {len(discovered_repos)} repositories in organization", "success"
|
|
@@ -4426,6 +4519,8 @@ def cache_stats(config: Path) -> None:
|
|
|
4426
4519
|
- Decide when to clear cache
|
|
4427
4520
|
- Troubleshoot slow analyses
|
|
4428
4521
|
"""
|
|
4522
|
+
from .core.cache import GitAnalysisCache
|
|
4523
|
+
|
|
4429
4524
|
try:
|
|
4430
4525
|
cfg = ConfigLoader.load(config)
|
|
4431
4526
|
cache = GitAnalysisCache(cfg.cache.directory)
|
|
@@ -4480,6 +4575,8 @@ def merge_identity(config: Path, dev1: str, dev2: str) -> None:
|
|
|
4480
4575
|
- Refreshes cached statistics
|
|
4481
4576
|
- Updates identity mappings
|
|
4482
4577
|
"""
|
|
4578
|
+
from .core.identity import DeveloperIdentityResolver
|
|
4579
|
+
|
|
4483
4580
|
try:
|
|
4484
4581
|
cfg = ConfigLoader.load(config)
|
|
4485
4582
|
identity_resolver = DeveloperIdentityResolver(cfg.cache.directory / "identities.db")
|
|
@@ -4745,6 +4842,7 @@ def discover_storypoint_fields(config: Path) -> None:
|
|
|
4745
4842
|
return
|
|
4746
4843
|
|
|
4747
4844
|
# Initialize PM integration (currently JIRA)
|
|
4845
|
+
from .core.cache import GitAnalysisCache
|
|
4748
4846
|
from .integrations.jira_integration import JIRAIntegration
|
|
4749
4847
|
|
|
4750
4848
|
# Create minimal cache for integration
|
|
@@ -4828,6 +4926,9 @@ def identities(config: Path, weeks: int, apply: bool) -> None:
|
|
|
4828
4926
|
Mappings are saved to 'analysis.identity.manual_mappings'
|
|
4829
4927
|
Bot exclusions go to 'analysis.exclude.authors'
|
|
4830
4928
|
"""
|
|
4929
|
+
from .core.analyzer import GitAnalyzer
|
|
4930
|
+
from .core.cache import GitAnalysisCache
|
|
4931
|
+
|
|
4831
4932
|
try:
|
|
4832
4933
|
cfg = ConfigLoader.load(config)
|
|
4833
4934
|
cache = GitAnalysisCache(cfg.cache.directory)
|
|
@@ -4888,9 +4989,7 @@ def identities(config: Path, weeks: int, apply: bool) -> None:
|
|
|
4888
4989
|
analyzer = GitAnalyzer(
|
|
4889
4990
|
cache,
|
|
4890
4991
|
branch_mapping_rules=cfg.analysis.branch_mapping_rules,
|
|
4891
|
-
allowed_ticket_platforms=
|
|
4892
|
-
cfg.analysis, "ticket_platforms", ["jira", "github", "clickup", "linear"]
|
|
4893
|
-
),
|
|
4992
|
+
allowed_ticket_platforms=cfg.get_effective_ticket_platforms(),
|
|
4894
4993
|
exclude_paths=cfg.analysis.exclude_paths,
|
|
4895
4994
|
story_point_patterns=cfg.analysis.story_point_patterns,
|
|
4896
4995
|
ml_categorization_config=ml_config,
|
|
@@ -5058,6 +5157,8 @@ def aliases_command(
|
|
|
5058
5157
|
"""
|
|
5059
5158
|
try:
|
|
5060
5159
|
from .config.aliases import AliasesManager, DeveloperAlias
|
|
5160
|
+
from .core.analyzer import GitAnalyzer
|
|
5161
|
+
from .core.cache import GitAnalysisCache
|
|
5061
5162
|
from .identity_llm.analyzer import LLMIdentityAnalyzer
|
|
5062
5163
|
|
|
5063
5164
|
# Load configuration
|
|
@@ -5125,9 +5226,7 @@ def aliases_command(
|
|
|
5125
5226
|
analyzer = GitAnalyzer(
|
|
5126
5227
|
cache,
|
|
5127
5228
|
branch_mapping_rules=cfg.analysis.branch_mapping_rules,
|
|
5128
|
-
allowed_ticket_platforms=
|
|
5129
|
-
cfg.analysis, "ticket_platforms", ["jira", "github", "clickup", "linear"]
|
|
5130
|
-
),
|
|
5229
|
+
allowed_ticket_platforms=cfg.get_effective_ticket_platforms(),
|
|
5131
5230
|
exclude_paths=cfg.analysis.exclude_paths,
|
|
5132
5231
|
story_point_patterns=cfg.analysis.story_point_patterns,
|
|
5133
5232
|
ml_categorization_config=ml_config,
|
|
@@ -5361,6 +5460,8 @@ def list_developers(config: Path) -> None:
|
|
|
5361
5460
|
- Finding developer email addresses
|
|
5362
5461
|
- Checking contribution statistics
|
|
5363
5462
|
"""
|
|
5463
|
+
from .core.identity import DeveloperIdentityResolver
|
|
5464
|
+
|
|
5364
5465
|
try:
|
|
5365
5466
|
cfg = ConfigLoader.load(config)
|
|
5366
5467
|
identity_resolver = DeveloperIdentityResolver(cfg.cache.directory / "identities.db")
|
|
@@ -5494,6 +5595,8 @@ def train(
|
|
|
5494
5595
|
- scikit-learn and pandas dependencies
|
|
5495
5596
|
- ~100MB disk space for model storage
|
|
5496
5597
|
"""
|
|
5598
|
+
from .core.cache import GitAnalysisCache
|
|
5599
|
+
from .integrations.orchestrator import IntegrationOrchestrator
|
|
5497
5600
|
|
|
5498
5601
|
# Configure logging
|
|
5499
5602
|
if log.upper() != "NONE":
|
|
@@ -5577,8 +5680,18 @@ def train(
|
|
|
5577
5680
|
try:
|
|
5578
5681
|
config_dir = Path(config).parent if config else Path.cwd()
|
|
5579
5682
|
repos_dir = config_dir / "repos"
|
|
5580
|
-
|
|
5683
|
+
|
|
5684
|
+
# Progress callback for repository discovery
|
|
5685
|
+
def discovery_progress(repo_name, count):
|
|
5686
|
+
click.echo(f"\r 📦 Checking repositories... {count}", nl=False)
|
|
5687
|
+
|
|
5688
|
+
discovered_repos = cfg.discover_organization_repositories(
|
|
5689
|
+
clone_base_path=repos_dir, progress_callback=discovery_progress
|
|
5690
|
+
)
|
|
5581
5691
|
repositories_to_analyze = discovered_repos
|
|
5692
|
+
|
|
5693
|
+
# Clear the progress line and show result
|
|
5694
|
+
click.echo("\r" + " " * 60 + "\r", nl=False)
|
|
5582
5695
|
click.echo(f"✅ Found {len(discovered_repos)} repositories in organization")
|
|
5583
5696
|
except Exception as e:
|
|
5584
5697
|
click.echo(f"❌ Failed to discover repositories: {e}")
|
|
@@ -5604,6 +5717,9 @@ def train(
|
|
|
5604
5717
|
# Initialize trainer
|
|
5605
5718
|
click.echo("🧠 Initializing training pipeline...")
|
|
5606
5719
|
try:
|
|
5720
|
+
# Lazy import - only needed for train command
|
|
5721
|
+
from .training.pipeline import CommitClassificationTrainer
|
|
5722
|
+
|
|
5607
5723
|
trainer = CommitClassificationTrainer(
|
|
5608
5724
|
config=cfg, cache=cache, orchestrator=orchestrator, training_config=training_config
|
|
5609
5725
|
)
|
|
@@ -5939,6 +6055,10 @@ def training_statistics(config: Path) -> None:
|
|
|
5939
6055
|
- Compare different model versions
|
|
5940
6056
|
"""
|
|
5941
6057
|
try:
|
|
6058
|
+
# Lazy imports
|
|
6059
|
+
from .core.cache import GitAnalysisCache
|
|
6060
|
+
from .training.pipeline import CommitClassificationTrainer
|
|
6061
|
+
|
|
5942
6062
|
cfg = ConfigLoader.load(config)
|
|
5943
6063
|
cache = GitAnalysisCache(cfg.cache.directory)
|
|
5944
6064
|
|