UNPKG

@zubenelakrab/gitstats

Version:

Powerful Git repository analyzer with comprehensive statistics and insights

135 lines 5.27 kB
import { dirname } from 'node:path'; /** * Calculate bus factor: minimum number of contributors that would need to * leave for a significant knowledge loss * * Algorithm: For a file/directory, how many people have >50% of the knowledge? * If 1 person = bus factor 1 (critical) * If 2-3 people = bus factor 2-3 (risky) * If 4+ people = healthier */ function calculateBusFactor(authorCommits, threshold = 0.5) { if (authorCommits.size === 0) return 0; if (authorCommits.size === 1) return 1; const totalCommits = Array.from(authorCommits.values()).reduce((a, b) => a + b, 0); if (totalCommits === 0) return 0; // Sort authors by contribution (descending) const sorted = Array.from(authorCommits.entries()).sort((a, b) => b[1] - a[1]); // Count how many authors cover the threshold of total commits let accumulated = 0; let busFactor = 0; for (const [, commits] of sorted) { accumulated += commits; busFactor++; if (accumulated / totalCommits >= threshold) { break; } } return busFactor; } /** * Determine risk level based on bus factor */ function getRiskLevel(busFactor) { if (busFactor <= 1) return 'high'; if (busFactor <= 2) return 'medium'; return 'low'; } /** * Analyzer for bus factor and knowledge distribution */ export class BusFactorAnalyzer { name = 'busfactor-analyzer'; description = 'Analyzes knowledge distribution and bus factor risk'; async analyze(commits, _config) { // Track commits per author at repository level const repoAuthorCommits = new Map(); // Track commits per author per directory const dirAuthorCommits = new Map(); // Track commits per author per file (for critical area detection) const fileAuthorCommits = new Map(); // Author lookup const authorLookup = new Map(); for (const commit of commits) { const authorKey = commit.author.email.toLowerCase(); authorLookup.set(authorKey, commit.author); // Repository level const repoCount = repoAuthorCommits.get(authorKey) || 0; repoAuthorCommits.set(authorKey, repoCount + 1); for (const file of commit.files) { // File level if (!fileAuthorCommits.has(file.path)) { fileAuthorCommits.set(file.path, new Map()); } const fileMap = fileAuthorCommits.get(file.path); const fileCount = fileMap.get(authorKey) || 0; fileMap.set(authorKey, fileCount + 1); // Directory level const dir = dirname(file.path); if (!dirAuthorCommits.has(dir)) { dirAuthorCommits.set(dir, new Map()); } const dirMap = dirAuthorCommits.get(dir); const dirCount = dirMap.get(authorKey) || 0; dirMap.set(authorKey, dirCount + 1); } } // Calculate overall bus factor const overall = calculateBusFactor(repoAuthorCommits); // Calculate bus factor by directory const byDirectory = {}; for (const [dir, authorMap] of dirAuthorCommits) { byDirectory[dir] = calculateBusFactor(authorMap); } // Find critical areas (files with bus factor = 1 and significant commits) const criticalAreas = []; const minCommitsForCritical = 5; // Only flag files with at least 5 commits for (const [path, authorMap] of fileAuthorCommits) { const totalCommits = Array.from(authorMap.values()).reduce((a, b) => a + b, 0); if (totalCommits < minCommitsForCritical) continue; const fileBusFactor = calculateBusFactor(authorMap); const risk = getRiskLevel(fileBusFactor); // Only add to critical if high or medium risk if (risk === 'high' || risk === 'medium') { // Find sole contributor if bus factor is 1 let soleContributor; if (fileBusFactor === 1) { const topAuthor = Array.from(authorMap.entries()) .sort((a, b) => b[1] - a[1])[0]; if (topAuthor) { soleContributor = authorLookup.get(topAuthor[0]); } } criticalAreas.push({ path, busFactor: fileBusFactor, soleContributor, risk, }); } } // Sort critical areas by risk (high first) then by path criticalAreas.sort((a, b) => { const riskOrder = { high: 0, medium: 1, low: 2 }; const riskDiff = riskOrder[a.risk] - riskOrder[b.risk]; if (riskDiff !== 0) return riskDiff; return a.path.localeCompare(b.path); }); return { overall, byDirectory, criticalAreas, }; } } export function createBusFactorAnalyzer() { return new BusFactorAnalyzer(); } //# sourceMappingURL=busfactor-analyzer.js.map