gitflow-analytics 3.13.0__py3-none-any.whl → 3.13.5__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.
@@ -1,4 +1,4 @@
1
1
  """Version information for gitflow-analytics."""
2
2
 
3
- __version__ = "3.13.0"
3
+ __version__ = "3.13.5"
4
4
  __version_info__ = tuple(int(x) for x in __version__.split("."))
gitflow_analytics/cli.py CHANGED
@@ -429,8 +429,7 @@ def cli(ctx: click.Context) -> None:
429
429
  "--use-batch-classification/--use-legacy-classification",
430
430
  default=True,
431
431
  help=(
432
- "Use batch LLM classification on pre-fetched data (Step 2 of 2) - "
433
- "now the default behavior"
432
+ "Use batch LLM classification on pre-fetched data (Step 2 of 2) - now the default behavior"
434
433
  ),
435
434
  )
436
435
  @click.option(
@@ -1213,8 +1212,7 @@ def analyze(
1213
1212
  display.update_progress_task(
1214
1213
  "main",
1215
1214
  description=(
1216
- f"🔍 Discovering repositories from organization: "
1217
- f"{cfg.github.organization}"
1215
+ f"🔍 Discovering repositories from organization: {cfg.github.organization}"
1218
1216
  ),
1219
1217
  completed=15,
1220
1218
  )
@@ -1457,8 +1455,7 @@ def analyze(
1457
1455
  display.update_progress_task(
1458
1456
  "repos",
1459
1457
  description=(
1460
- f"Step 1: Fetching data for "
1461
- f"{len(repos_needing_analysis)} repositories"
1458
+ f"Step 1: Fetching data for {len(repos_needing_analysis)} repositories"
1462
1459
  ),
1463
1460
  completed=0,
1464
1461
  )
@@ -1532,7 +1529,6 @@ def analyze(
1532
1529
  description="Processing repositories",
1533
1530
  unit="repos",
1534
1531
  ) as repos_progress_ctx:
1535
-
1536
1532
  for idx, repo_config in enumerate(repos_needing_analysis, 1):
1537
1533
  try:
1538
1534
  repo_path = Path(repo_config.path)
@@ -2285,11 +2281,9 @@ def analyze(
2285
2281
  f" 🗃️ Database state: {pre_classification_commits} commits, "
2286
2282
  f"{pre_classification_batches} batches"
2287
2283
  )
2288
- click.echo(
2289
- " 💡 Commits exist but no daily batches - " "batch creation failed"
2290
- )
2284
+ click.echo(" 💡 Commits exist but no daily batches - batch creation failed")
2291
2285
  raise click.ClickException(
2292
- "No batches available for classification - " "batch creation process failed"
2286
+ "No batches available for classification - batch creation process failed"
2293
2287
  )
2294
2288
 
2295
2289
  if display:
@@ -2717,7 +2711,9 @@ def analyze(
2717
2711
  )
2718
2712
  commit["canonical_id"] = canonical_id
2719
2713
  # Also add canonical display name for reports
2720
- commit["canonical_name"] = identity_resolver.get_canonical_name(canonical_id)
2714
+ commit["canonical_name"] = identity_resolver.get_canonical_name(
2715
+ canonical_id
2716
+ )
2721
2717
 
2722
2718
  all_commits.extend(commits)
2723
2719
  if display:
@@ -2934,9 +2930,9 @@ def analyze(
2934
2930
  ):
2935
2931
  existing_mappings.append(new_mapping)
2936
2932
 
2937
- config_data["analysis"]["identity"][
2938
- "manual_mappings"
2939
- ] = existing_mappings
2933
+ config_data["analysis"]["identity"]["manual_mappings"] = (
2934
+ existing_mappings
2935
+ )
2940
2936
 
2941
2937
  # Apply bot exclusions
2942
2938
  if suggested_config.get("exclude", {}).get("authors"):
@@ -3382,7 +3378,7 @@ def analyze(
3382
3378
  # Weekly metrics report (only if CSV generation is enabled)
3383
3379
  if generate_csv:
3384
3380
  weekly_report = (
3385
- output / f'weekly_metrics_{datetime.now(timezone.utc).strftime("%Y%m%d")}.csv'
3381
+ output / f"weekly_metrics_{datetime.now(timezone.utc).strftime('%Y%m%d')}.csv"
3386
3382
  )
3387
3383
  try:
3388
3384
  logger.debug("Starting weekly metrics report generation")
@@ -3402,7 +3398,7 @@ def analyze(
3402
3398
  if generate_csv:
3403
3399
  activity_summary_report = (
3404
3400
  output
3405
- / f'developer_activity_summary_{datetime.now(timezone.utc).strftime("%Y%m%d")}.csv'
3401
+ / f"developer_activity_summary_{datetime.now(timezone.utc).strftime('%Y%m%d')}.csv"
3406
3402
  )
3407
3403
  try:
3408
3404
  logger.debug("Starting developer activity summary report generation")
@@ -3429,7 +3425,7 @@ def analyze(
3429
3425
 
3430
3426
  # Summary report (only if CSV generation is enabled)
3431
3427
  if generate_csv:
3432
- summary_report = output / f'summary_{datetime.now().strftime("%Y%m%d")}.csv'
3428
+ summary_report = output / f"summary_{datetime.now().strftime('%Y%m%d')}.csv"
3433
3429
  try:
3434
3430
  report_gen.generate_summary_report(
3435
3431
  all_commits,
@@ -3453,7 +3449,7 @@ def analyze(
3453
3449
 
3454
3450
  # Developer report (only if CSV generation is enabled)
3455
3451
  if generate_csv:
3456
- developer_report = output / f'developers_{datetime.now().strftime("%Y%m%d")}.csv'
3452
+ developer_report = output / f"developers_{datetime.now().strftime('%Y%m%d')}.csv"
3457
3453
  try:
3458
3454
  report_gen.generate_developer_report(developer_stats, developer_report)
3459
3455
  generated_reports.append(developer_report.name)
@@ -3471,7 +3467,7 @@ def analyze(
3471
3467
  # Untracked commits report (only if CSV generation is enabled)
3472
3468
  if generate_csv:
3473
3469
  untracked_commits_report = (
3474
- output / f'untracked_commits_{datetime.now().strftime("%Y%m%d")}.csv'
3470
+ output / f"untracked_commits_{datetime.now().strftime('%Y%m%d')}.csv"
3475
3471
  )
3476
3472
  try:
3477
3473
  report_gen.generate_untracked_commits_report(
@@ -3492,7 +3488,7 @@ def analyze(
3492
3488
  # Weekly Categorization report (only if CSV generation is enabled)
3493
3489
  if generate_csv:
3494
3490
  weekly_categorization_report = (
3495
- output / f'weekly_categorization_{datetime.now().strftime("%Y%m%d")}.csv'
3491
+ output / f"weekly_categorization_{datetime.now().strftime('%Y%m%d')}.csv"
3496
3492
  )
3497
3493
  try:
3498
3494
  logger.debug("Starting weekly categorization report generation")
@@ -3510,7 +3506,7 @@ def analyze(
3510
3506
  # PM Correlations report (if PM data is available and CSV generation is enabled)
3511
3507
  if aggregated_pm_data and generate_csv:
3512
3508
  pm_correlations_report = (
3513
- output / f'pm_correlations_{datetime.now().strftime("%Y%m%d")}.csv'
3509
+ output / f"pm_correlations_{datetime.now().strftime('%Y%m%d')}.csv"
3514
3510
  )
3515
3511
  try:
3516
3512
  report_gen.generate_pm_correlations_report(
@@ -3525,7 +3521,7 @@ def analyze(
3525
3521
  # Story Point Correlation report (only if CSV generation is enabled)
3526
3522
  if generate_csv:
3527
3523
  story_point_correlation_report = (
3528
- output / f'story_point_correlation_{datetime.now().strftime("%Y%m%d")}.csv'
3524
+ output / f"story_point_correlation_{datetime.now().strftime('%Y%m%d')}.csv"
3529
3525
  )
3530
3526
  try:
3531
3527
  logger.debug("Starting story point correlation report generation")
@@ -3541,7 +3537,7 @@ def analyze(
3541
3537
  click.echo(f" ⚠️ Warning: Story point correlation report failed: {e}")
3542
3538
 
3543
3539
  # Activity distribution report (always generate data, optionally write CSV)
3544
- activity_report = output / f'activity_distribution_{datetime.now().strftime("%Y%m%d")}.csv'
3540
+ activity_report = output / f"activity_distribution_{datetime.now().strftime('%Y%m%d')}.csv"
3545
3541
  try:
3546
3542
  logger.debug("Starting activity distribution report generation")
3547
3543
  analytics_gen.generate_activity_distribution_report(
@@ -3565,7 +3561,7 @@ def analyze(
3565
3561
  raise
3566
3562
 
3567
3563
  # Developer focus report (always generate data, optionally write CSV)
3568
- focus_report = output / f'developer_focus_{datetime.now().strftime("%Y%m%d")}.csv'
3564
+ focus_report = output / f"developer_focus_{datetime.now().strftime('%Y%m%d')}.csv"
3569
3565
  try:
3570
3566
  logger.debug("Starting developer focus report generation")
3571
3567
  analytics_gen.generate_developer_focus_report(
@@ -3589,7 +3585,7 @@ def analyze(
3589
3585
  raise
3590
3586
 
3591
3587
  # Qualitative insights report (always generate data, optionally write CSV)
3592
- insights_report = output / f'qualitative_insights_{datetime.now().strftime("%Y%m%d")}.csv'
3588
+ insights_report = output / f"qualitative_insights_{datetime.now().strftime('%Y%m%d')}.csv"
3593
3589
  try:
3594
3590
  logger.debug("Starting qualitative insights report generation")
3595
3591
  analytics_gen.generate_qualitative_insights_report(
@@ -3609,7 +3605,7 @@ def analyze(
3609
3605
 
3610
3606
  branch_health_gen = BranchHealthReportGenerator()
3611
3607
 
3612
- branch_health_report = output / f'branch_health_{datetime.now().strftime("%Y%m%d")}.csv'
3608
+ branch_health_report = output / f"branch_health_{datetime.now().strftime('%Y%m%d')}.csv"
3613
3609
  try:
3614
3610
  logger.debug("Starting branch health report generation")
3615
3611
  branch_health_gen.generate_csv_report(branch_health_metrics, branch_health_report)
@@ -3623,7 +3619,7 @@ def analyze(
3623
3619
 
3624
3620
  # Detailed branch report
3625
3621
  detailed_branch_report = (
3626
- output / f'branch_details_{datetime.now().strftime("%Y%m%d")}.csv'
3622
+ output / f"branch_details_{datetime.now().strftime('%Y%m%d')}.csv"
3627
3623
  )
3628
3624
  try:
3629
3625
  branch_health_gen.generate_detailed_branch_report(
@@ -3663,7 +3659,7 @@ def analyze(
3663
3659
 
3664
3660
  # Weekly trends report (includes developer and project trends) (only if CSV generation is enabled)
3665
3661
  if generate_csv:
3666
- trends_report = output / f'weekly_trends_{datetime.now().strftime("%Y%m%d")}.csv'
3662
+ trends_report = output / f"weekly_trends_{datetime.now().strftime('%Y%m%d')}.csv"
3667
3663
  try:
3668
3664
  logger.debug("Starting weekly trends report generation")
3669
3665
  analytics_gen.generate_weekly_trends_report(
@@ -3739,7 +3735,7 @@ def analyze(
3739
3735
  # Weekly velocity report (only if CSV generation is enabled)
3740
3736
  if generate_csv:
3741
3737
  weekly_velocity_report = (
3742
- output / f'weekly_velocity_{datetime.now().strftime("%Y%m%d")}.csv'
3738
+ output / f"weekly_velocity_{datetime.now().strftime('%Y%m%d')}.csv"
3743
3739
  )
3744
3740
  try:
3745
3741
  logger.debug("Starting weekly velocity report generation")
@@ -3765,7 +3761,7 @@ def analyze(
3765
3761
  # Weekly DORA metrics report (only if CSV generation is enabled)
3766
3762
  if generate_csv:
3767
3763
  weekly_dora_report = (
3768
- output / f'weekly_dora_metrics_{datetime.now().strftime("%Y%m%d")}.csv'
3764
+ output / f"weekly_dora_metrics_{datetime.now().strftime('%Y%m%d')}.csv"
3769
3765
  )
3770
3766
  try:
3771
3767
  logger.debug("Starting weekly DORA metrics report generation")
@@ -3983,7 +3979,7 @@ def analyze(
3983
3979
  logger.debug("Starting comprehensive JSON export generation")
3984
3980
  click.echo(" 🔄 Generating comprehensive JSON export...")
3985
3981
  json_report = (
3986
- output / f'comprehensive_export_{datetime.now().strftime("%Y%m%d")}.json'
3982
+ output / f"comprehensive_export_{datetime.now().strftime('%Y%m%d')}.json"
3987
3983
  )
3988
3984
 
3989
3985
  # Initialize comprehensive JSON exporter
@@ -5178,7 +5174,7 @@ def identities(config: Path, weeks: int, apply: bool) -> None:
5178
5174
 
5179
5175
  # Run analysis
5180
5176
  identity_report_path = (
5181
- cfg.cache.directory / f'identity_analysis_{datetime.now().strftime("%Y%m%d")}.yaml'
5177
+ cfg.cache.directory / f"identity_analysis_{datetime.now().strftime('%Y%m%d')}.yaml"
5182
5178
  )
5183
5179
  identity_result = analysis_pass.run_analysis(
5184
5180
  all_commits, output_path=identity_report_path, apply_to_config=False
@@ -5525,7 +5521,9 @@ def aliases_command(
5525
5521
  confidence_color = (
5526
5522
  "green"
5527
5523
  if alias.confidence >= 0.9
5528
- else "yellow" if alias.confidence >= 0.8 else "red"
5524
+ else "yellow"
5525
+ if alias.confidence >= 0.8
5526
+ else "red"
5529
5527
  )
5530
5528
  click.echo(" Confidence: ", nl=False)
5531
5529
  click.secho(f"{alias.confidence:.0%}", fg=confidence_color)
@@ -5941,12 +5939,10 @@ def create_alias_interactive(config: Path, output: Optional[Path]) -> None:
5941
5939
  )
5942
5940
  @click.option(
5943
5941
  "--old-name",
5944
- required=True,
5945
5942
  help="Current canonical name to rename (must match a name in manual_mappings)",
5946
5943
  )
5947
5944
  @click.option(
5948
5945
  "--new-name",
5949
- required=True,
5950
5946
  help="New canonical display name to use in reports",
5951
5947
  )
5952
5948
  @click.option(
@@ -5959,12 +5955,19 @@ def create_alias_interactive(config: Path, output: Optional[Path]) -> None:
5959
5955
  is_flag=True,
5960
5956
  help="Show what would be changed without applying changes",
5961
5957
  )
5958
+ @click.option(
5959
+ "--interactive",
5960
+ "-i",
5961
+ is_flag=True,
5962
+ help="Interactive mode: select developer from numbered list",
5963
+ )
5962
5964
  def alias_rename(
5963
5965
  config: Path,
5964
5966
  old_name: str,
5965
5967
  new_name: str,
5966
5968
  update_cache: bool,
5967
5969
  dry_run: bool,
5970
+ interactive: bool,
5968
5971
  ) -> None:
5969
5972
  """Rename a developer's canonical display name.
5970
5973
 
@@ -5975,6 +5978,9 @@ def alias_rename(
5975
5978
 
5976
5979
  \b
5977
5980
  EXAMPLES:
5981
+ # Interactive mode: select from numbered list
5982
+ gitflow-analytics alias-rename -c config.yaml --interactive
5983
+
5978
5984
  # Rename with dry-run to see changes
5979
5985
  gitflow-analytics alias-rename -c config.yaml \\
5980
5986
  --old-name "bianco-zaelot" \\
@@ -6001,27 +6007,11 @@ def alias_rename(
6001
6007
  try:
6002
6008
  from .core.identity import DeveloperIdentityResolver
6003
6009
 
6004
- # Validate inputs
6005
- if not old_name.strip():
6006
- click.echo("❌ Error: --old-name cannot be empty", err=True)
6007
- sys.exit(1)
6008
-
6009
- if not new_name.strip():
6010
- click.echo("❌ Error: --new-name cannot be empty", err=True)
6011
- sys.exit(1)
6012
-
6013
- old_name = old_name.strip()
6014
- new_name = new_name.strip()
6015
-
6016
- if old_name == new_name:
6017
- click.echo("❌ Error: old-name and new-name are identical", err=True)
6018
- sys.exit(1)
6019
-
6020
6010
  # Load the YAML config file
6021
6011
  click.echo(f"\n📋 Loading configuration from {config}...")
6022
6012
 
6023
6013
  try:
6024
- with open(config, "r", encoding="utf-8") as f:
6014
+ with open(config, encoding="utf-8") as f:
6025
6015
  config_data = yaml.safe_load(f)
6026
6016
  except Exception as e:
6027
6017
  click.echo(f"❌ Error loading config file: {e}", err=True)
@@ -6037,7 +6027,9 @@ def alias_rename(
6037
6027
  sys.exit(1)
6038
6028
 
6039
6029
  if "manual_mappings" not in config_data["analysis"]["identity"]:
6040
- click.echo("❌ Error: 'analysis.identity.manual_mappings' not found in config", err=True)
6030
+ click.echo(
6031
+ "❌ Error: 'analysis.identity.manual_mappings' not found in config", err=True
6032
+ )
6041
6033
  sys.exit(1)
6042
6034
 
6043
6035
  manual_mappings = config_data["analysis"]["identity"]["manual_mappings"]
@@ -6046,6 +6038,62 @@ def alias_rename(
6046
6038
  click.echo("❌ Error: manual_mappings is empty", err=True)
6047
6039
  sys.exit(1)
6048
6040
 
6041
+ # Interactive mode: display numbered list and prompt for selection
6042
+ if interactive or not old_name or not new_name:
6043
+ click.echo("\n" + "=" * 60)
6044
+ click.echo(click.style("Current Developers:", fg="cyan", bold=True))
6045
+ click.echo("=" * 60 + "\n")
6046
+
6047
+ developer_names = []
6048
+ for idx, mapping in enumerate(manual_mappings, 1):
6049
+ name = mapping.get("name", "Unknown")
6050
+ email = mapping.get("primary_email", "N/A")
6051
+ alias_count = len(mapping.get("aliases", []))
6052
+
6053
+ developer_names.append(name)
6054
+ click.echo(f" {idx}. {click.style(name, fg='green')}")
6055
+ click.echo(f" Email: {email}")
6056
+ click.echo(f" Aliases: {alias_count} email(s)")
6057
+ click.echo()
6058
+
6059
+ # Prompt for selection
6060
+ try:
6061
+ selection = click.prompt(
6062
+ "Select developer number to rename (or 0 to cancel)",
6063
+ type=click.IntRange(0, len(developer_names)),
6064
+ )
6065
+ except click.Abort:
6066
+ click.echo("\n👋 Cancelled by user.")
6067
+ sys.exit(0)
6068
+
6069
+ if selection == 0:
6070
+ click.echo("\n👋 Cancelled.")
6071
+ sys.exit(0)
6072
+
6073
+ # Get selected developer name
6074
+ old_name = developer_names[selection - 1]
6075
+ click.echo(f"\n📝 Selected: {click.style(old_name, fg='green')}")
6076
+
6077
+ # Prompt for new name if not provided
6078
+ if not new_name:
6079
+ new_name = click.prompt("Enter new canonical name", type=str)
6080
+
6081
+ # Validate inputs
6082
+ if not old_name or not old_name.strip():
6083
+ click.echo("❌ Error: --old-name cannot be empty", err=True)
6084
+ sys.exit(1)
6085
+
6086
+ if not new_name or not new_name.strip():
6087
+ click.echo("❌ Error: --new-name cannot be empty", err=True)
6088
+ sys.exit(1)
6089
+
6090
+ old_name = old_name.strip()
6091
+ new_name = new_name.strip()
6092
+
6093
+ if old_name == new_name:
6094
+ click.echo("❌ Error: old-name and new-name are identical", err=True)
6095
+ sys.exit(1)
6096
+
6049
6097
  # Find the matching entry
6050
6098
  matching_entry = None
6051
6099
  matching_index = None
@@ -6065,24 +6113,30 @@ def alias_rename(
6065
6113
  sys.exit(1)
6066
6114
 
6067
6115
  # Display what will be changed
6068
- click.echo(f"\n🔍 Found matching entry:")
6116
+ click.echo("\n🔍 Found matching entry:")
6069
6117
  click.echo(f" Current name: {old_name}")
6070
6118
  click.echo(f" New name: {new_name}")
6071
6119
  click.echo(f" Email: {matching_entry.get('primary_email', 'N/A')}")
6072
6120
  click.echo(f" Aliases: {len(matching_entry.get('aliases', []))} email(s)")
6073
6121
 
6074
6122
  if dry_run:
6075
- click.echo(f"\n🔎 DRY RUN - No changes will be made")
6123
+ click.echo("\n🔎 DRY RUN - No changes will be made")
6076
6124
 
6077
6125
  # Update the config file
6078
6126
  if not dry_run:
6079
- click.echo(f"\n📝 Updating configuration file...")
6127
+ click.echo("\n📝 Updating configuration file...")
6080
6128
  manual_mappings[matching_index]["name"] = new_name
6081
6129
 
6082
6130
  try:
6083
6131
  with open(config, "w", encoding="utf-8") as f:
6084
- yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
6085
- click.echo(f"✅ Configuration file updated")
6132
+ yaml.dump(
6133
+ config_data,
6134
+ f,
6135
+ default_flow_style=False,
6136
+ allow_unicode=True,
6137
+ sort_keys=False,
6138
+ )
6139
+ click.echo("✅ Configuration file updated")
6086
6140
  except Exception as e:
6087
6141
  click.echo(f"❌ Error writing config file: {e}", err=True)
6088
6142
  sys.exit(1)
@@ -6091,7 +6145,7 @@ def alias_rename(
6091
6145
 
6092
6146
  # Update database cache if requested
6093
6147
  if update_cache:
6094
- click.echo(f"\n💾 Checking database cache...")
6148
+ click.echo("\n💾 Checking database cache...")
6095
6149
 
6096
6150
  # Load config to get cache directory
6097
6151
  cfg = ConfigLoader.load(config)
@@ -6099,7 +6153,7 @@ def alias_rename(
6099
6153
 
6100
6154
  if not identity_db_path.exists():
6101
6155
  click.echo(f"⚠️ Warning: Identity database not found at {identity_db_path}")
6102
- click.echo(f" Skipping cache update")
6156
+ click.echo(" Skipping cache update")
6103
6157
  else:
6104
6158
  # Initialize identity resolver to access database
6105
6159
  identity_resolver = DeveloperIdentityResolver(
@@ -6113,15 +6167,17 @@ def alias_rename(
6113
6167
  with identity_resolver.get_session() as session:
6114
6168
  # Count developer_identities records
6115
6169
  result = session.execute(
6116
- text("SELECT COUNT(*) FROM developer_identities WHERE primary_name = :old_name"),
6117
- {"old_name": old_name}
6170
+ text(
6171
+ "SELECT COUNT(*) FROM developer_identities WHERE primary_name = :old_name"
6172
+ ),
6173
+ {"old_name": old_name},
6118
6174
  )
6119
6175
  identity_count = result.scalar()
6120
6176
 
6121
6177
  # Count developer_aliases records
6122
6178
  result = session.execute(
6123
6179
  text("SELECT COUNT(*) FROM developer_aliases WHERE name = :old_name"),
6124
- {"old_name": old_name}
6180
+ {"old_name": old_name},
6125
6181
  )
6126
6182
  alias_count = result.scalar()
6127
6183
 
@@ -6129,28 +6185,34 @@ def alias_rename(
6129
6185
  click.echo(f" Found {alias_count} alias record(s)")
6130
6186
 
6131
6187
  if identity_count == 0 and alias_count == 0:
6132
- click.echo(f" ℹ️ No database records to update")
6188
+ click.echo(" ℹ️ No database records to update")
6133
6189
  elif not dry_run:
6134
- click.echo(f" Updating database records...")
6190
+ click.echo(" Updating database records...")
6135
6191
 
6136
6192
  with identity_resolver.get_session() as session:
6137
6193
  # Update developer_identities
6138
6194
  if identity_count > 0:
6139
6195
  session.execute(
6140
- text("UPDATE developer_identities SET primary_name = :new_name WHERE primary_name = :old_name"),
6141
- {"new_name": new_name, "old_name": old_name}
6196
+ text(
6197
+ "UPDATE developer_identities SET primary_name = :new_name WHERE primary_name = :old_name"
6198
+ ),
6199
+ {"new_name": new_name, "old_name": old_name},
6142
6200
  )
6143
6201
 
6144
6202
  # Update developer_aliases
6145
6203
  if alias_count > 0:
6146
6204
  session.execute(
6147
- text("UPDATE developer_aliases SET name = :new_name WHERE name = :old_name"),
6148
- {"new_name": new_name, "old_name": old_name}
6205
+ text(
6206
+ "UPDATE developer_aliases SET name = :new_name WHERE name = :old_name"
6207
+ ),
6208
+ {"new_name": new_name, "old_name": old_name},
6149
6209
  )
6150
6210
 
6151
- click.echo(f" ✅ Database updated")
6211
+ click.echo(" ✅ Database updated")
6152
6212
  else:
6153
- click.echo(f" [Would update {identity_count + alias_count} database record(s)]")
6213
+ click.echo(
6214
+ f" [Would update {identity_count + alias_count} database record(s)]"
6215
+ )
6154
6216
 
6155
6217
  # Summary
6156
6218
  click.echo(f"\n{'🔎 DRY RUN SUMMARY' if dry_run else '✅ RENAME COMPLETE'}")
@@ -6160,14 +6222,14 @@ def alias_rename(
6160
6222
  if update_cache:
6161
6223
  click.echo(f" Cache: {'Would update' if dry_run else 'Updated'}")
6162
6224
  else:
6163
- click.echo(f" Cache: Skipped (use --update-cache to update)")
6225
+ click.echo(" Cache: Skipped (use --update-cache to update)")
6164
6226
 
6165
6227
  if dry_run:
6166
- click.echo(f"\n💡 Run without --dry-run to apply changes")
6228
+ click.echo("\n💡 Run without --dry-run to apply changes")
6167
6229
  else:
6168
- click.echo(f"\n💡 Next steps:")
6230
+ click.echo("\n💡 Next steps:")
6169
6231
  click.echo(f" - Review the updated config file: {config}")
6170
- click.echo(f" - Re-run analysis to see updated reports with new name")
6232
+ click.echo(" - Re-run analysis to see updated reports with new name")
6171
6233
 
6172
6234
  except KeyboardInterrupt:
6173
6235
  click.echo("\n\n👋 Interrupted by user. Exiting.")
@@ -6175,6 +6237,7 @@ def alias_rename(
6175
6237
  except Exception as e:
6176
6238
  click.echo(f"❌ Unexpected error: {e}", err=True)
6177
6239
  import traceback
6240
+
6178
6241
  traceback.print_exc()
6179
6242
  sys.exit(1)
6180
6243
 
@@ -6750,7 +6813,10 @@ def training_statistics(config: Path) -> None:
6750
6813
 
6751
6814
  # Initialize trainer to access statistics
6752
6815
  trainer = CommitClassificationTrainer(
6753
- config=cfg, cache=cache, orchestrator=None, training_config={} # Not needed for stats
6816
+ config=cfg,
6817
+ cache=cache,
6818
+ orchestrator=None,
6819
+ training_config={}, # Not needed for stats
6754
6820
  )
6755
6821
 
6756
6822
  stats = trainer.get_training_statistics()
@@ -5,6 +5,7 @@ is run without arguments, offering options for configuration, alias management,
5
5
  analysis execution, and more.
6
6
  """
7
7
 
8
+ import contextlib
8
9
  import logging
9
10
  import os
10
11
  import subprocess
@@ -98,10 +99,8 @@ def _atomic_yaml_write(config_path: Path, config_data: dict) -> None:
98
99
  except Exception as e:
99
100
  # Cleanup temp file on error
100
101
  if temp_fd is not None:
101
- try:
102
+ with contextlib.suppress(Exception):
102
103
  os.close(temp_fd)
103
- except Exception:
104
- pass
105
104
 
106
105
  if temp_path and temp_path.exists():
107
106
  temp_path.unlink(missing_ok=True)
@@ -242,7 +241,7 @@ def validate_config(config_path: Path) -> bool:
242
241
 
243
242
  # Line before
244
243
  if mark.line > 0:
245
- click.echo(f" {mark.line}: {lines[mark.line-1].rstrip()}", err=True)
244
+ click.echo(f" {mark.line}: {lines[mark.line - 1].rstrip()}", err=True)
246
245
 
247
246
  # Problematic line (highlighted)
248
247
  click.echo(
@@ -256,7 +255,7 @@ def validate_config(config_path: Path) -> bool:
256
255
 
257
256
  # Line after
258
257
  if mark.line + 1 < len(lines):
259
- click.echo(f" {mark.line + 2}: {lines[mark.line+1].rstrip()}", err=True)
258
+ click.echo(f" {mark.line + 2}: {lines[mark.line + 1].rstrip()}", err=True)
260
259
  except Exception:
261
260
  # If we can't read file, just skip context
262
261
  pass
@@ -562,6 +561,145 @@ def run_full_analysis(config_path: Path) -> bool:
562
561
  return success
563
562
 
564
563
 
564
+ def rename_developer_alias(config_path: Path) -> bool:
565
+ """Interactive interface for renaming developer aliases.
566
+
567
+ Args:
568
+ config_path: Path to config.yaml file
569
+
570
+ Returns:
571
+ True if rename succeeded, False otherwise.
572
+ """
573
+ click.echo("\n" + "=" * 60)
574
+ click.echo(click.style("Rename Developer Alias", fg="cyan", bold=True))
575
+ click.echo("=" * 60 + "\n")
576
+
577
+ click.echo("Update a developer's canonical display name in reports.")
578
+ click.echo("This updates the configuration file and optionally the cache.\n")
579
+
580
+ try:
581
+ # Load config to get manual_mappings
582
+ with open(config_path) as f:
583
+ config_data = yaml.safe_load(f)
584
+
585
+ # Navigate to manual_mappings
586
+ manual_mappings = (
587
+ config_data.get("analysis", {}).get("identity", {}).get("manual_mappings", [])
588
+ )
589
+
590
+ if not manual_mappings:
591
+ click.echo(
592
+ click.style(
593
+ "❌ No manual_mappings found in config. Please add developers first.", fg="red"
594
+ ),
595
+ err=True,
596
+ )
597
+ return False
598
+
599
+ # Display numbered list of developers
600
+ click.echo(click.style("Current Developers:", fg="cyan", bold=True))
601
+ click.echo()
602
+
603
+ developer_names = []
604
+ for idx, mapping in enumerate(manual_mappings, 1):
605
+ name = mapping.get("name", "Unknown")
606
+ email = mapping.get("primary_email", "N/A")
607
+ alias_count = len(mapping.get("aliases", []))
608
+
609
+ developer_names.append(name)
610
+ click.echo(f" {idx}. {click.style(name, fg='green')}")
611
+ click.echo(f" Email: {email}")
612
+ click.echo(f" Aliases: {alias_count} email(s)")
613
+ click.echo()
614
+
615
+ # Prompt for selection
616
+ try:
617
+ selection = click.prompt(
618
+ "Select developer number to rename (or 0 to cancel)",
619
+ type=click.IntRange(0, len(developer_names)),
620
+ )
621
+ except click.Abort:
622
+ click.echo(click.style("\n❌ Cancelled", fg="yellow"))
623
+ return False
624
+
625
+ if selection == 0:
626
+ click.echo(click.style("\n❌ Cancelled", fg="yellow"))
627
+ return False
628
+
629
+ # Get selected developer name
630
+ old_name = developer_names[selection - 1]
631
+ click.echo(f"\n📝 Selected: {click.style(old_name, fg='green')}")
632
+
633
+ # Prompt for new name
634
+ new_name = click.prompt("Enter new canonical name", type=str)
635
+
636
+ # Validate new name
637
+ new_name = new_name.strip()
638
+ if not new_name:
639
+ click.echo(click.style("❌ New name cannot be empty", fg="red"), err=True)
640
+ return False
641
+
642
+ if new_name == old_name:
643
+ click.echo(click.style("❌ New name is identical to current name", fg="yellow"))
644
+ return False
645
+
646
+ # Ask about cache update
647
+ update_cache = click.confirm("\nAlso update database cache?", default=True)
648
+
649
+ # Show what will be done
650
+ click.echo("\n" + "=" * 60)
651
+ click.echo(click.style("Summary", fg="yellow", bold=True))
652
+ click.echo("=" * 60)
653
+ click.echo(f" Old name: {old_name}")
654
+ click.echo(f" New name: {new_name}")
655
+ click.echo(f" Update cache: {'Yes' if update_cache else 'No'}")
656
+ click.echo()
657
+
658
+ # Confirm
659
+ if not click.confirm("Proceed with rename?", default=True):
660
+ click.echo(click.style("\n❌ Cancelled", fg="yellow"))
661
+ return False
662
+
663
+ except Exception as e:
664
+ click.echo(click.style(f"❌ Error reading config: {e}", fg="red"), err=True)
665
+ logger.error(f"Config read error: {type(e).__name__}: {e}")
666
+ return False
667
+
668
+ try:
669
+ # Validate config path
670
+ _validate_subprocess_path(config_path)
671
+ except ValueError as e:
672
+ click.echo(click.style(f"❌ Invalid config path: {e}", fg="red"), err=True)
673
+ logger.error(f"Config path validation failed: {e}")
674
+ return False
675
+
676
+ # Build command
677
+ cmd = [
678
+ sys.executable,
679
+ "-m",
680
+ "gitflow_analytics.cli",
681
+ "alias-rename",
682
+ "-c",
683
+ str(config_path),
684
+ "--old-name",
685
+ old_name,
686
+ "--new-name",
687
+ new_name,
688
+ ]
689
+
690
+ if update_cache:
691
+ cmd.append("--update-cache")
692
+
693
+ # Run with timeout
694
+ success = _run_subprocess_safely(cmd, operation_name="Alias Rename", timeout=60)
695
+
696
+ if success:
697
+ click.echo(click.style("\n✅ Rename completed successfully!", fg="green"))
698
+ click.echo(f"Future reports will show '{new_name}' instead of '{old_name}'")
699
+
700
+ return success
701
+
702
+
565
703
  def show_main_menu(config_path: Optional[Path] = None) -> None:
566
704
  """Display main interactive menu.
567
705
 
@@ -597,13 +735,14 @@ def show_main_menu(config_path: Optional[Path] = None) -> None:
597
735
  click.echo(" 3. Re-pull Data (Re-run Analysis)")
598
736
  click.echo(" 4. Set Number of Weeks")
599
737
  click.echo(" 5. Run Full Analysis")
738
+ click.echo(" 6. Rename Developer Alias")
600
739
  click.echo(" 0. Exit")
601
740
 
602
741
  # Get user choice
603
742
  click.echo()
604
743
  choice = click.prompt(
605
744
  click.style("Enter your choice", fg="yellow"),
606
- type=click.Choice(["0", "1", "2", "3", "4", "5"], case_sensitive=False),
745
+ type=click.Choice(["0", "1", "2", "3", "4", "5", "6"], case_sensitive=False),
607
746
  show_choices=False,
608
747
  )
609
748
 
@@ -623,6 +762,8 @@ def show_main_menu(config_path: Optional[Path] = None) -> None:
623
762
  success = set_weeks(config_path)
624
763
  elif choice == "5":
625
764
  success = run_full_analysis(config_path)
765
+ elif choice == "6":
766
+ success = rename_developer_alias(config_path)
626
767
 
627
768
  # Show warning if operation failed
628
769
  if not success and choice != "0":
@@ -968,7 +968,9 @@ class ConfigLoader:
968
968
  (
969
969
  cls._resolve_env_var(item)
970
970
  if isinstance(item, str)
971
- else cls._resolve_config_dict(item) if isinstance(item, dict) else item
971
+ else cls._resolve_config_dict(item)
972
+ if isinstance(item, dict)
973
+ else item
972
974
  )
973
975
  for item in value
974
976
  ]
@@ -234,8 +234,7 @@ class ProfileManager:
234
234
  if not profile_class:
235
235
  available = ", ".join(cls._profiles.keys())
236
236
  raise ValueError(
237
- f"Unknown configuration profile: {profile_name}. "
238
- f"Available profiles: {available}"
237
+ f"Unknown configuration profile: {profile_name}. Available profiles: {available}"
239
238
  )
240
239
 
241
240
  profile_settings = profile_class.get_settings()
@@ -192,7 +192,6 @@ class GitDataFetcher:
192
192
  description=f"📊 Processing repository: {project_key}",
193
193
  unit="steps",
194
194
  ) as repo_progress_ctx:
195
-
196
195
  # Step 1: Fetch commits
197
196
  progress.set_description(repo_progress_ctx, f"🔍 {project_key}: Fetching commits")
198
197
  daily_commits = self._fetch_commits_by_day(
@@ -538,7 +537,6 @@ class GitDataFetcher:
538
537
  unit="days",
539
538
  nested=True,
540
539
  ) as day_progress_ctx:
541
-
542
540
  for day_date in days_to_process:
543
541
  # Update description to show current repository and day clearly
544
542
  day_str = day_date.strftime("%Y-%m-%d")
@@ -503,7 +503,9 @@ class TicketExtractor:
503
503
  :100
504
504
  ], # Increased from 60 to 100
505
505
  "full_message": commit.get("message", ""),
506
- "author": commit.get("canonical_name", commit.get("author_name", "Unknown")),
506
+ "author": commit.get(
507
+ "canonical_name", commit.get("author_name", "Unknown")
508
+ ),
507
509
  "author_email": commit.get("author_email", ""),
508
510
  "canonical_id": commit.get("canonical_id", commit.get("author_email", "")),
509
511
  "timestamp": commit.get("timestamp"),
@@ -101,7 +101,7 @@ class GitHubIntegration:
101
101
 
102
102
  if cache_hits > 0 or cache_misses > 0:
103
103
  print(
104
- f" 📊 GitHub PR cache: {cache_hits} hits, {cache_misses} misses ({cache_hits/(cache_hits+cache_misses)*100:.1f}% hit rate)"
104
+ f" 📊 GitHub PR cache: {cache_hits} hits, {cache_misses} misses ({cache_hits / (cache_hits + cache_misses) * 100:.1f}% hit rate)"
105
105
  if (cache_hits + cache_misses) > 0
106
106
  else ""
107
107
  )
@@ -186,7 +186,7 @@ class JIRAIntegration:
186
186
 
187
187
  if cache_hits > 0 or cache_misses > 0:
188
188
  print(
189
- f" 📊 JIRA cache: {cache_hits} hits, {cache_misses} misses ({cache_hits/(cache_hits+cache_misses)*100:.1f}% hit rate)"
189
+ f" 📊 JIRA cache: {cache_hits} hits, {cache_misses} misses ({cache_hits / (cache_hits + cache_misses) * 100:.1f}% hit rate)"
190
190
  )
191
191
 
192
192
  # Fetch missing tickets from JIRA
@@ -169,16 +169,16 @@ class ChatGPTQualitativeAnalyzer:
169
169
  def _create_executive_summary_prompt(self, summary_data: dict[str, Any]) -> str:
170
170
  """Create the prompt for ChatGPT."""
171
171
 
172
- prompt = f"""Based on the following GitFlow Analytics data from the past {summary_data['period_weeks']} weeks, provide a comprehensive executive summary with qualitative insights:
172
+ prompt = f"""Based on the following GitFlow Analytics data from the past {summary_data["period_weeks"]} weeks, provide a comprehensive executive summary with qualitative insights:
173
173
 
174
174
  ## Key Metrics:
175
- - Total Commits: {summary_data['total_commits']:,}
176
- - Active Developers: {summary_data['total_developers']}
177
- - Lines Changed: {summary_data['lines_changed']:,}
178
- - Story Points Delivered: {summary_data['story_points']}
179
- - Ticket Coverage: {summary_data['ticket_coverage']:.1f}%
180
- - Team Health Score: {summary_data['team_health_score']:.1f}/100 ({summary_data['team_health_rating']})
181
- - Velocity Trend: {summary_data['velocity_trend']}
175
+ - Total Commits: {summary_data["total_commits"]:,}
176
+ - Active Developers: {summary_data["total_developers"]}
177
+ - Lines Changed: {summary_data["lines_changed"]:,}
178
+ - Story Points Delivered: {summary_data["story_points"]}
179
+ - Ticket Coverage: {summary_data["ticket_coverage"]:.1f}%
180
+ - Team Health Score: {summary_data["team_health_score"]:.1f}/100 ({summary_data["team_health_rating"]})
181
+ - Velocity Trend: {summary_data["velocity_trend"]}
182
182
 
183
183
  ## Top Contributors:
184
184
  """
@@ -222,18 +222,18 @@ Report only statistical patterns, measurable trends, and process gaps. Use factu
222
222
 
223
223
  return f"""## Executive Summary
224
224
 
225
- Over the past {summary_data['period_weeks']} weeks, the development team generated {summary_data['total_commits']:,} commits across {summary_data['total_developers']} active developers.
225
+ Over the past {summary_data["period_weeks"]} weeks, the development team generated {summary_data["total_commits"]:,} commits across {summary_data["total_developers"]} active developers.
226
226
 
227
- The team health score measured {summary_data['team_health_score']:.1f}/100 ({summary_data['team_health_rating']}). Ticket coverage reached {summary_data['ticket_coverage']:.1f}% of total commits with trackable references.
227
+ The team health score measured {summary_data["team_health_score"]:.1f}/100 ({summary_data["team_health_rating"]}). Ticket coverage reached {summary_data["ticket_coverage"]:.1f}% of total commits with trackable references.
228
228
 
229
229
  ### Measured Outputs:
230
- - Code changes: {summary_data['lines_changed']:,} lines modified
231
- - Story points completed: {summary_data['story_points']}
232
- - Velocity trend: {summary_data['velocity_trend']}
230
+ - Code changes: {summary_data["lines_changed"]:,} lines modified
231
+ - Story points completed: {summary_data["story_points"]}
232
+ - Velocity trend: {summary_data["velocity_trend"]}
233
233
 
234
234
  ### Process Recommendations:
235
- 1. {'Maintain current output rate' if summary_data['velocity_trend'] == 'increasing' else 'Analyze velocity decline factors'}
236
- 2. {'Sustain current tracking rate' if summary_data['ticket_coverage'] > 60 else 'Increase commit-ticket linking to reach 70% coverage target'}
235
+ 1. {"Maintain current output rate" if summary_data["velocity_trend"] == "increasing" else "Analyze velocity decline factors"}
236
+ 2. {"Sustain current tracking rate" if summary_data["ticket_coverage"] > 60 else "Increase commit-ticket linking to reach 70% coverage target"}
237
237
  3. Review projects with health scores below 60/100 for process gaps
238
238
 
239
239
  *Note: This is a fallback summary. For detailed analysis, configure ChatGPT integration.*
@@ -353,7 +353,7 @@ Response (format: CATEGORY confidence reasoning):""",
353
353
  """
354
354
  formatted = []
355
355
  for i, example in enumerate(examples, 1):
356
- formatted.append(f"{i}. Message: \"{example['message']}\"")
356
+ formatted.append(f'{i}. Message: "{example["message"]}"')
357
357
  formatted.append(f" Response: {example['response']}")
358
358
  return "\n".join(formatted)
359
359
 
@@ -577,8 +577,7 @@ class QualitativeProcessor:
577
577
  llm_pct = (llm_processed / total_commits) * 100 if total_commits > 0 else 0
578
578
 
579
579
  self.logger.info(
580
- f"Processing breakdown: {cache_pct:.1f}% cached, "
581
- f"{nlp_pct:.1f}% NLP, {llm_pct:.1f}% LLM"
580
+ f"Processing breakdown: {cache_pct:.1f}% cached, {nlp_pct:.1f}% NLP, {llm_pct:.1f}% LLM"
582
581
  )
583
582
 
584
583
  def _should_optimize_cache(self) -> bool:
@@ -906,7 +906,9 @@ class EnhancedQualitativeAnalyzer:
906
906
  "status": (
907
907
  "excellent"
908
908
  if activity_score >= 80
909
- else "good" if activity_score >= 60 else "needs_improvement"
909
+ else "good"
910
+ if activity_score >= 60
911
+ else "needs_improvement"
910
912
  ),
911
913
  },
912
914
  "contributor_diversity": {
@@ -915,7 +917,9 @@ class EnhancedQualitativeAnalyzer:
915
917
  "status": (
916
918
  "excellent"
917
919
  if len(contributors) >= 4
918
- else "good" if len(contributors) >= 2 else "concerning"
920
+ else "good"
921
+ if len(contributors) >= 2
922
+ else "concerning"
919
923
  ),
920
924
  },
921
925
  "pr_velocity": {
@@ -929,7 +933,9 @@ class EnhancedQualitativeAnalyzer:
929
933
  "status": (
930
934
  "excellent"
931
935
  if ticket_coverage >= 80
932
- else "good" if ticket_coverage >= 60 else "needs_improvement"
936
+ else "good"
937
+ if ticket_coverage >= 60
938
+ else "needs_improvement"
933
939
  ),
934
940
  },
935
941
  }
@@ -948,7 +954,9 @@ class EnhancedQualitativeAnalyzer:
948
954
  "status": (
949
955
  "excellent"
950
956
  if overall_score >= 80
951
- else "good" if overall_score >= 60 else "needs_improvement"
957
+ else "good"
958
+ if overall_score >= 60
959
+ else "needs_improvement"
952
960
  ),
953
961
  }
954
962
 
@@ -1918,7 +1926,9 @@ class EnhancedQualitativeAnalyzer:
1918
1926
  "status": (
1919
1927
  "excellent"
1920
1928
  if ticket_coverage >= 80
1921
- else "good" if ticket_coverage >= 60 else "needs_improvement"
1929
+ else "good"
1930
+ if ticket_coverage >= 60
1931
+ else "needs_improvement"
1922
1932
  ),
1923
1933
  },
1924
1934
  "message_quality": {
@@ -1926,7 +1936,9 @@ class EnhancedQualitativeAnalyzer:
1926
1936
  "status": (
1927
1937
  "excellent"
1928
1938
  if message_quality >= 80
1929
- else "good" if message_quality >= 60 else "needs_improvement"
1939
+ else "good"
1940
+ if message_quality >= 60
1941
+ else "needs_improvement"
1930
1942
  ),
1931
1943
  },
1932
1944
  "commit_size_compliance": {
@@ -1934,7 +1946,9 @@ class EnhancedQualitativeAnalyzer:
1934
1946
  "status": (
1935
1947
  "excellent"
1936
1948
  if size_compliance >= 80
1937
- else "good" if size_compliance >= 60 else "needs_improvement"
1949
+ else "good"
1950
+ if size_compliance >= 60
1951
+ else "needs_improvement"
1938
1952
  ),
1939
1953
  },
1940
1954
  "pr_approval_rate": {"score": pr_approval_rate, "status": "good"}, # Placeholder
@@ -1986,7 +2000,9 @@ class EnhancedQualitativeAnalyzer:
1986
2000
  "collaboration_level": (
1987
2001
  "high"
1988
2002
  if collaboration_score >= 70
1989
- else "medium" if collaboration_score >= 40 else "low"
2003
+ else "medium"
2004
+ if collaboration_score >= 40
2005
+ else "low"
1990
2006
  ),
1991
2007
  "patterns": {
1992
2008
  "multi_project_engagement": cross_collaboration_rate >= 50,
@@ -457,12 +457,16 @@ class RichProgressDisplay:
457
457
  mem_icon = (
458
458
  "🟢"
459
459
  if self.statistics.memory_usage < 500
460
- else "🟡" if self.statistics.memory_usage < 1000 else "🔴"
460
+ else "🟡"
461
+ if self.statistics.memory_usage < 1000
462
+ else "🔴"
461
463
  )
462
464
  cpu_icon = (
463
465
  "🟢"
464
466
  if self.statistics.cpu_percent < 50
465
- else "🟡" if self.statistics.cpu_percent < 80 else "🔴"
467
+ else "🟡"
468
+ if self.statistics.cpu_percent < 80
469
+ else "🔴"
466
470
  )
467
471
  system_stats.append(f"{mem_icon} Memory: {self.statistics.memory_usage:.0f} MB")
468
472
  system_stats.append(f"{cpu_icon} CPU: {self.statistics.cpu_percent:.1f}%")
@@ -471,7 +475,9 @@ class RichProgressDisplay:
471
475
  speed_icon = (
472
476
  "🚀"
473
477
  if self.statistics.processing_speed > 100
474
- else "⚡" if self.statistics.processing_speed > 50 else "🐢"
478
+ else "⚡"
479
+ if self.statistics.processing_speed > 50
480
+ else "🐢"
475
481
  )
476
482
  system_stats.append(
477
483
  f"{speed_icon} Speed: {self.statistics.processing_speed:.1f} commits/s"
@@ -484,7 +490,9 @@ class RichProgressDisplay:
484
490
  phase_indicator = (
485
491
  "⚙️"
486
492
  if "Processing" in self.statistics.current_phase
487
- else "🔍" if "Analyzing" in self.statistics.current_phase else "✨"
493
+ else "🔍"
494
+ if "Analyzing" in self.statistics.current_phase
495
+ else "✨"
488
496
  )
489
497
  phase_text = f"{phase_indicator} [bold green]{self.statistics.current_phase}[/bold green]"
490
498
  elapsed_text = f"⏱️ [bold blue]{self.statistics.get_elapsed_time()}[/bold blue]"
@@ -1250,9 +1258,9 @@ class SimpleProgressDisplay:
1250
1258
  # Compatibility methods for CLI interface
1251
1259
  def show_header(self):
1252
1260
  """Display header - compatibility method for CLI."""
1253
- print(f"\n{'='*60}")
1261
+ print(f"\n{'=' * 60}")
1254
1262
  print(f"GitFlow Analytics v{self.version}")
1255
- print(f"{'='*60}\n")
1263
+ print(f"{'=' * 60}\n")
1256
1264
 
1257
1265
  def start_live_display(self):
1258
1266
  """Start live display - compatibility wrapper for start()."""
@@ -636,7 +636,7 @@ class ActivityVerifier:
636
636
  # Group consecutive days
637
637
  lines.append(f"Found {len(zero_activity_days)} days with no activity:")
638
638
  for i in range(0, len(zero_activity_days), 7):
639
- lines.append(f" {', '.join(zero_activity_days[i:i+7])}")
639
+ lines.append(f" {', '.join(zero_activity_days[i : i + 7])}")
640
640
  else:
641
641
  lines.append("No days with zero activity found!")
642
642
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitflow-analytics
3
- Version: 3.13.0
3
+ Version: 3.13.5
4
4
  Summary: Analyze Git repositories for developer productivity insights
5
5
  Author-email: Bob Matyas <bobmatnyc@gmail.com>
6
6
  License: MIT
@@ -1183,6 +1183,42 @@ gitflow-analytics analyze -c config.yaml --debug
1183
1183
 
1184
1184
  Contributions are welcome! Please feel free to submit a Pull Request.
1185
1185
 
1186
+ ### Development Setup
1187
+
1188
+ ```bash
1189
+ # Clone the repository
1190
+ git clone https://github.com/bobmatnyc/gitflow-analytics.git
1191
+ cd gitflow-analytics
1192
+
1193
+ # Install development dependencies
1194
+ make install-dev
1195
+
1196
+ # Run tests
1197
+ make test
1198
+
1199
+ # Format code
1200
+ make format
1201
+
1202
+ # Run all quality checks
1203
+ make quality-gate
1204
+ ```
1205
+
1206
+ ### Release Workflow
1207
+
1208
+ This project uses a Makefile-based release workflow for simplicity and transparency. See [RELEASE.md](RELEASE.md) for detailed documentation.
1209
+
1210
+ **Quick Reference:**
1211
+ ```bash
1212
+ make release-patch # Bug fixes (3.13.1 → 3.13.2)
1213
+ make release-minor # New features (3.13.1 → 3.14.0)
1214
+ make release-major # Breaking changes (3.13.1 → 4.0.0)
1215
+ ```
1216
+
1217
+ For more details, see:
1218
+ - [RELEASE.md](RELEASE.md) - Comprehensive release guide
1219
+ - [RELEASE_QUICKREF.md](RELEASE_QUICKREF.md) - Quick reference card
1220
+ - `make help` - All available commands
1221
+
1186
1222
  ## License
1187
1223
 
1188
1224
  This project is licensed under the MIT License - see the LICENSE file for details.
@@ -1,9 +1,9 @@
1
1
  gitflow_analytics/__init__.py,sha256=W3Jaey5wuT1nBPehVLTIRkVIyBa5jgYOlBKc_UFfh-4,773
2
- gitflow_analytics/_version.py,sha256=TklDtu9hDGukw4XEfy3LR-nKoAb-vOvLBNLNYEL_r1Y,138
3
- gitflow_analytics/cli.py,sha256=52-T-1jysBeCKkRG9y3qUbbZFzni4tTflO-uU2lj1VY,300203
2
+ gitflow_analytics/_version.py,sha256=-dJJFHKImkdT2L9Kw0gRXFj8dh26CFKPTsdrAlg1aTg,138
3
+ gitflow_analytics/cli.py,sha256=KdnuqiUodiqmImsDfMv15MuOlLrI_2zTv3x4s1BV0U8,302532
4
4
  gitflow_analytics/config.py,sha256=XRuxvzLWyn_ML7mDCcuZ9-YFNAEsnt33vIuWxQQ_jxg,1033
5
5
  gitflow_analytics/constants.py,sha256=GXEncUJS9ijOI5KWtQCTANwdqxPfXpw-4lNjhaWTKC4,2488
6
- gitflow_analytics/verify_activity.py,sha256=aRQnmypf5NDasXudf2iz_WdJnCWtwlbAiJ5go0DJLSU,27050
6
+ gitflow_analytics/verify_activity.py,sha256=q82VnU8FhHEPlnupYMvh1XtyaDJfIPPg-AI8cSM0PIk,27054
7
7
  gitflow_analytics/classification/__init__.py,sha256=p8shPUZpGaw7-ivhfAVrPDbSP2LrpvWC1WEsBJIg-PI,969
8
8
  gitflow_analytics/classification/batch_classifier.py,sha256=wR1hwYOB4JbV2h5fQrs-UHlf4XwCPZSJUjKFWyD4Qv0,37696
9
9
  gitflow_analytics/classification/classifier.py,sha256=U1vpdiMXqGdHR8iHWf_wPdrJxxNRB5By94BDpck8R9g,17750
@@ -12,13 +12,13 @@ gitflow_analytics/classification/linguist_analyzer.py,sha256=HjLx9mM7hGXtrvMba6o
12
12
  gitflow_analytics/classification/model.py,sha256=2KbmFh9MpyvHMcNHbqwUTAAVLHHu3MiTfFIPyZSGa-8,16356
13
13
  gitflow_analytics/cli_wizards/__init__.py,sha256=iSCVYkwAnyPweZixLtFnNa7pB8DRLAj_sJrUPYesdn8,432
14
14
  gitflow_analytics/cli_wizards/install_wizard.py,sha256=gz5c1NYeGLCzs-plL6ju7GXn7VldF7VyMw8MO4CzUGk,70345
15
- gitflow_analytics/cli_wizards/menu.py,sha256=hjKziABmFqvTLrVwhVUnrtwqCrqGEEbvNTqQY84ZXzQ,21330
15
+ gitflow_analytics/cli_wizards/menu.py,sha256=Jcz4aTimVQ2kt1z9yC3I8uWUrmxitLvCvvSgem_nRpI,26106
16
16
  gitflow_analytics/cli_wizards/run_launcher.py,sha256=J6G_C7IqxPg7_GhAfbV99D1dIIWwb1s_qmHC7Iv2iGI,15038
17
17
  gitflow_analytics/config/__init__.py,sha256=KziRIbBJctB5LOLcKLzELWA1rXwjS6-C2_DeM_hT9rM,1133
18
18
  gitflow_analytics/config/aliases.py,sha256=z9F0X6qbbF544Tw7sHlOoBj5mpRSddMkCpoKLzvVzDU,10960
19
19
  gitflow_analytics/config/errors.py,sha256=IBKhAIwJ4gscZFnLDyE3jEp03wn2stPR7JQJXNSIfok,10386
20
- gitflow_analytics/config/loader.py,sha256=LpXUDyhLnlLcyPkDgFr1ustVhQmSS8Wn2ZiEaw_iNnY,38036
21
- gitflow_analytics/config/profiles.py,sha256=yUjFAWW6uzOUdi5qlPE-QV9681HigyrLiSJFpL8X9A0,7967
20
+ gitflow_analytics/config/loader.py,sha256=khhxlt14TE_J-q-07cuhGpvmatU9Ttii0oMcKnsFpMA,38084
21
+ gitflow_analytics/config/profiles.py,sha256=61lGoRScui3kBE63Bb9CSA442ISVjD_TupCEK-Yh7Yk,7947
22
22
  gitflow_analytics/config/repository.py,sha256=u7JHcKvqmXOl3i7EmNUfJ6wtjzElxPMyXRkATnVyQ0I,4685
23
23
  gitflow_analytics/config/schema.py,sha256=ETxxWUwpAAwMXiXFkawoYcwJvvSo9D6zK0uHU-JLyS0,17270
24
24
  gitflow_analytics/config/validator.py,sha256=l7AHjXYJ8wEmyA1rn2WiItZXtAiRb9YBLjFCDl53qKM,5907
@@ -26,7 +26,7 @@ gitflow_analytics/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
26
26
  gitflow_analytics/core/analyzer.py,sha256=apLbRFAOGDPCNnBTNOG_eXaVXh_QglO07t6p5sINnKo,59924
27
27
  gitflow_analytics/core/branch_mapper.py,sha256=1L1ctrhTEqMZ61eS1nZRkcyaarLipeQgotw4HdXcSmM,7407
28
28
  gitflow_analytics/core/cache.py,sha256=2SBzry3FoLCJyhu-I-AgNTSzN_MkA-DunzOAxq_lyTw,69152
29
- gitflow_analytics/core/data_fetcher.py,sha256=Adj1EE2RaHxSC6xmcagQb_ak9M9RgUC2GgfPuSrITME,106946
29
+ gitflow_analytics/core/data_fetcher.py,sha256=KI0lGxrKvjOHf2UjnGytmcy9GSnSsA28c5mysOH7q1o,106944
30
30
  gitflow_analytics/core/git_auth.py,sha256=QP7U5_Mi9J-hEtoEhdjoMBl61nCukOGlL8PYXYSyN3g,6369
31
31
  gitflow_analytics/core/git_timeout_wrapper.py,sha256=14K8PHKSOonW4hJpLigB5XQNSWxmFbMFbrpu8cT1h-M,12534
32
32
  gitflow_analytics/core/identity.py,sha256=CTjxpM5BeeMyGQ8QbtSCsUmuzMmU7vhBwrdQctjI7Z0,31397
@@ -38,14 +38,14 @@ gitflow_analytics/extractors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
38
38
  gitflow_analytics/extractors/base.py,sha256=AKbYkFiMhNxVj7zfNzsJfh0rpyTdNr4Faea3bcZPPBo,1168
39
39
  gitflow_analytics/extractors/ml_tickets.py,sha256=js5OFmbZt9JHy5r_crehhuB1MxrkdfrPj2u_4B6K35c,43304
40
40
  gitflow_analytics/extractors/story_points.py,sha256=IggP-Ei832oV9aD08a3li08kmjF3BqyU9i8EgAZcpfs,5324
41
- gitflow_analytics/extractors/tickets.py,sha256=KDXOTCb6FsTIsA71gldbW58_WiuGDopC2tVYpUfCnUI,43829
41
+ gitflow_analytics/extractors/tickets.py,sha256=vNOUGyUSHiaBguPOWwg1gzB5gJ4RJtZs4HdLAJqvI0k,43883
42
42
  gitflow_analytics/identity_llm/__init__.py,sha256=tpWDwapm6zIyb8LxLO8A6pHlE3wNorT_fBL-Yp9-XnU,250
43
43
  gitflow_analytics/identity_llm/analysis_pass.py,sha256=FJF1BEGekHRY4i5jasgxxL_UWFGYP5kBkvn8hAtMorY,9728
44
44
  gitflow_analytics/identity_llm/analyzer.py,sha256=-a7lUJt_Dlgx9aNOH1YlFqPe7BSxtwY2RoGruIzwrzs,17932
45
45
  gitflow_analytics/identity_llm/models.py,sha256=F1RN6g8og9esj-m4TPY_928Ci9TA43G9NFNHYf4zHHQ,2677
46
46
  gitflow_analytics/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
- gitflow_analytics/integrations/github_integration.py,sha256=52lyq5GNJIlTXIv7iwrkuxg0firpTcwYtTU9RAn8EIk,13324
48
- gitflow_analytics/integrations/jira_integration.py,sha256=3DV1hGNs1HxAOSGt2BfqBrWSigRN5H8BT1-G7E_8hGg,28761
47
+ gitflow_analytics/integrations/github_integration.py,sha256=4hTV8I1ACY9ELRIbl9vikamtPkiUyVN1ualtkBvrWPE,13330
48
+ gitflow_analytics/integrations/jira_integration.py,sha256=-Rutft6uQG7hUAcbUck0iHJZ2dbKcToN9zhjd9aIWng,28767
49
49
  gitflow_analytics/integrations/orchestrator.py,sha256=u3FKZF2yD5g5HhNFm6nIJe69ZKfU1QLni4S14GDRIrY,13205
50
50
  gitflow_analytics/metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
51
  gitflow_analytics/metrics/activity_scoring.py,sha256=lSrdeH9SVNzeSR80vvzdWBuFcD3BkHsOdSZu97q6Bdg,13171
@@ -61,8 +61,8 @@ gitflow_analytics/pm_framework/registry.py,sha256=ggUHS3WFsKXifaYPZgY15r2vGZEKyx
61
61
  gitflow_analytics/pm_framework/adapters/__init__.py,sha256=vS5btB-yIwVHZfoFYacWxHk3HszxIMWLnvBUgVDdNDU,1756
62
62
  gitflow_analytics/pm_framework/adapters/jira_adapter.py,sha256=E5-NuKHFDGkqObjhWvXqoEsVVnLXrnAiF2v81hTYQ7A,72527
63
63
  gitflow_analytics/qualitative/__init__.py,sha256=fwlb_xrv7Gatjylk5wclzckZxyss8K5cdZhhTHMWfYw,1184
64
- gitflow_analytics/qualitative/chatgpt_analyzer.py,sha256=CHiaGO5ESGCcQ6pJPxfVZI9gTMp_9OD4TTltmgxCypU,11816
65
- gitflow_analytics/qualitative/enhanced_analyzer.py,sha256=XWN27-hRkxLcQUt_XVejyGkPCkxA1qpHyGyACe2xtw8,92429
64
+ gitflow_analytics/qualitative/chatgpt_analyzer.py,sha256=nQDk1Rf2z2svpsnXoz0mxbwLXytFo3EgImbegg53FvI,11816
65
+ gitflow_analytics/qualitative/enhanced_analyzer.py,sha256=js27aVVxt9ZUYEcbJwYdmpt7JZTRbLHcWCkO2LrxA9E,92733
66
66
  gitflow_analytics/qualitative/example_enhanced_usage.py,sha256=pKKhAjOCwmBaJPzZ8RDl8R4uG23NwFXUUeAqv6oYM2E,14924
67
67
  gitflow_analytics/qualitative/classifiers/__init__.py,sha256=lgabpW-_aub_O-1CVbmgeUVEo2jf5O-DK0Y2dF-WrZc,346
68
68
  gitflow_analytics/qualitative/classifiers/change_type.py,sha256=3glCIkNxTQAbk0s0Urp4nLp9OXtYzF0-I8SzOEpx9JE,23291
@@ -76,13 +76,13 @@ gitflow_analytics/qualitative/classifiers/llm/batch_processor.py,sha256=oSlJPYOA
76
76
  gitflow_analytics/qualitative/classifiers/llm/cache.py,sha256=5UWRMgz0bOc_GRShE5gvYEzLhxB-VBDkhZKRECNmjOI,16930
77
77
  gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py,sha256=2mqWPHo9SrhwZQ0Q_FNViI_M83Rl18sJVg3p1lwkcBM,14902
78
78
  gitflow_analytics/qualitative/classifiers/llm/openai_client.py,sha256=pkLvXpAR9tK2VLPwpUoK3GYmThMb8VcekRj2qpvVrNw,15255
79
- gitflow_analytics/qualitative/classifiers/llm/prompts.py,sha256=7dLvCb8bQyZGOsQF_ajZpFxaXyHIbtfUfTO2yQJPxJs,12906
79
+ gitflow_analytics/qualitative/classifiers/llm/prompts.py,sha256=JCA2_Ecswugk3L6Kw0GB7NFsBpEvhsG3FSnG8x1T5-c,12904
80
80
  gitflow_analytics/qualitative/classifiers/llm/response_parser.py,sha256=AyGfTpmvpx4QvRKJDWGS3CbylLm2SOAD0nSZVrkGTrM,9931
81
81
  gitflow_analytics/qualitative/core/__init__.py,sha256=22tZJDPyYE0k5-9lx_84R2SsZN8PRc_1I1L6prSkoSE,315
82
82
  gitflow_analytics/qualitative/core/llm_fallback.py,sha256=q6KijFgi7PMgmWdgKEhFV3JSskbrs4PrD27npVSTDz4,25572
83
83
  gitflow_analytics/qualitative/core/nlp_engine.py,sha256=c-R0chjKmCif5ilBl3JIURNushVNw5musc8INJhL3cc,14490
84
84
  gitflow_analytics/qualitative/core/pattern_cache.py,sha256=H_t759ftWGJC3QQy6dKqdt3Mf2TojWfV6C41eVdPZTo,18537
85
- gitflow_analytics/qualitative/core/processor.py,sha256=QcFqMf2-udmVpbKFHQbroNSqQkSPHnBTbDBlXYCrbac,27450
85
+ gitflow_analytics/qualitative/core/processor.py,sha256=uLeE2qHo4Py54UNJQFMlMubMUmC4NuXK6SivFEB5p8U,27434
86
86
  gitflow_analytics/qualitative/models/__init__.py,sha256=Ro_lAXyt3jfL29xgZ6jn_DvDRv3PZ6myzsrXq_ctqRQ,457
87
87
  gitflow_analytics/qualitative/models/schemas.py,sha256=9GRvp_mFOtIRiAbaNjzxq5Lo8PzD-r4F_-sqoyOjN3w,10670
88
88
  gitflow_analytics/qualitative/utils/__init__.py,sha256=YGLGiP4WWFO-KnZERJ6uj8M3uJsmizsSeoR1tsoGK0c,319
@@ -124,12 +124,12 @@ gitflow_analytics/training/pipeline.py,sha256=PQegTk_-OsPexVyRDfiy-3Df-7pcs25C4v
124
124
  gitflow_analytics/types/__init__.py,sha256=v31ysjqF7jgCUkqAKaj9gqV3RDjL74sJRzX3uh7NxZA,156
125
125
  gitflow_analytics/types/commit_types.py,sha256=Ub7Nyh5ajGQW_YVoVV_iQ1Y05aEHZd-YGA4xjOSHElc,1684
126
126
  gitflow_analytics/ui/__init__.py,sha256=UBhYhZMvwlSrCuGWjkIdoP2zNbiQxOHOli-I8mqIZUE,441
127
- gitflow_analytics/ui/progress_display.py,sha256=3xJnCOSs1DRVAfS-rTu37EsLfWDFW5-mbv-bPS9NMm4,59182
127
+ gitflow_analytics/ui/progress_display.py,sha256=omCS86mCQR0QeMoM0YnsV3Gf2oALsDLu8u7XseQU6lk,59306
128
128
  gitflow_analytics/utils/__init__.py,sha256=YE3E5Mx-LmVRqLIgUUwDmbstm6gkpeavYHrQmVjwR3o,197
129
129
  gitflow_analytics/utils/commit_utils.py,sha256=TBgrWW73EODGOegGCF79ch0L0e5R6gpydNWutiQOa14,1356
130
- gitflow_analytics-3.13.0.dist-info/licenses/LICENSE,sha256=xwvSwY1GYXpRpmbnFvvnbmMwpobnrdN9T821sGvjOY0,1066
131
- gitflow_analytics-3.13.0.dist-info/METADATA,sha256=_qRLGhFaiq52qsJIVwf-1hN6Nx3pRfB12zTPfhxCx-Y,39531
132
- gitflow_analytics-3.13.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
133
- gitflow_analytics-3.13.0.dist-info/entry_points.txt,sha256=ZOsX0GLsnMysp5FWPOfP_qyoS7WJ8IgcaDFDxWBYl1g,98
134
- gitflow_analytics-3.13.0.dist-info/top_level.txt,sha256=CQyxZXjKvpSB1kgqqtuE0PCRqfRsXZJL8JrYpJKtkrk,18
135
- gitflow_analytics-3.13.0.dist-info/RECORD,,
130
+ gitflow_analytics-3.13.5.dist-info/licenses/LICENSE,sha256=xwvSwY1GYXpRpmbnFvvnbmMwpobnrdN9T821sGvjOY0,1066
131
+ gitflow_analytics-3.13.5.dist-info/METADATA,sha256=iB3vdWqPASbljtKC1rORHcIbiP1X21egIORMtIIGFws,40374
132
+ gitflow_analytics-3.13.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
133
+ gitflow_analytics-3.13.5.dist-info/entry_points.txt,sha256=ZOsX0GLsnMysp5FWPOfP_qyoS7WJ8IgcaDFDxWBYl1g,98
134
+ gitflow_analytics-3.13.5.dist-info/top_level.txt,sha256=CQyxZXjKvpSB1kgqqtuE0PCRqfRsXZJL8JrYpJKtkrk,18
135
+ gitflow_analytics-3.13.5.dist-info/RECORD,,