gitflow-analytics 3.3.0__py3-none-any.whl → 3.5.2__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 (36) hide show
  1. gitflow_analytics/_version.py +1 -1
  2. gitflow_analytics/cli.py +517 -15
  3. gitflow_analytics/cli_wizards/__init__.py +10 -0
  4. gitflow_analytics/cli_wizards/install_wizard.py +1181 -0
  5. gitflow_analytics/cli_wizards/run_launcher.py +433 -0
  6. gitflow_analytics/config/__init__.py +3 -0
  7. gitflow_analytics/config/aliases.py +306 -0
  8. gitflow_analytics/config/loader.py +35 -1
  9. gitflow_analytics/config/schema.py +13 -0
  10. gitflow_analytics/constants.py +75 -0
  11. gitflow_analytics/core/cache.py +7 -3
  12. gitflow_analytics/core/data_fetcher.py +66 -30
  13. gitflow_analytics/core/git_timeout_wrapper.py +6 -4
  14. gitflow_analytics/core/progress.py +2 -4
  15. gitflow_analytics/core/subprocess_git.py +31 -5
  16. gitflow_analytics/identity_llm/analysis_pass.py +13 -3
  17. gitflow_analytics/identity_llm/analyzer.py +14 -2
  18. gitflow_analytics/identity_llm/models.py +7 -1
  19. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +5 -3
  20. gitflow_analytics/security/config.py +6 -6
  21. gitflow_analytics/security/extractors/dependency_checker.py +14 -14
  22. gitflow_analytics/security/extractors/secret_detector.py +8 -14
  23. gitflow_analytics/security/extractors/vulnerability_scanner.py +9 -9
  24. gitflow_analytics/security/llm_analyzer.py +10 -10
  25. gitflow_analytics/security/security_analyzer.py +17 -17
  26. gitflow_analytics/tui/screens/analysis_progress_screen.py +1 -1
  27. gitflow_analytics/ui/progress_display.py +36 -29
  28. gitflow_analytics/verify_activity.py +23 -26
  29. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/METADATA +1 -1
  30. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/RECORD +34 -31
  31. gitflow_analytics/security/reports/__init__.py +0 -5
  32. gitflow_analytics/security/reports/security_report.py +0 -358
  33. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/WHEEL +0 -0
  34. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/entry_points.txt +0 -0
  35. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/licenses/LICENSE +0 -0
  36. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
1
  """Version information for gitflow-analytics."""
2
2
 
3
- __version__ = "3.3.0"
3
+ __version__ = "3.5.2"
4
4
  __version_info__ = tuple(int(x) for x in __version__.split("."))
gitflow_analytics/cli.py CHANGED
@@ -33,6 +33,8 @@ from .reports.weekly_trends_writer import WeeklyTrendsWriter
33
33
  from .training.pipeline import CommitClassificationTrainer
34
34
  from .ui.progress_display import create_progress_display
35
35
 
36
+ logger = logging.getLogger(__name__)
37
+
36
38
 
37
39
  class RichHelpFormatter:
38
40
  """Rich help formatter for enhanced CLI help display."""
@@ -48,8 +50,19 @@ class RichHelpFormatter:
48
50
  return help_text
49
51
 
50
52
  @staticmethod
51
- def format_option_help(description: str, default: Any = None, choices: list = None) -> str:
52
- """Format option help with default and choices."""
53
+ def format_option_help(
54
+ description: str, default: Optional[str] = None, choices: Optional[list[str]] = None
55
+ ) -> str:
56
+ """Format option help with default and choices.
57
+
58
+ Args:
59
+ description: Option description text
60
+ default: Default value to display (optional)
61
+ choices: List of valid choices (optional)
62
+
63
+ Returns:
64
+ Formatted help text string
65
+ """
53
66
  help_text = description
54
67
  if default is not None:
55
68
  help_text += f" [default: {default}]"
@@ -281,12 +294,18 @@ class TUIAsDefaultGroup(click.Group):
281
294
  if args and args[0].startswith("-"):
282
295
  # Check if TUI dependencies are available
283
296
  try:
284
- import textual
297
+ import importlib.util
285
298
 
