UNPKG

@zubenelakrab/gitstats

Version:

Powerful Git repository analyzer with comprehensive statistics and insights

209 lines 8.82 kB
export class CollaborationAnalyzer { name = 'collaboration-analyzer'; description = 'Analyzes team collaboration patterns'; async analyze(commits, _config) { if (commits.length === 0) { return this.emptyStats(); } // Track file authors const fileAuthors = new Map(); const authorFiles = new Map(); const fileHistory = new Map(); for (const commit of commits) { const authorKey = commit.author.email.toLowerCase(); if (!authorFiles.has(authorKey)) { authorFiles.set(authorKey, new Set()); } for (const file of commit.files) { // Track authors per file if (!fileAuthors.has(file.path)) { fileAuthors.set(file.path, new Set()); } fileAuthors.get(file.path).add(authorKey); authorFiles.get(authorKey).add(file.path); // Track file history for handoffs if (!fileHistory.has(file.path)) { fileHistory.set(file.path, []); } fileHistory.get(file.path).push({ author: authorKey, date: commit.date, }); } } // Calculate collaboration pairs const pairCollaboration = new Map(); for (const [file, authors] of fileAuthors) { if (authors.size < 2) continue; const authorList = Array.from(authors); for (let i = 0; i < authorList.length; i++) { for (let j = i + 1; j < authorList.length; j++) { const pairKey = [authorList[i], authorList[j]].sort().join('|'); if (!pairCollaboration.has(pairKey)) { pairCollaboration.set(pairKey, { sharedFiles: new Set(), commits: 0 }); } pairCollaboration.get(pairKey).sharedFiles.add(file); pairCollaboration.get(pairKey).commits++; } } } const collaborationPairs = Array.from(pairCollaboration.entries()) .map(([key, data]) => { const [author1, author2] = key.split('|'); const sharedFiles = data.sharedFiles.size; const maxPossibleShared = Math.min(authorFiles.get(author1)?.size || 0, authorFiles.get(author2)?.size || 0); const collaborationStrength = maxPossibleShared > 0 ? (sharedFiles / maxPossibleShared) * 100 : 0; return { author1, author2, sharedFiles, sharedCommits: data.commits, collaborationStrength: Math.round(collaborationStrength), }; }) .sort((a, b) => b.sharedFiles - a.sharedFiles); // Shared files analysis const sharedFiles = Array.from(fileAuthors.entries()) .filter(([, authors]) => authors.size > 1) .map(([path, authors]) => { const history = fileHistory.get(path) || []; let conflicts = 0; // Count sequential modifications by different authors for (let i = 1; i < history.length; i++) { if (history[i].author !== history[i - 1].author) { conflicts++; } } return { path, authors: Array.from(authors), authorCount: authors.size, potentialConflicts: conflicts, }; }) .sort((a, b) => b.authorCount - a.authorCount); // Calculate handoffs const handoffMap = new Map(); for (const [file, history] of fileHistory) { const sorted = [...history].sort((a, b) => a.date.getTime() - b.date.getTime()); for (let i = 1; i < sorted.length; i++) { if (sorted[i].author !== sorted[i - 1].author) { const key = `${file}|${sorted[i - 1].author}|${sorted[i].author}`; handoffMap.set(key, (handoffMap.get(key) || 0) + 1); } } } const handoffs = Array.from(handoffMap.entries()) .map(([key, count]) => { const [file, fromAuthor, toAuthor] = key.split('|'); return { file, fromAuthor, toAuthor, count }; }) .sort((a, b) => b.count - a.count) .slice(0, 50); // Find lone wolves const loneWolves = []; for (const [authorKey, files] of authorFiles) { let soloFiles = 0; for (const file of files) { if (fileAuthors.get(file)?.size === 1) { soloFiles++; } } const soloPercentage = (soloFiles / files.size) * 100; if (soloPercentage > 50 && files.size > 5) { // Find author name from commits const authorCommit = commits.find(c => c.author.email.toLowerCase() === authorKey); loneWolves.push({ name: authorCommit?.author.name || authorKey, email: authorKey, soloFiles, totalFiles: files.size, soloPercentage, }); } } loneWolves.sort((a, b) => b.soloPercentage - a.soloPercentage); // Simple clustering based on shared files const clusters = this.detectClusters(collaborationPairs, authorFiles); // Collaboration score const collaborationScore = this.calculateCollaborationScore(collaborationPairs, sharedFiles, loneWolves, authorFiles.size); return { collaborationPairs: collaborationPairs.slice(0, 30), sharedFiles: sharedFiles.slice(0, 30), handoffs, clusters, collaborationScore, loneWolves, }; } detectClusters(pairs, authorFiles) { // Simple clustering: group authors with high collaboration const clusters = []; const assigned = new Set(); const strongPairs = pairs.filter(p => p.collaborationStrength > 30); for (const pair of strongPairs) { if (assigned.has(pair.author1) && assigned.has(pair.author2)) continue; const members = new Set(); members.add(pair.author1); members.add(pair.author2); // Find other authors that collaborate with both for (const otherPair of strongPairs) { if (members.has(otherPair.author1) || members.has(otherPair.author2)) { members.add(otherPair.author1); members.add(otherPair.author2); } } if (members.size >= 2) { // Find common files const membersList = Array.from(members); let commonFiles = []; if (membersList.length > 0) { const firstFiles = authorFiles.get(membersList[0]) || new Set(); commonFiles = Array.from(firstFiles).filter(file => membersList.every(m => authorFiles.get(m)?.has(file))); } clusters.push({ members: membersList, commonFiles: commonFiles.slice(0, 10), commitOverlap: membersList.length, }); membersList.forEach(m => assigned.add(m)); } } return clusters.slice(0, 5); } calculateCollaborationScore(pairs, sharedFiles, loneWolves, totalAuthors) { if (totalAuthors <= 1) return 0; let score = 50; // Bonus for collaboration pairs const avgCollaboration = pairs.length > 0 ? pairs.reduce((sum, p) => sum + p.collaborationStrength, 0) / pairs.length : 0; score += avgCollaboration * 0.3; // Bonus for shared files const sharedPercentage = sharedFiles.length > 0 ? Math.min(100, sharedFiles.length) : 0; score += sharedPercentage * 0.1; // Penalty for lone wolves const loneWolfPercentage = (loneWolves.length / totalAuthors) * 100; score -= loneWolfPercentage * 0.5; return Math.max(0, Math.min(100, Math.round(score))); } emptyStats() { return { collaborationPairs: [], sharedFiles: [], handoffs: [], clusters: [], collaborationScore: 0, loneWolves: [], }; } } export function createCollaborationAnalyzer() { return new CollaborationAnalyzer(); } //# sourceMappingURL=collaboration-analyzer.js.map