UNPKG

@zubenelakrab/gitstats

Version:

Powerful Git repository analyzer with comprehensive statistics and insights

294 lines 11.8 kB
import { daysDifference } from '../utils/date.js'; import { execGit } from '../utils/exec.js'; export class BranchesAnalyzer { name = 'branches-analyzer'; description = 'Analyzes branch patterns and health'; async analyze(_commits, config, branches) { if (!branches || branches.length === 0) { return this.emptyStats(); } const now = new Date(); // Separate local and remote const localBranches = branches.filter(b => !b.isRemote); const remoteBranches = branches.filter(b => b.isRemote); // Categorize branches const staleBranches = []; const activeBranches = []; const orphanBranches = []; // Get merged branches for orphan detection let mergedBranches = new Set(); try { const merged = await execGit(['branch', '--merged', 'HEAD'], config.repoPath); mergedBranches = new Set(merged.split('\n') .map(b => b.trim().replace('* ', '')) .filter(Boolean)); } catch { // Ignore errors } for (const branch of branches) { const daysSinceCommit = daysDifference(branch.lastCommitDate, now); if (daysSinceCommit > 90) { // Stale branch let recommendation = 'Consider deleting'; if (daysSinceCommit > 365) { recommendation = 'Strongly recommend deleting - over 1 year old'; } else if (daysSinceCommit > 180) { recommendation = 'Should be reviewed for deletion'; } staleBranches.push({ name: branch.name, lastCommitDate: branch.lastCommitDate, daysSinceCommit, isRemote: branch.isRemote, recommendation, }); // Check if orphan (stale + not merged) const baseName = branch.name.replace('origin/', ''); if (!mergedBranches.has(baseName) && daysSinceCommit > 60) { orphanBranches.push({ name: branch.name, lastCommitDate: branch.lastCommitDate, daysSinceCommit, reason: 'Not merged and inactive for 60+ days', }); } } else { // Active branch activeBranches.push({ name: branch.name, lastCommitDate: branch.lastCommitDate, daysSinceCommit, isRemote: branch.isRemote, isCurrent: branch.isCurrent, }); } } // Sort by age staleBranches.sort((a, b) => b.daysSinceCommit - a.daysSinceCommit); activeBranches.sort((a, b) => a.daysSinceCommit - b.daysSinceCommit); orphanBranches.sort((a, b) => b.daysSinceCommit - a.daysSinceCommit); // Analyze naming patterns const namingPatterns = this.analyzeNamingPatterns(branches); // Calculate branch ages const branchAges = branches.map(b => daysDifference(b.lastCommitDate, now)); const averageBranchAge = branchAges.length > 0 ? branchAges.reduce((a, b) => a + b, 0) / branchAges.length : 0; const sortedByAge = [...branches].sort((a, b) => a.lastCommitDate.getTime() - b.lastCommitDate.getTime()); const oldestBranch = sortedByAge.length > 0 ? { name: sortedByAge[0].name, age: daysDifference(sortedByAge[0].lastCommitDate, now) } : null; const newestBranch = sortedByAge.length > 0 ? { name: sortedByAge[sortedByAge.length - 1].name, age: daysDifference(sortedByAge[sortedByAge.length - 1].lastCommitDate, now) } : null; // Calculate health score const branchHealthScore = this.calculateHealthScore(staleBranches.length, orphanBranches.length, branches.length); // Calculate branch lifecycle metrics const branchLifecycle = this.calculateBranchLifecycle(branches, mergedBranches, namingPatterns, now); return { totalBranches: branches.length, localBranches: localBranches.length, remoteBranches: remoteBranches.length, staleBranches, activeBranches, orphanBranches, namingPatterns, averageBranchAge: Math.round(averageBranchAge), oldestBranch, newestBranch, branchHealthScore, branchLifecycle, }; } analyzeNamingPatterns(branches) { const patterns = { 'feature/*': [], 'bugfix/*': [], 'hotfix/*': [], 'release/*': [], 'develop': [], 'main/master': [], 'other': [], }; for (const branch of branches) { const name = branch.name.replace('origin/', ''); if (name.startsWith('feature/') || name.startsWith('feat/')) { patterns['feature/*'].push(name); } else if (name.startsWith('bugfix/') || name.startsWith('bug/') || name.startsWith('fix/')) { patterns['bugfix/*'].push(name); } else if (name.startsWith('hotfix/') || name.startsWith('hot/')) { patterns['hotfix/*'].push(name); } else if (name.startsWith('release/') || name.startsWith('rel/')) { patterns['release/*'].push(name); } else if (name === 'develop' || name === 'dev' || name === 'development') { patterns['develop'].push(name); } else if (name === 'main' || name === 'master') { patterns['main/master'].push(name); } else { patterns['other'].push(name); } } const result = []; const descriptions = { 'feature/*': 'Feature branches following GitFlow', 'bugfix/*': 'Bug fix branches', 'hotfix/*': 'Hotfix branches for urgent fixes', 'release/*': 'Release branches', 'develop': 'Development integration branch', 'main/master': 'Main production branch', 'other': 'Non-standard naming', }; for (const [pattern, names] of Object.entries(patterns)) { if (names.length > 0) { result.push({ pattern, count: names.length, examples: names.slice(0, 5), description: descriptions[pattern], }); } } result.sort((a, b) => b.count - a.count); return result; } calculateHealthScore(staleCount, orphanCount, totalCount) { if (totalCount === 0) return 100; let score = 100; // Penalty for stale branches const stalePercentage = (staleCount / totalCount) * 100; score -= stalePercentage * 0.5; // Bigger penalty for orphan branches const orphanPercentage = (orphanCount / totalCount) * 100; score -= orphanPercentage * 1.5; return Math.max(0, Math.min(100, Math.round(score))); } calculateBranchLifecycle(branches, mergedBranches, namingPatterns, now) { // Activity breakdown let activeCount = 0; let inactiveCount = 0; let staleCount = 0; let shortLivedBranches = 0; let longLivedBranches = 0; let branchesCreatedLast30Days = 0; let branchesCreatedLast90Days = 0; for (const branch of branches) { const daysSinceCommit = daysDifference(branch.lastCommitDate, now); if (daysSinceCommit < 30) { activeCount++; branchesCreatedLast30Days++; branchesCreatedLast90Days++; } else if (daysSinceCommit < 90) { inactiveCount++; branchesCreatedLast90Days++; } else { staleCount++; } // Lifespan estimates based on branch age if (daysSinceCommit < 7) { shortLivedBranches++; } else if (daysSinceCommit > 30) { longLivedBranches++; } } const totalBranches = branches.length || 1; const activePercentage = Math.round((activeCount / totalBranches) * 100); // Merge statistics const mergedCount = mergedBranches.size; const unmergedCount = totalBranches - mergedCount; const mergeRate = Math.round((mergedCount / totalBranches) * 100); // Estimate average lifespan (rough approximation based on activity) const branchAges = branches.map(b => daysDifference(b.lastCommitDate, now)); const estimatedAvgLifespan = branchAges.length > 0 ? Math.round(branchAges.reduce((a, b) => a + b, 0) / branchAges.length) : 0; // Workflow detection const hasFeatureBranches = namingPatterns.some(p => p.pattern === 'feature/*' && p.count > 0); const hasDevelop = namingPatterns.some(p => p.pattern === 'develop' && p.count > 0); const hasReleaseBranches = namingPatterns.some(p => p.pattern === 'release/*' && p.count > 0); const hasHotfixBranches = namingPatterns.some(p => p.pattern === 'hotfix/*' && p.count > 0); const hasGitFlow = hasDevelop && hasFeatureBranches && (hasReleaseBranches || hasHotfixBranches); const hasTrunkBased = !hasDevelop && totalBranches <= 5; let workflowType = 'unknown'; if (hasGitFlow) { workflowType = 'gitflow'; } else if (hasTrunkBased) { workflowType = 'trunk-based'; } else if (hasFeatureBranches) { workflowType = 'feature-branch'; } else if (hasFeatureBranches && hasDevelop) { workflowType = 'mixed'; } return { activeCount, inactiveCount, staleCount, activePercentage, mergedBranches: mergedCount, unmergedBranches: unmergedCount, mergeRate, estimatedAvgLifespan, shortLivedBranches, longLivedBranches, hasGitFlow, hasTrunkBased, workflowType, branchesCreatedLast30Days, branchesCreatedLast90Days, }; } emptyStats() { return { totalBranches: 0, localBranches: 0, remoteBranches: 0, staleBranches: [], activeBranches: [], orphanBranches: [], namingPatterns: [], averageBranchAge: 0, oldestBranch: null, newestBranch: null, branchHealthScore: 100, branchLifecycle: { activeCount: 0, inactiveCount: 0, staleCount: 0, activePercentage: 100, mergedBranches: 0, unmergedBranches: 0, mergeRate: 0, estimatedAvgLifespan: 0, shortLivedBranches: 0, longLivedBranches: 0, hasGitFlow: false, hasTrunkBased: false, workflowType: 'unknown', branchesCreatedLast30Days: 0, branchesCreatedLast90Days: 0, }, }; } } export function createBranchesAnalyzer() { return new BranchesAnalyzer(); } //# sourceMappingURL=branches-analyzer.js.map