299
+ textual_spec = importlib.util.find_spec("textual")
286
300
  # TUI is available - route to TUI
287
- new_args = ["tui"] + args
288
- return super().parse_args(ctx, new_args)
289
- except ImportError:
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):
290
309
  # TUI not available - fallback to analyze
291
310
  new_args = ["analyze"] + args
292
311
  return super().parse_args(ctx, new_args)
@@ -323,11 +342,28 @@ def cli(ctx: click.Context) -> None:
323
342
  \b
324
343
  COMMANDS:
325
344
  analyze Analyze repositories and generate reports (default)
345
+ install Interactive installation wizard
346
+ run Interactive launcher with preferences
347
+ aliases Generate developer identity aliases using LLM
326
348
  identities Manage developer identity resolution
327
349
  train Train ML models for commit classification
328
350
  fetch Fetch external data (GitHub PRs, PM tickets)
329
351
  help Show detailed help and documentation
330
352
 
353
+ \b
354
+ EXAMPLES:
355
+ # Interactive installation
356
+ gitflow-analytics install
357
+
358
+ # Interactive launcher
359
+ gitflow-analytics run -c config.yaml
360
+
361
+ # Generate developer aliases
362
+ gitflow-analytics aliases -c config.yaml --apply
363
+
364
+ # Run analysis
365
+ gitflow-analytics -c config.yaml --weeks 4
366
+
331
367
  \b
332
368
  For detailed command help: gitflow-analytics COMMAND --help
333
369
  For documentation: https://github.com/yourusername/gitflow-analytics
@@ -1066,10 +1102,7 @@ def analyze(
1066
1102
  )
1067
1103
 
1068
1104
  # Extract commits from the raw data
1069
- if raw_data and raw_data.get("commits"):
1070
- commits = raw_data["commits"]
1071
- else:
1072
- commits = []
1105
+ commits = raw_data["commits"] if raw_data and raw_data.get("commits") else []
1073
1106
  all_commits.extend(commits)
1074
1107
 
1075
1108
  if not all_commits:
@@ -4460,6 +4493,114 @@ def merge_identity(config: Path, dev1: str, dev2: str) -> None:
4460
4493
  sys.exit(1)
4461
4494
 
4462
4495
 
