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.
Files changed (27) hide show
  1. gitflow_analytics/__init__.py +8 -12
  2. gitflow_analytics/_version.py +1 -1
  3. gitflow_analytics/cli.py +323 -203
  4. gitflow_analytics/cli_wizards/install_wizard.py +5 -5
  5. gitflow_analytics/config/repository.py +9 -1
  6. gitflow_analytics/config/schema.py +39 -0
  7. gitflow_analytics/identity_llm/analysis_pass.py +7 -2
  8. gitflow_analytics/models/database.py +229 -8
  9. {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.4.dist-info}/METADATA +2 -4
  10. {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.4.dist-info}/RECORD +14 -27
  11. gitflow_analytics/tui/__init__.py +0 -5
  12. gitflow_analytics/tui/app.py +0 -726
  13. gitflow_analytics/tui/progress_adapter.py +0 -313
  14. gitflow_analytics/tui/screens/__init__.py +0 -8
  15. gitflow_analytics/tui/screens/analysis_progress_screen.py +0 -857
  16. gitflow_analytics/tui/screens/configuration_screen.py +0 -523
  17. gitflow_analytics/tui/screens/loading_screen.py +0 -348
  18. gitflow_analytics/tui/screens/main_screen.py +0 -321
  19. gitflow_analytics/tui/screens/results_screen.py +0 -735
  20. gitflow_analytics/tui/widgets/__init__.py +0 -7
  21. gitflow_analytics/tui/widgets/data_table.py +0 -255
  22. gitflow_analytics/tui/widgets/export_modal.py +0 -301
  23. gitflow_analytics/tui/widgets/progress_widget.py +0 -187
  24. {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.4.dist-info}/WHEEL +0 -0
  25. {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.4.dist-info}/entry_points.txt +0 -0
  26. {gitflow_analytics-3.6.2.dist-info → gitflow_analytics-3.7.4.dist-info}/licenses/LICENSE +0 -0
  27. {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 TUIAsDefaultGroup(click.Group):
263
+ class AnalyzeAsDefaultGroup(click.Group):
261
264
  """
262
- Custom Click group that defaults to TUI when no explicit subcommand is provided.
263
- This allows 'gitflow-analytics -c config.yaml' to launch the TUI by default.
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 TUI unless explicit subcommand or CLI-only options."""
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 TUI
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
- # Check if we have arguments that indicate explicit CLI analysis request
279
- cli_only_indicators = {
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=TUIAsDefaultGroup, invoke_without_command=True)
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 output is default to prevent TUI hanging)",
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
- # Initialize progress service early with the correct style
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 to prevent TUI hanging
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.analysis.ticket_platforms,
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=getattr(
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
- discovered_repos = cfg.discover_organization_repositories(clone_base_path=repos_dir)
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 EMERGENCY FETCH if validation didn't pass
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 emergency data fetch", "warning"
1727
+ "Data validation failed - running initial data fetch", "warning"
1840
1728
  )
1841
1729
  else:
1842
- click.echo("⚠️ Data validation failed - running emergency data fetch")
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"Emergency fetch: Fetching data for {len(repos_needing_analysis)} repositories...",
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"🚨 Emergency fetch: Fetching data for {len(repos_needing_analysis)} repositories..."
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"Emergency fetch complete: {total_commits} commits, {total_tickets} tickets",
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"🚨 Emergency fetch complete: {total_commits} commits, {total_tickets} tickets"
2027
+ f"🚨 Initial fetch complete: {total_commits} commits, {total_tickets} tickets"
1970
2028
  )
1971
2029
 
1972
- # RE-VALIDATE after emergency fetch
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: Emergency fetch completed but still 0 commits stored in database"
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(f" 📊 Emergency stats: {total_commits} commits reported")
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
- "Emergency fetch failed validation - no data available for classification"
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 = ["git", "clone", "--config", "credential.helper="]
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
- capture_output=True,
2456
+ stdout=subprocess.PIPE,
2457
+ stderr=None, # Let stderr (progress) flow to terminal
2390
2458
  text=True,
2391
- timeout=30, # 30 second 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=result.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, env=env, capture_output=True, text=True, timeout=30
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 output is default to prevent TUI hanging)",
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 to prevent TUI hanging, rich only when explicitly enabled
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=getattr(
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
- discovered_repos = cfg.discover_organization_repositories(clone_base_path=repos_dir)
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=getattr(
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=getattr(
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
- discovered_repos = cfg.discover_organization_repositories(clone_base_path=repos_dir)
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