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.
- gitflow_analytics/_version.py +1 -1
- gitflow_analytics/cli.py +517 -15
- gitflow_analytics/cli_wizards/__init__.py +10 -0
- gitflow_analytics/cli_wizards/install_wizard.py +1181 -0
- gitflow_analytics/cli_wizards/run_launcher.py +433 -0
- gitflow_analytics/config/__init__.py +3 -0
- gitflow_analytics/config/aliases.py +306 -0
- gitflow_analytics/config/loader.py +35 -1
- gitflow_analytics/config/schema.py +13 -0
- gitflow_analytics/constants.py +75 -0
- gitflow_analytics/core/cache.py +7 -3
- gitflow_analytics/core/data_fetcher.py +66 -30
- gitflow_analytics/core/git_timeout_wrapper.py +6 -4
- gitflow_analytics/core/progress.py +2 -4
- gitflow_analytics/core/subprocess_git.py +31 -5
- gitflow_analytics/identity_llm/analysis_pass.py +13 -3
- gitflow_analytics/identity_llm/analyzer.py +14 -2
- gitflow_analytics/identity_llm/models.py +7 -1
- gitflow_analytics/qualitative/classifiers/llm/openai_client.py +5 -3
- gitflow_analytics/security/config.py +6 -6
- gitflow_analytics/security/extractors/dependency_checker.py +14 -14
- gitflow_analytics/security/extractors/secret_detector.py +8 -14
- gitflow_analytics/security/extractors/vulnerability_scanner.py +9 -9
- gitflow_analytics/security/llm_analyzer.py +10 -10
- gitflow_analytics/security/security_analyzer.py +17 -17
- gitflow_analytics/tui/screens/analysis_progress_screen.py +1 -1
- gitflow_analytics/ui/progress_display.py +36 -29
- gitflow_analytics/verify_activity.py +23 -26
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/METADATA +1 -1
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/RECORD +34 -31
- gitflow_analytics/security/reports/__init__.py +0 -5
- gitflow_analytics/security/reports/security_report.py +0 -358
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/WHEEL +0 -0
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/top_level.txt +0 -0
gitflow_analytics/_version.py
CHANGED
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(
|
|
52
|
-
|
|
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
|
|
297
|
+
import importlib.util
|
|
285
298
|
|
|
299
|
+
textual_spec = importlib.util.find_spec("textual")
|
|
286
300
|
# TUI is available - route to TUI
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
4679
|
-
|
|
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(
|
|
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"
|
|
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"]
|