4496
+ @cli.command(name="run")
4497
+ @click.option(
4498
+ "--config",
4499
+ "-c",
4500
+ type=click.Path(exists=True, path_type=Path),
4501
+ help="Path to configuration file (optional, will search for default)",
4502
+ )
4503
+ def run_launcher(config: Optional[Path]) -> None:
4504
+ """Interactive launcher for gitflow-analytics.
4505
+
4506
+ \b
4507
+ This interactive command guides you through:
4508
+ • Repository selection (multi-select)
4509
+ • Analysis period configuration
4510
+ • Cache management
4511
+ • Identity analysis preferences
4512
+ • Preferences storage
4513
+
4514
+ \b
4515
+ EXAMPLES:
4516
+ # Launch interactive mode
4517
+ gitflow-analytics run
4518
+
4519
+ # Launch with specific config
4520
+ gitflow-analytics run -c config.yaml
4521
+
4522
+ \b
4523
+ PREFERENCES:
4524
+ Your selections are saved to the launcher section
4525
+ in your configuration file for future use.
4526
+
4527
+ \b
4528
+ WORKFLOW:
4529
+ 1. Select repositories to analyze
4530
+ 2. Choose analysis period (weeks)
4531
+ 3. Configure cache clearing
4532
+ 4. Set identity analysis preference
4533
+ 5. Run analysis with your selections
4534
+ """
4535
+ try:
4536
+ from .cli_wizards.run_launcher import run_interactive_launcher
4537
+
4538
+ success = run_interactive_launcher(config_path=config)
4539
+ sys.exit(0 if success else 1)
4540
+
4541
+ except Exception as e:
4542
+ click.echo(f"❌ Launcher failed: {e}", err=True)
4543
+ logger.error(f"Launcher error: {type(e).__name__}")
4544
+ sys.exit(1)
4545
+
4546
+
4547
+ @cli.command(name="install")
4548
+ @click.option(
4549
+ "--output-dir",
4550
+ type=click.Path(path_type=Path),
4551
+ default=".",
4552
+ help="Directory for config files (default: current directory)",
4553
+ )
4554
+ @click.option(
4555
+ "--skip-validation",
4556
+ is_flag=True,
4557
+ help="Skip credential validation (for testing)",
4558
+ )
4559
+ def install_command(output_dir: Path, skip_validation: bool) -> None:
4560
+ """Interactive installation wizard for GitFlow Analytics.
4561
+
4562
+ \b
4563
+ This wizard will guide you through setting up GitFlow Analytics:
4564
+ • GitHub credentials and repository configuration
4565
+ • Optional JIRA integration
4566
+ • Optional AI-powered insights (OpenRouter/ChatGPT)
4567
+ • Analysis settings and defaults
4568
+
4569
+ \b
4570
+ EXAMPLES:
4571
+ # Run installation wizard in current directory
4572
+ gitflow-analytics install
4573
+
4574
+ # Install to specific directory
4575
+ gitflow-analytics install --output-dir ./my-config
4576
+
4577
+ \b
4578
+ The wizard will:
4579
+ 1. Validate all credentials before saving
4580
+ 2. Generate config.yaml and .env files
4581
+ 3. Set secure permissions on .env (0600)
4582
+ 4. Update .gitignore if in a git repository
4583
+ 5. Test the configuration
4584
+ 6. Optionally run initial analysis
4585
+
4586
+ \b
4587
+ SECURITY NOTES:
4588
+ • .env file contains sensitive credentials
4589
+ • Never commit .env to version control
4590
+ • File permissions set to owner-only (0600)
4591
+ """
4592
+ try:
4593
+ from .cli_wizards.install_wizard import InstallWizard
4594
+
4595
+ wizard = InstallWizard(output_dir=Path(output_dir), skip_validation=skip_validation)
4596
+ success = wizard.run()
4597
+ sys.exit(0 if success else 1)
4598
+
4599
+ except Exception as e:
4600
+ click.echo(f"❌ Installation failed: {e}", err=True)
4601
+ sys.exit(1)
4602
+
4603
+
4463
4604
  @cli.command(name="discover-storypoint-fields")
