gitflow-analytics 1.0.1__py3-none-any.whl โ†’ 1.3.6__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 (119) hide show
  1. gitflow_analytics/__init__.py +11 -11
  2. gitflow_analytics/_version.py +2 -2
  3. gitflow_analytics/classification/__init__.py +31 -0
  4. gitflow_analytics/classification/batch_classifier.py +752 -0
  5. gitflow_analytics/classification/classifier.py +464 -0
  6. gitflow_analytics/classification/feature_extractor.py +725 -0
  7. gitflow_analytics/classification/linguist_analyzer.py +574 -0
  8. gitflow_analytics/classification/model.py +455 -0
  9. gitflow_analytics/cli.py +4490 -378
  10. gitflow_analytics/cli_rich.py +503 -0
  11. gitflow_analytics/config/__init__.py +43 -0
  12. gitflow_analytics/config/errors.py +261 -0
  13. gitflow_analytics/config/loader.py +904 -0
  14. gitflow_analytics/config/profiles.py +264 -0
  15. gitflow_analytics/config/repository.py +124 -0
  16. gitflow_analytics/config/schema.py +441 -0
  17. gitflow_analytics/config/validator.py +154 -0
  18. gitflow_analytics/config.py +44 -398
  19. gitflow_analytics/core/analyzer.py +1320 -172
  20. gitflow_analytics/core/branch_mapper.py +132 -132
  21. gitflow_analytics/core/cache.py +1554 -175
  22. gitflow_analytics/core/data_fetcher.py +1193 -0
  23. gitflow_analytics/core/identity.py +571 -185
  24. gitflow_analytics/core/metrics_storage.py +526 -0
  25. gitflow_analytics/core/progress.py +372 -0
  26. gitflow_analytics/core/schema_version.py +269 -0
  27. gitflow_analytics/extractors/base.py +13 -11
  28. gitflow_analytics/extractors/ml_tickets.py +1100 -0
  29. gitflow_analytics/extractors/story_points.py +77 -59
  30. gitflow_analytics/extractors/tickets.py +841 -89
  31. gitflow_analytics/identity_llm/__init__.py +6 -0
  32. gitflow_analytics/identity_llm/analysis_pass.py +231 -0
  33. gitflow_analytics/identity_llm/analyzer.py +464 -0
  34. gitflow_analytics/identity_llm/models.py +76 -0
  35. gitflow_analytics/integrations/github_integration.py +258 -87
  36. gitflow_analytics/integrations/jira_integration.py +572 -123
  37. gitflow_analytics/integrations/orchestrator.py +206 -82
  38. gitflow_analytics/metrics/activity_scoring.py +322 -0
  39. gitflow_analytics/metrics/branch_health.py +470 -0
  40. gitflow_analytics/metrics/dora.py +542 -179
  41. gitflow_analytics/models/database.py +986 -59
  42. gitflow_analytics/pm_framework/__init__.py +115 -0
  43. gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
  44. gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
  45. gitflow_analytics/pm_framework/base.py +406 -0
  46. gitflow_analytics/pm_framework/models.py +211 -0
  47. gitflow_analytics/pm_framework/orchestrator.py +652 -0
  48. gitflow_analytics/pm_framework/registry.py +333 -0
  49. gitflow_analytics/qualitative/__init__.py +29 -0
  50. gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
  51. gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
  52. gitflow_analytics/qualitative/classifiers/change_type.py +742 -0
  53. gitflow_analytics/qualitative/classifiers/domain_classifier.py +506 -0
  54. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +535 -0
  55. gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
  56. gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
  57. gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
  58. gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
  59. gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
  60. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
  61. gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
  62. gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
  63. gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
  64. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +438 -0
  65. gitflow_analytics/qualitative/core/__init__.py +13 -0
  66. gitflow_analytics/qualitative/core/llm_fallback.py +657 -0
  67. gitflow_analytics/qualitative/core/nlp_engine.py +382 -0
  68. gitflow_analytics/qualitative/core/pattern_cache.py +479 -0
  69. gitflow_analytics/qualitative/core/processor.py +673 -0
  70. gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
  71. gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
  72. gitflow_analytics/qualitative/models/__init__.py +25 -0
  73. gitflow_analytics/qualitative/models/schemas.py +306 -0
  74. gitflow_analytics/qualitative/utils/__init__.py +13 -0
  75. gitflow_analytics/qualitative/utils/batch_processor.py +339 -0
  76. gitflow_analytics/qualitative/utils/cost_tracker.py +345 -0
  77. gitflow_analytics/qualitative/utils/metrics.py +361 -0
  78. gitflow_analytics/qualitative/utils/text_processing.py +285 -0
  79. gitflow_analytics/reports/__init__.py +100 -0
  80. gitflow_analytics/reports/analytics_writer.py +550 -18
  81. gitflow_analytics/reports/base.py +648 -0
  82. gitflow_analytics/reports/branch_health_writer.py +322 -0
  83. gitflow_analytics/reports/classification_writer.py +924 -0
  84. gitflow_analytics/reports/cli_integration.py +427 -0
  85. gitflow_analytics/reports/csv_writer.py +1700 -216
  86. gitflow_analytics/reports/data_models.py +504 -0
  87. gitflow_analytics/reports/database_report_generator.py +427 -0
  88. gitflow_analytics/reports/example_usage.py +344 -0
  89. gitflow_analytics/reports/factory.py +499 -0
  90. gitflow_analytics/reports/formatters.py +698 -0
  91. gitflow_analytics/reports/html_generator.py +1116 -0
  92. gitflow_analytics/reports/interfaces.py +489 -0
  93. gitflow_analytics/reports/json_exporter.py +2770 -0
  94. gitflow_analytics/reports/narrative_writer.py +2289 -158
  95. gitflow_analytics/reports/story_point_correlation.py +1144 -0
  96. gitflow_analytics/reports/weekly_trends_writer.py +389 -0
  97. gitflow_analytics/training/__init__.py +5 -0
  98. gitflow_analytics/training/model_loader.py +377 -0
  99. gitflow_analytics/training/pipeline.py +550 -0
  100. gitflow_analytics/tui/__init__.py +5 -0
  101. gitflow_analytics/tui/app.py +724 -0
  102. gitflow_analytics/tui/screens/__init__.py +8 -0
  103. gitflow_analytics/tui/screens/analysis_progress_screen.py +496 -0
  104. gitflow_analytics/tui/screens/configuration_screen.py +523 -0
  105. gitflow_analytics/tui/screens/loading_screen.py +348 -0
  106. gitflow_analytics/tui/screens/main_screen.py +321 -0
  107. gitflow_analytics/tui/screens/results_screen.py +722 -0
  108. gitflow_analytics/tui/widgets/__init__.py +7 -0
  109. gitflow_analytics/tui/widgets/data_table.py +255 -0
  110. gitflow_analytics/tui/widgets/export_modal.py +301 -0
  111. gitflow_analytics/tui/widgets/progress_widget.py +187 -0
  112. gitflow_analytics-1.3.6.dist-info/METADATA +1015 -0
  113. gitflow_analytics-1.3.6.dist-info/RECORD +122 -0
  114. gitflow_analytics-1.0.1.dist-info/METADATA +0 -463
  115. gitflow_analytics-1.0.1.dist-info/RECORD +0 -31
  116. {gitflow_analytics-1.0.1.dist-info โ†’ gitflow_analytics-1.3.6.dist-info}/WHEEL +0 -0
  117. {gitflow_analytics-1.0.1.dist-info โ†’ gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
  118. {gitflow_analytics-1.0.1.dist-info โ†’ gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
  119. {gitflow_analytics-1.0.1.dist-info โ†’ gitflow_analytics-1.3.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,470 @@
1
+ """Branch health metrics for project health assessment.
2
+
3
+ Based on 2025 software engineering best practices, this module analyzes
4
+ branch patterns to assess project health, integration practices, and
5
+ development workflow efficiency.
6
+ """
7
+
8
+ import logging
9
+ from datetime import datetime, timedelta, timezone
10
+ from typing import Any, Optional
11
+
12
+ import git
13
+ from git import Repo
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class BranchHealthAnalyzer:
19
+ """Analyze branch patterns and health metrics for repositories."""
20
+
21
+ # Default thresholds based on 2025 best practices
22
+ DEFAULT_STALE_BRANCH_DAYS = 30
23
+ DEFAULT_HEALTHY_BRANCH_COUNT = 10
24
+ DEFAULT_LONG_LIVED_BRANCH_DAYS = 14
25
+ DEFAULT_IDEAL_PR_SIZE_LINES = 200
26
+
27
+ def __init__(
28
+ self,
29
+ stale_branch_days: int = DEFAULT_STALE_BRANCH_DAYS,
30
+ healthy_branch_count: int = DEFAULT_HEALTHY_BRANCH_COUNT,
31
+ long_lived_branch_days: int = DEFAULT_LONG_LIVED_BRANCH_DAYS,
32
+ ):
33
+ """Initialize branch health analyzer with configurable thresholds."""
34
+ self.stale_branch_days = stale_branch_days
35
+ self.healthy_branch_count = healthy_branch_count
36
+ self.long_lived_branch_days = long_lived_branch_days
37
+
38
+ def analyze_repository_branches(self, repo_path: str) -> dict[str, Any]:
39
+ """Analyze all branches in a repository for health metrics.
40
+
41
+ Args:
42
+ repo_path: Path to the git repository
43
+
44
+ Returns:
45
+ Dictionary containing comprehensive branch health metrics
46
+ """
47
+ try:
48
+ repo = Repo(repo_path)
49
+ except Exception as e:
50
+ logger.error(f"Failed to open repository at {repo_path}: {e}")
51
+ return self._empty_metrics()
52
+
53
+ now = datetime.now(timezone.utc)
54
+ metrics = {
55
+ "analysis_timestamp": now.isoformat(),
56
+ "repository_path": repo_path,
57
+ "branches": {},
58
+ "summary": {},
59
+ "health_indicators": {},
60
+ "recommendations": [],
61
+ }
62
+
63
+ # Identify main/master branch
64
+ main_branch = self._identify_main_branch(repo)
65
+ if not main_branch:
66
+ logger.warning(f"Could not identify main branch for {repo_path}")
67
+ return metrics
68
+
69
+ metrics["main_branch"] = main_branch.name
70
+
71
+ # Analyze all branches
72
+ all_branches = list(repo.heads)
73
+ remote_branches = [ref for ref in repo.refs if ref.name.startswith("origin/")]
74
+
75
+ # Track branch categories
76
+ active_branches = []
77
+ stale_branches = []
78
+ long_lived_branches = []
79
+
80
+ for branch in all_branches:
81
+ branch_data = self._analyze_branch(repo, branch, main_branch, now)
82
+ metrics["branches"][branch.name] = branch_data
83
+
84
+ # Categorize branches
85
+ if branch_data["is_stale"]:
86
+ stale_branches.append(branch.name)
87
+ elif branch_data["age_days"] > self.long_lived_branch_days:
88
+ long_lived_branches.append(branch.name)
89
+ else:
90
+ active_branches.append(branch.name)
91
+
92
+ # Calculate summary metrics
93
+ metrics["summary"] = {
94
+ "total_branches": len(all_branches),
95
+ "active_branches": len(active_branches),
96
+ "stale_branches": len(stale_branches),
97
+ "long_lived_branches": len(long_lived_branches),
98
+ "remote_branches": len(remote_branches),
99
+ "branch_creation_rate_per_week": self._calculate_creation_rate(repo, all_branches),
100
+ "average_branch_age_days": self._calculate_average_age(metrics["branches"]),
101
+ "average_commits_per_branch": self._calculate_average_commits(metrics["branches"]),
102
+ }
103
+
104
+ # Calculate health indicators
105
+ metrics["health_indicators"] = self._calculate_health_indicators(metrics["summary"])
106
+
107
+ # Generate recommendations
108
+ metrics["recommendations"] = self._generate_recommendations(metrics)
109
+
110
+ return metrics
111
+
112
+ def _identify_main_branch(self, repo: Repo) -> Optional[git.Head]:
113
+ """Identify the main/master branch of the repository."""
114
+ # Common main branch names
115
+ main_branch_names = ["main", "master", "develop", "trunk"]
116
+
117
+ for name in main_branch_names:
118
+ try:
119
+ return repo.heads[name]
120
+ except IndexError:
121
+ continue
122
+
123
+ # If no standard names, use the branch with most commits
124
+ if repo.heads:
125
+ return max(repo.heads, key=lambda b: len(list(repo.iter_commits(b))))
126
+
127
+ return None
128
+
129
+ def _analyze_branch(
130
+ self, repo: Repo, branch: git.Head, main_branch: git.Head, now: datetime
131
+ ) -> dict[str, Any]:
132
+ """Analyze a single branch for health metrics."""
133
+ try:
134
+ # Get branch commits
135
+ branch_commits = list(repo.iter_commits(branch))
136
+ if not branch_commits:
137
+ return self._empty_branch_metrics(branch.name)
138
+
139
+ # Get latest commit info
140
+ latest_commit = branch_commits[0]
141
+ latest_activity = latest_commit.committed_datetime
142
+ if latest_activity.tzinfo is None:
143
+ latest_activity = latest_activity.replace(tzinfo=timezone.utc)
144
+
145
+ # Calculate age
146
+ age_delta = now - latest_activity
147
+ age_days = age_delta.days
148
+
149
+ # Check if branch is merged
150
+ is_merged = self._is_branch_merged(repo, branch, main_branch)
151
+
152
+ # Calculate divergence from main
153
+ ahead, behind = self._calculate_divergence(repo, branch, main_branch)
154
+
155
+ # Analyze commit patterns
156
+ commit_frequency = self._analyze_commit_frequency(branch_commits)
157
+
158
+ return {
159
+ "name": branch.name,
160
+ "latest_activity": latest_activity.isoformat(),
161
+ "age_days": age_days,
162
+ "is_stale": age_days > self.stale_branch_days,
163
+ "is_merged": is_merged,
164
+ "total_commits": len(branch_commits),
165
+ "unique_authors": len(set(c.author.email for c in branch_commits if c.author)),
166
+ "ahead_of_main": ahead,
167
+ "behind_main": behind,
168
+ "divergence_score": ahead + behind,
169
+ "commit_frequency": commit_frequency,
170
+ "health_score": self._calculate_branch_health_score(
171
+ age_days, ahead, behind, is_merged, len(branch_commits)
172
+ ),
173
+ }
174
+
175
+ except Exception as e:
176
+ logger.error(f"Error analyzing branch {branch.name}: {e}")
177
+ return self._empty_branch_metrics(branch.name)
178
+
179
+ def _is_branch_merged(self, repo: Repo, branch: git.Head, main_branch: git.Head) -> bool:
180
+ """Check if a branch has been merged into main."""
181
+ try:
182
+ # Get merge base
183
+ merge_base = repo.merge_base(branch, main_branch)
184
+ if not merge_base:
185
+ return False
186
+
187
+ # If branch tip is in main's history, it's merged
188
+ branch_tip = branch.commit
189
+ main_commits = set(repo.iter_commits(main_branch))
190
+ return branch_tip in main_commits
191
+
192
+ except Exception:
193
+ return False
194
+
195
+ def _calculate_divergence(
196
+ self, repo: Repo, branch: git.Head, main_branch: git.Head
197
+ ) -> tuple[int, int]:
198
+ """Calculate how many commits a branch is ahead/behind main."""
199
+ try:
200
+ # Get commits ahead (in branch but not in main)
201
+ ahead = list(repo.iter_commits(f"{main_branch.name}..{branch.name}"))
202
+
203
+ # Get commits behind (in main but not in branch)
204
+ behind = list(repo.iter_commits(f"{branch.name}..{main_branch.name}"))
205
+
206
+ return len(ahead), len(behind)
207
+
208
+ except Exception as e:
209
+ logger.error(f"Error calculating divergence: {e}")
210
+ return 0, 0
211
+
212
+ def _analyze_commit_frequency(self, commits: list[git.Commit]) -> dict[str, Any]:
213
+ """Analyze commit frequency patterns."""
214
+ if not commits:
215
+ return {"daily_average": 0, "weekly_average": 0}
216
+
217
+ # Sort commits by date
218
+ sorted_commits = sorted(commits, key=lambda c: c.committed_datetime)
219
+
220
+ # Calculate date range
221
+ first_date = sorted_commits[0].committed_datetime
222
+ last_date = sorted_commits[-1].committed_datetime
223
+
224
+ if first_date.tzinfo is None:
225
+ first_date = first_date.replace(tzinfo=timezone.utc)
226
+ if last_date.tzinfo is None:
227
+ last_date = last_date.replace(tzinfo=timezone.utc)
228
+
229
+ duration_days = max((last_date - first_date).days, 1)
230
+
231
+ return {
232
+ "daily_average": len(commits) / duration_days,
233
+ "weekly_average": (len(commits) / duration_days) * 7,
234
+ "total_days": duration_days,
235
+ }
236
+
237
+ def _calculate_branch_health_score(
238
+ self, age_days: int, ahead: int, behind: int, is_merged: bool, commit_count: int
239
+ ) -> float:
240
+ """Calculate a health score for a branch (0-100)."""
241
+ if is_merged:
242
+ return 100.0 # Merged branches are healthy
243
+
244
+ score = 100.0
245
+
246
+ # Penalize for age
247
+ if age_days > self.stale_branch_days:
248
+ score -= 40
249
+ elif age_days > self.long_lived_branch_days:
250
+ score -= 20
251
+ elif age_days > 7:
252
+ score -= 10
253
+
254
+ # Penalize for divergence
255
+ if behind > 100:
256
+ score -= 30
257
+ elif behind > 50:
258
+ score -= 20
259
+ elif behind > 20:
260
+ score -= 10
261
+
262
+ # Penalize for being too far ahead (large PRs)
263
+ if ahead > 50:
264
+ score -= 15
265
+ elif ahead > 20:
266
+ score -= 5
267
+
268
+ # Bonus for regular activity
269
+ if commit_count > 1 and age_days < 7:
270
+ score += 10
271
+
272
+ return max(0, min(100, score))
273
+
274
+ def _calculate_creation_rate(self, repo: Repo, branches: list[git.Head]) -> float:
275
+ """Calculate branch creation rate per week."""
276
+ # This is an approximation based on first commit dates
277
+ creation_dates = []
278
+
279
+ for branch in branches:
280
+ try:
281
+ commits = list(repo.iter_commits(branch))
282
+ if commits:
283
+ first_commit = commits[-1]
284
+ creation_date = first_commit.committed_datetime
285
+ if creation_date.tzinfo is None:
286
+ creation_date = creation_date.replace(tzinfo=timezone.utc)
287
+ creation_dates.append(creation_date)
288
+ except Exception:
289
+ continue
290
+
291
+ if len(creation_dates) < 2:
292
+ return 0.0
293
+
294
+ # Calculate rate over the past 4 weeks
295
+ now = datetime.now(timezone.utc)
296
+ four_weeks_ago = now - timedelta(weeks=4)
297
+ recent_branches = sum(1 for d in creation_dates if d > four_weeks_ago)
298
+
299
+ return recent_branches / 4.0
300
+
301
+ def _calculate_average_age(self, branches: dict[str, dict[str, Any]]) -> float:
302
+ """Calculate average age of active branches."""
303
+ active_ages = [
304
+ b["age_days"]
305
+ for b in branches.values()
306
+ if not b.get("is_merged", False) and b.get("age_days", 0) > 0
307
+ ]
308
+
309
+ return sum(active_ages) / len(active_ages) if active_ages else 0.0
310
+
311
+ def _calculate_average_commits(self, branches: dict[str, dict[str, Any]]) -> float:
312
+ """Calculate average commits per branch."""
313
+ commit_counts = [b.get("total_commits", 0) for b in branches.values()]
314
+ return sum(commit_counts) / len(commit_counts) if commit_counts else 0.0
315
+
316
+ def _calculate_health_indicators(self, summary: dict[str, Any]) -> dict[str, Any]:
317
+ """Calculate overall health indicators based on 2025 best practices."""
318
+ total = summary["total_branches"]
319
+ stale = summary["stale_branches"]
320
+ active = summary["active_branches"]
321
+
322
+ # Calculate health percentages
323
+ stale_percentage = (stale / total * 100) if total > 0 else 0
324
+
325
+ # Determine health status
326
+ if stale_percentage > 50:
327
+ branch_health = "poor"
328
+ elif stale_percentage > 30:
329
+ branch_health = "fair"
330
+ elif stale_percentage > 15:
331
+ branch_health = "good"
332
+ else:
333
+ branch_health = "excellent"
334
+
335
+ # Check branch count health
336
+ if total > self.healthy_branch_count * 2:
337
+ count_health = "poor"
338
+ elif total > self.healthy_branch_count:
339
+ count_health = "fair"
340
+ else:
341
+ count_health = "good"
342
+
343
+ return {
344
+ "overall_health": branch_health,
345
+ "branch_count_health": count_health,
346
+ "stale_branch_percentage": round(stale_percentage, 1),
347
+ "active_branch_percentage": round((active / total * 100) if total > 0 else 0, 1),
348
+ "integration_frequency": (
349
+ "daily" if summary.get("branch_creation_rate_per_week", 0) > 7 else "weekly"
350
+ ),
351
+ }
352
+
353
+ def _generate_recommendations(self, metrics: dict[str, Any]) -> list[str]:
354
+ """Generate actionable recommendations based on metrics."""
355
+ recommendations = []
356
+ summary = metrics["summary"]
357
+ health = metrics["health_indicators"]
358
+
359
+ # Check stale branches
360
+ if summary["stale_branches"] > 0:
361
+ recommendations.append(
362
+ f"๐Ÿงน Clean up {summary['stale_branches']} stale branches "
363
+ f"(inactive for >{self.stale_branch_days} days)"
364
+ )
365
+
366
+ # Check branch count
367
+ if summary["total_branches"] > self.healthy_branch_count:
368
+ recommendations.append(
369
+ f"๐Ÿ“Š Consider reducing active branches from {summary['total_branches']} "
370
+ f"to under {self.healthy_branch_count} for better focus"
371
+ )
372
+
373
+ # Check long-lived branches
374
+ if summary["long_lived_branches"] > 3:
375
+ recommendations.append(
376
+ f"โฑ๏ธ Review {summary['long_lived_branches']} long-lived branches - "
377
+ "consider smaller, more frequent integrations"
378
+ )
379
+
380
+ # Check for branches far behind main
381
+ behind_branches = [
382
+ name
383
+ for name, data in metrics["branches"].items()
384
+ if data.get("behind_main", 0) > 50 and not data.get("is_merged", False)
385
+ ]
386
+ if behind_branches:
387
+ recommendations.append(
388
+ f"๐Ÿ”„ Update {len(behind_branches)} branches that are >50 commits behind main"
389
+ )
390
+
391
+ # Positive feedback
392
+ if health["overall_health"] == "excellent":
393
+ recommendations.append("โœ… Excellent branch hygiene! Keep up the good practices")
394
+
395
+ return recommendations
396
+
397
+ def _empty_metrics(self) -> dict[str, Any]:
398
+ """Return empty metrics structure."""
399
+ return {
400
+ "analysis_timestamp": datetime.now(timezone.utc).isoformat(),
401
+ "repository_path": "",
402
+ "branches": {},
403
+ "summary": {},
404
+ "health_indicators": {},
405
+ "recommendations": [],
406
+ }
407
+
408
+ def _empty_branch_metrics(self, branch_name: str) -> dict[str, Any]:
409
+ """Return empty metrics for a branch."""
410
+ return {
411
+ "name": branch_name,
412
+ "latest_activity": None,
413
+ "age_days": 0,
414
+ "is_stale": False,
415
+ "is_merged": False,
416
+ "total_commits": 0,
417
+ "unique_authors": 0,
418
+ "ahead_of_main": 0,
419
+ "behind_main": 0,
420
+ "divergence_score": 0,
421
+ "commit_frequency": {"daily_average": 0, "weekly_average": 0},
422
+ "health_score": 0.0,
423
+ }
424
+
425
+ def analyze_multiple_repositories(self, repo_paths: list[str]) -> dict[str, dict[str, Any]]:
426
+ """Analyze branch health across multiple repositories.
427
+
428
+ Args:
429
+ repo_paths: List of repository paths to analyze
430
+
431
+ Returns:
432
+ Dictionary mapping repo paths to their health metrics
433
+ """
434
+ results = {}
435
+
436
+ for repo_path in repo_paths:
437
+ logger.info(f"Analyzing branch health for {repo_path}")
438
+ results[repo_path] = self.analyze_repository_branches(repo_path)
439
+
440
+ return results
441
+
442
+ def generate_aggregate_metrics(
443
+ self, multi_repo_results: dict[str, dict[str, Any]]
444
+ ) -> dict[str, Any]:
445
+ """Generate aggregate metrics across multiple repositories."""
446
+ if not multi_repo_results:
447
+ return {}
448
+
449
+ total_branches = 0
450
+ total_stale = 0
451
+ total_active = 0
452
+ all_recommendations = []
453
+
454
+ for _repo_path, metrics in multi_repo_results.items():
455
+ summary = metrics.get("summary", {})
456
+ total_branches += summary.get("total_branches", 0)
457
+ total_stale += summary.get("stale_branches", 0)
458
+ total_active += summary.get("active_branches", 0)
459
+ all_recommendations.extend(metrics.get("recommendations", []))
460
+
461
+ return {
462
+ "total_repositories": len(multi_repo_results),
463
+ "total_branches_all_repos": total_branches,
464
+ "total_stale_branches": total_stale,
465
+ "total_active_branches": total_active,
466
+ "average_branches_per_repo": (
467
+ total_branches / len(multi_repo_results) if multi_repo_results else 0
468
+ ),
469
+ "aggregate_recommendations": list(set(all_recommendations)), # Unique recommendations
470
+ }