4464
4605
  @click.option(
4465
4606
  "--config",
@@ -4672,16 +4813,41 @@ def identities(config: Path, weeks: int, apply: bool) -> None:
4672
4813
  # Show suggestions
4673
4814
  click.echo(f"\n⚠️ Found {len(identity_result.clusters)} potential identity clusters:")
4674
4815
 
4675
- # Display all mappings
4816
+ # Display all mappings with confidence scores
4676
4817
  if suggested_config.get("analysis", {}).get("manual_identity_mappings"):
4677
4818
  click.echo("\n📋 Suggested identity mappings:")
4678
- for mapping in suggested_config["analysis"]["manual_identity_mappings"]:
4679
- canonical = mapping["canonical_email"]
4819
+ for i, mapping in enumerate(
4820
+ suggested_config["analysis"]["manual_identity_mappings"], 1
4821
+ ):
4822
+ canonical = mapping["primary_email"]
4680
4823
  aliases = mapping.get("aliases", [])
4824
+ confidence = mapping.get("confidence", 0.0)
4825
+ reasoning = mapping.get("reasoning", "")
4826
+
4827
+ # Color-code based on confidence (90%+ threshold)
4828
+ if confidence >= 0.95:
4829
+ confidence_indicator = "🟢" # Very high confidence
4830
+ elif confidence >= 0.90:
4831
+ confidence_indicator = "🟡" # High confidence (above threshold)
4832
+ else:
4833
+ confidence_indicator = "🟠" # Medium confidence (below threshold)
4834
+
4681
4835
  if aliases:
4682
- click.echo(f" {canonical}")
4836
+ click.echo(
4837
+ f"\n {confidence_indicator} Cluster {i} "
4838
+ f"(Confidence: {confidence:.1%}):"
4839
+ )
4840
+ click.echo(f" Primary: {canonical}")
4683
4841
  for alias in aliases:
4684
- click.echo(f"{alias}")
4842
+ click.echo(f" Alias: {alias}")
4843
+
4844
+ # Show reasoning if available
4845
+ if reasoning:
4846
+ # Truncate reasoning for display
4847
+ display_reasoning = (
4848
+ reasoning if len(reasoning) <= 80 else reasoning[:77] + "..."
4849
+ )
4850
+ click.echo(f" Reason: {display_reasoning}")
4685
4851
 
4686
4852
  # Check for bot exclusions
4687
4853
  if suggested_config.get("exclude", {}).get("authors"):
@@ -4708,6 +4874,342 @@ def identities(config: Path, weeks: int, apply: bool) -> None:
4708
4874
  sys.exit(1)
4709
4875
 
4710
4876
 
4877
+ @cli.command(name="aliases")
4878
+ @click.option(
4879
+ "--config",
4880
+ "-c",
4881
+ type=click.Path(exists=True, path_type=Path),
4882
+ required=True,
4883
+ help="Path to configuration file",
4884
+ )
4885
+ @click.option(
4886
+ "--output",
4887
+ "-o",
4888
+ type=click.Path(path_type=Path),
4889
+ help="Output path for aliases.yaml (default: same dir as config)",
4890
+ )
4891
+ @click.option(
4892
+ "--confidence-threshold",
4893
+ type=float,
4894
+ default=0.9,
4895
+ help="Minimum confidence threshold for LLM matches (default: 0.9)",
4896
+ )
4897
+ @click.option(
4898
+ "--apply", is_flag=True, help="Automatically update config to use generated aliases file"
4899
+ )
4900
+ @click.option(
4901
+ "--weeks", type=int, default=12, help="Number of weeks of history to analyze (default: 12)"
4902
+ )
4903
+ def aliases_command(
4904
+ config: Path,
4905
+ output: Optional[Path],
4906
+ confidence_threshold: float,
4907
+ apply: bool,
4908
+ weeks: int,
4909
+ ) -> None:
4910
+ """Generate developer identity aliases using LLM analysis.
4911
+
4912
+ \b
4913
+ This command analyzes commit history and uses LLM to identify
4914
+ developer aliases (same person with different email addresses).
4915
+ Results are saved to aliases.yaml which can be shared across
4916
+ multiple config files.
4917
+
4918
+ \b
4919
+ EXAMPLES:
4920
+ # Generate aliases and review
4921
+ gitflow-analytics aliases -c config.yaml
4922
+
4923
+ # Generate and apply automatically
4924
+ gitflow-analytics aliases -c config.yaml --apply
4925
+
4926
+ # Save to specific location
4927
+ gitflow-analytics aliases -c config.yaml -o ~/shared/aliases.yaml
4928
+
4929
+ # Use longer history for better accuracy
4930
+ gitflow-analytics aliases -c config.yaml --weeks 24
4931
+
4932
+ \b
4933
+ CONFIGURATION:
4934
+ Aliases are saved to aliases.yaml and can be referenced in
4935
+ multiple config files for consistent identity resolution.
4936
+ """
4937
+ try:
4938
+ from .config.aliases import AliasesManager, DeveloperAlias
4939
+ from .identity_llm.analyzer import LLMIdentityAnalyzer
4940
+
4941
+ # Load configuration
4942
+ click.echo(f"\n📋 Loading configuration from {config}...")
4943
+ cfg = ConfigLoader.load(config)
4944
+
4945
+ # Determine output path
4946
+ if not output:
4947
+ output = config.parent / "aliases.yaml"
4948
+
4949
+ click.echo(f"🔍 Analyzing developer identities (last {weeks} weeks)")
4950
+ click.echo(f"📊 Confidence threshold: {confidence_threshold:.0%}")
4951
+ click.echo(f"💾 Output: {output}\n")
4952
+
4953
+ # Set up date range
4954
+ end_date = datetime.now(timezone.utc)
4955
+ start_date = end_date - timedelta(weeks=weeks)
4956
+
4957
+ # Analyze repositories to collect commits
4958
+ click.echo("📥 Fetching commit history...\n")
4959
+ cache = GitAnalysisCache(cfg.cache.directory)
4960
+
4961
+ # Prepare ML categorization config for analyzer
4962
+ ml_config = None
4963
+ if hasattr(cfg.analysis, "ml_categorization"):
4964
+ ml_config = {
4965
+ "enabled": cfg.analysis.ml_categorization.enabled,
4966
+ "min_confidence": cfg.analysis.ml_categorization.min_confidence,
4967
+ "semantic_weight": cfg.analysis.ml_categorization.semantic_weight,
4968
+ "file_pattern_weight": cfg.analysis.ml_categorization.file_pattern_weight,
4969
+ "hybrid_threshold": cfg.analysis.ml_categorization.hybrid_threshold,
4970
+ "cache_duration_days": cfg.analysis.ml_categorization.cache_duration_days,
4971
+ "batch_size": cfg.analysis.ml_categorization.batch_size,
4972
+ "enable_caching": cfg.analysis.ml_categorization.enable_caching,
4973
+ "spacy_model": cfg.analysis.ml_categorization.spacy_model,
4974
+ }
4975
+
4976
+ # LLM classification configuration
4977
+ llm_config = {
4978
+ "enabled": cfg.analysis.llm_classification.enabled,
4979
+ "api_key": cfg.analysis.llm_classification.api_key,
4980
+ "model": cfg.analysis.llm_classification.model,
4981
+ "confidence_threshold": cfg.analysis.llm_classification.confidence_threshold,
4982
+ "max_tokens": cfg.analysis.llm_classification.max_tokens,
4983
+ "temperature": cfg.analysis.llm_classification.temperature,
4984
+ "timeout_seconds": cfg.analysis.llm_classification.timeout_seconds,
4985
+ "cache_duration_days": cfg.analysis.llm_classification.cache_duration_days,
4986
+ "enable_caching": cfg.analysis.llm_classification.enable_caching,
4987
+ "max_daily_requests": cfg.analysis.llm_classification.max_daily_requests,
4988
+ "domain_terms": cfg.analysis.llm_classification.domain_terms,
4989
+ }
4990
+
4991
+ # Configure branch analysis
4992
+ branch_analysis_config = {
4993
+ "strategy": cfg.analysis.branch_analysis.strategy,
4994
+ "max_branches_per_repo": cfg.analysis.branch_analysis.max_branches_per_repo,
4995
+ "active_days_threshold": cfg.analysis.branch_analysis.active_days_threshold,
4996
+ "include_main_branches": cfg.analysis.branch_analysis.include_main_branches,
4997
+ "always_include_patterns": cfg.analysis.branch_analysis.always_include_patterns,
4998
+ "always_exclude_patterns": cfg.analysis.branch_analysis.always_exclude_patterns,
4999
+ "enable_progress_logging": cfg.analysis.branch_analysis.enable_progress_logging,
5000
+ "branch_commit_limit": cfg.analysis.branch_analysis.branch_commit_limit,
5001
+ }
5002
+
5003
+ analyzer = GitAnalyzer(
5004
+ cache,
5005
+ branch_mapping_rules=cfg.analysis.branch_mapping_rules,
5006
+ allowed_ticket_platforms=getattr(
5007
+ cfg.analysis, "ticket_platforms", ["jira", "github", "clickup", "linear"]
5008
+ ),
5009
+ exclude_paths=cfg.analysis.exclude_paths,
5010
+ story_point_patterns=cfg.analysis.story_point_patterns,
5011
+ ml_categorization_config=ml_config,
5012
+ llm_config=llm_config,
5013
+ branch_analysis_config=branch_analysis_config,
5014
+ )
5015
+
5016
+ all_commits = []
5017
+
5018
+ # Get repositories to analyze
5019
+ repositories = cfg.repositories if cfg.repositories else []
5020
+
5021
+ if not repositories:
5022
+ click.echo("❌ No repositories configured", err=True)
5023
+ sys.exit(1)
5024
+
5025
+ # Collect commits from all repositories
5026
+ with click.progressbar(
5027
+ repositories,
5028
+ label="Analyzing repositories",
5029
+ item_show_func=lambda r: r.name if r else "",
5030
+ ) as repos:
5031
+ for repo_config in repos:
5032
+ try:
5033
+ if not repo_config.path.exists():
5034
+ continue
5035
+
5036
+ # Fetch commits
5037
+ repo_commits = analyzer.analyze_repository(
5038
+ repo_config.path, start_date=start_date, branch=repo_config.branch
5039
+ )
5040
+
5041
+ if repo_commits:
5042
+ all_commits.extend(repo_commits)
5043
+
5044
+ except Exception as e:
5045
+ click.echo(f"\n⚠️ Warning: Failed to analyze repository: {e}", err=True)
5046
+ continue
5047
+
5048
+ click.echo(f"\n✅ Collected {len(all_commits)} commits\n")
5049
+
5050
+ if not all_commits:
5051
+ click.echo("❌ No commits found to analyze", err=True)
5052
+ sys.exit(1)
5053
+
5054
+ # Initialize LLM identity analyzer
5055
+ click.echo("🤖 Running LLM identity analysis...\n")
5056
+
5057
+ # Get OpenRouter API key from config
5058
+ api_key = None
5059
+ if cfg.chatgpt and cfg.chatgpt.api_key:
5060
+ # Resolve environment variable if needed
5061
+ api_key_value = cfg.chatgpt.api_key
5062
+ if api_key_value.startswith("${") and api_key_value.endswith("}"):
5063
+ var_name = api_key_value[2:-1]
5064
+ api_key = os.getenv(var_name)
5065
+ else:
5066
+ api_key = api_key_value
5067
+
5068
+ if not api_key:
5069
+ click.echo(
5070
+ "⚠️ No OpenRouter API key configured - using heuristic analysis only", err=True
5071
+ )
5072
+
5073
+ llm_analyzer = LLMIdentityAnalyzer(
5074
+ api_key=api_key, confidence_threshold=confidence_threshold
5075
+ )
5076
+
5077
+ # Run analysis
5078
+ result = llm_analyzer.analyze_identities(all_commits)
5079
+
5080
+ click.echo("✅ Analysis complete:")
5081
+ click.echo(f" - Found {len(result.clusters)} identity clusters")
5082
+ click.echo(f" - {len(result.unresolved_identities)} unresolved identities")
5083
+ click.echo(f" - Method: {result.analysis_metadata.get('analysis_method', 'unknown')}\n")
5084
+
5085
+ # Create aliases manager and add clusters
5086
+ aliases_mgr = AliasesManager(output)
5087
+
5088
+ # Load existing aliases if file exists
5089
+ if output.exists():
5090
+ click.echo(f"📂 Loading existing aliases from {output}...")
5091
+ aliases_mgr.load()
5092
+ existing_count = len(aliases_mgr.aliases)
5093
+ click.echo(f" Found {existing_count} existing aliases\n")
5094
+
5095
+ # Add new clusters
5096
+ new_count = 0
5097
+ updated_count = 0
5098
+
5099
+ for cluster in result.clusters:
5100
+ # Check if this is a new or updated alias
5101
+ existing = aliases_mgr.get_alias(cluster.canonical_email)
5102
+
5103
+ alias = DeveloperAlias(
5104
+ name=cluster.preferred_display_name or cluster.canonical_name,
5105
+ primary_email=cluster.canonical_email,
5106
+ aliases=[a.email for a in cluster.aliases],
5107
+ confidence=cluster.confidence,
5108
+ reasoning=(
5109
+ cluster.reasoning[:200] if cluster.reasoning else ""
5110
+ ), # Truncate for readability
5111
+ )
5112
+
5113
+ if existing:
5114
+ updated_count += 1
5115
+ else:
5116
+ new_count += 1
5117
+
5118
+ aliases_mgr.add_alias(alias)
5119
+
5120
+ # Save aliases
5121
+ click.echo("💾 Saving aliases...\n")
5122
+ aliases_mgr.save()
5123
+
5124
+ click.echo(f"✅ Saved to {output}")
5125
+ click.echo(f" - New aliases: {new_count}")
5126
+ click.echo(f" - Updated aliases: {updated_count}")
5127
+ click.echo(f" - Total aliases: {len(aliases_mgr.aliases)}\n")
5128
+
5129
+ # Display summary
5130
+ if aliases_mgr.aliases:
5131
+ click.echo("📋 Generated Aliases:\n")
5132
+
5133
+ for alias in sorted(aliases_mgr.aliases, key=lambda a: a.primary_email):
5134
+ name_display = (
5135
+ f"{alias.name} <{alias.primary_email}>" if alias.name else alias.primary_email
5136
+ )
5137
+ click.echo(f" • {name_display}")
5138
+
5139
+ if alias.aliases:
5140
+ for alias_email in alias.aliases:
5141
+ click.echo(f" → {alias_email}")
5142
+
5143
+ if alias.confidence < 1.0:
5144
+ confidence_color = (
5145
+ "green"
5146
+ if alias.confidence >= 0.9
5147
+ else "yellow" if alias.confidence >= 0.8 else "red"
5148
+ )
5149
+ click.echo(" Confidence: ", nl=False)
5150
+ click.secho(f"{alias.confidence:.0%}", fg=confidence_color)
5151
+
5152
+ click.echo() # Blank line between aliases
5153
+
5154
+ # Apply to config if requested
5155
+ if apply:
5156
+ click.echo(f"🔄 Updating {config} to reference aliases file...\n")
5157
+
5158
+ # Read current config
5159
+ with open(config) as f:
5160
+ config_data = yaml.safe_load(f)
5161
+
5162
+ # Ensure analysis section exists
5163
+ if "analysis" not in config_data:
5164
+ config_data["analysis"] = {}
5165
+
5166
+ if "identity" not in config_data["analysis"]:
5167
+ config_data["analysis"]["identity"] = {}
5168
+
5169
+ # Calculate relative path from config to aliases file
5170
+ try:
5171
+ rel_path = output.relative_to(config.parent)
5172
+ config_data["analysis"]["identity"]["aliases_file"] = str(rel_path)
5173
+ except ValueError:
5174
+ # Not relative, use absolute
5175
+ config_data["analysis"]["identity"]["aliases_file"] = str(output)
5176
+
5177
+ # Remove manual_mappings if present (now in aliases file)
5178
+ if "manual_identity_mappings" in config_data["analysis"].get("identity", {}):
5179
+ del config_data["analysis"]["identity"]["manual_identity_mappings"]
5180
+ click.echo(" Removed inline manual_identity_mappings (now in aliases file)")
5181
+
5182
+ # Save updated config
5183
+ with open(config, "w") as f:
5184
+ yaml.dump(config_data, f, default_flow_style=False, sort_keys=False)
5185
+
5186
+ click.echo(f"✅ Updated {config}")
5187
+ click.echo(
5188
+ f" Added: analysis.identity.aliases_file = "
5189
+ f"{config_data['analysis']['identity']['aliases_file']}\n"
5190
+ )
5191
+
5192
+ # Summary and next steps
5193
+ click.echo("✨ Identity alias generation complete!\n")
5194
+
5195
+ if not apply:
5196
+ click.echo("💡 Next steps:")
5197
+ click.echo(f" 1. Review the aliases in {output}")
5198
+ click.echo(" 2. Update your config.yaml to reference the aliases file:")
5199
+ click.echo(" analysis:")
5200
+ click.echo(" identity:")
5201
+ click.echo(f" aliases_file: {output.name}")
5202
+ click.echo(" 3. Or run with --apply flag to update automatically\n")
5203
+
5204
+ except Exception as e:
5205
+ click.echo(f"\n❌ Error generating aliases: {e}", err=True)
5206
+ import traceback
5207
+
5208
+ if os.getenv("GITFLOW_DEBUG"):
5209
+ traceback.print_exc()
5210
+ sys.exit(1)
5211
+
5212
+
4711
5213
  @cli.command()
4712
5214
  @click.option(
4713
5215
  "--config",
@@ -0,0 +1,10 @@
1
+ """CLI subpackage for GitFlow Analytics.
2
+
3
+ This package contains CLI-related modules including the installation wizard
4
+ and interactive launcher.
5
+ """
6
+
7
+ from .install_wizard import InstallWizard
8
+ from .run_launcher import InteractiveLauncher, run_interactive_launcher
9
+
10
+ __all__ = ["InstallWizard", "InteractiveLauncher", "run_interactive_launcher"]