const fs = require('fs'); const path = require('path'); /** * Intelligence utilities for smart git operations */ class Intelligence { constructor(git) { this.git = git; } /** * Get file type emoji based on file extension */ static getFileEmoji(filename) { const ext = path.extname(filename).toLowerCase(); const name = path.basename(filename).toLowerCase(); // Special files if (name === 'package.json' || name === 'package-lock.json') return '๐Ÿ“ฆ'; if (name === 'readme.md' || name === 'readme') return '๐Ÿ“š'; if (name === '.gitignore') return '๐Ÿ™ˆ'; if (name === '.env' || name.startsWith('.env.')) return '๐Ÿ”'; if (name === 'dockerfile' || name === 'docker-compose.yml') return '๐Ÿณ'; if (name.includes('config') || name.includes('rc')) return 'โš™๏ธ'; if (name === 'license' || name === 'license.md') return '๐Ÿ“œ'; // Test files if (filename.includes('.test.') || filename.includes('.spec.') || filename.includes('__tests__')) return '๐Ÿงช'; // By extension const emojiMap = { '.js': '๐Ÿ“„', '.jsx': 'โš›๏ธ', '.ts': '๐Ÿ“˜', '.tsx': 'โš›๏ธ', '.css': '๐ŸŽจ', '.scss': '๐ŸŽจ', '.sass': '๐ŸŽจ', '.less': '๐ŸŽจ', '.html': '๐ŸŒ', '.htm': '๐ŸŒ', '.json': '๐Ÿ“‹', '.yaml': '๐Ÿ“‹', '.yml': '๐Ÿ“‹', '.toml': '๐Ÿ“‹', '.md': '๐Ÿ“', '.mdx': '๐Ÿ“', '.txt': '๐Ÿ“„', '.py': '๐Ÿ', '.rb': '๐Ÿ’Ž', '.go': '๐Ÿ”ท', '.rs': '๐Ÿฆ€', '.java': 'โ˜•', '.sh': '๐Ÿš', '.bash': '๐Ÿš', '.zsh': '๐Ÿš', '.sql': '๐Ÿ—ƒ๏ธ', '.graphql': '๐Ÿ”—', '.gql': '๐Ÿ”—', '.png': '๐Ÿ–ผ๏ธ', '.jpg': '๐Ÿ–ผ๏ธ', '.jpeg': '๐Ÿ–ผ๏ธ', '.gif': '๐Ÿ–ผ๏ธ', '.svg': '๐ŸŽฏ', '.mp3': '๐ŸŽต', '.wav': '๐ŸŽต', '.mp4': '๐ŸŽฌ', '.mov': '๐ŸŽฌ', '.zip': '๐Ÿ“ฆ', '.tar': '๐Ÿ“ฆ', '.gz': '๐Ÿ“ฆ', '.lock': '๐Ÿ”’', }; return emojiMap[ext] || '๐Ÿ“„'; } /** * Analyze user's commit patterns from history */ async analyzeCommitPatterns(limit = 50) { try { const log = await this.git.log({ maxCount: limit }); const commits = log.all; if (commits.length === 0) { return { hasHistory: false }; } // Analyze patterns const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|perf|build|ci|revert)(\(.+\))?:/i; const conventionalCommits = commits.filter(c => conventionalPattern.test(c.message)); // Calculate average message length const avgLength = Math.round(commits.reduce((sum, c) => sum + c.message.split('\n')[0].length, 0) / commits.length); // Detect common scopes const scopes = {}; commits.forEach(c => { const match = c.message.match(/^\w+\(([^)]+)\)/); if (match) { scopes[match[1]] = (scopes[match[1]] || 0) + 1; } }); const topScopes = Object.entries(scopes) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([scope]) => scope); // Detect if user uses emojis const emojiPattern = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]/u; const usesEmojis = commits.some(c => emojiPattern.test(c.message)); // Detect commit message style (imperative vs past tense) const imperativeWords = ['add', 'fix', 'update', 'remove', 'change', 'implement', 'refactor']; const pastTenseWords = ['added', 'fixed', 'updated', 'removed', 'changed', 'implemented', 'refactored']; let imperativeCount = 0; let pastTenseCount = 0; commits.forEach(c => { const firstWord = c.message.split(/[\s(:]/)[0].toLowerCase(); if (imperativeWords.includes(firstWord)) imperativeCount++; if (pastTenseWords.includes(firstWord)) pastTenseCount++; }); return { hasHistory: true, totalAnalyzed: commits.length, conventionalRatio: (conventionalCommits.length / commits.length * 100).toFixed(0), usesConventional: conventionalCommits.length > commits.length * 0.5, avgMessageLength: avgLength, topScopes, usesEmojis, style: imperativeCount >= pastTenseCount ? 'imperative' : 'past-tense', authors: [...new Set(commits.map(c => c.author_name))], }; } catch (error) { return { hasHistory: false, error: error.message }; } } /** * Detect semantic meaning of changes */ async detectSemanticChanges(diff) { const changes = { type: 'misc', labels: [], suggestions: [], breakingChange: false, }; if (!diff || !diff.trim()) return changes; const lines = diff.split('\n'); const addedLines = lines.filter(l => l.startsWith('+') && !l.startsWith('+++')); const removedLines = lines.filter(l => l.startsWith('-') && !l.startsWith('---')); // Detect breaking changes if (removedLines.some(l => l.includes('export ') && l.includes('function ')) || removedLines.some(l => l.includes('module.exports'))) { changes.labels.push('โš ๏ธ Potential breaking change'); changes.breakingChange = true; } // Detect new features if (addedLines.some(l => l.includes('export ') && l.includes('function ')) || addedLines.some(l => l.includes('class ')) || addedLines.some(l => l.includes('async function '))) { changes.labels.push('โœจ New functionality'); changes.type = 'feat'; } // Detect bug fixes if (addedLines.some(l => l.includes('catch') || l.includes('try {')) || addedLines.some(l => l.includes('|| null') || l.includes('?? ') || l.includes('?.'))) { changes.labels.push('๐Ÿ› Error handling'); if (changes.type === 'misc') changes.type = 'fix'; } // Detect refactoring if (addedLines.length > 20 && removedLines.length > 20 && Math.abs(addedLines.length - removedLines.length) < 10) { changes.labels.push('โ™ป๏ธ Refactoring'); if (changes.type === 'misc') changes.type = 'refactor'; } // Detect documentation if (addedLines.some(l => l.includes('/**') || l.includes('* @') || l.includes('// '))) { changes.labels.push('๐Ÿ“ Documentation'); if (changes.type === 'misc') changes.type = 'docs'; } // Detect test changes if (diff.includes('.test.') || diff.includes('.spec.') || addedLines.some(l => l.includes('describe(') || l.includes('it(') || l.includes('test('))) { changes.labels.push('๐Ÿงช Tests'); if (changes.type === 'misc') changes.type = 'test'; } // Detect dependency changes if (diff.includes('package.json') && (diff.includes('"dependencies"') || diff.includes('"devDependencies"'))) { changes.labels.push('๐Ÿ“ฆ Dependencies'); } // Detect style/formatting if (addedLines.every(l => l.trim().length < 3 || l.includes(' ') || l === '+')) { changes.labels.push('๐ŸŽจ Formatting'); if (changes.type === 'misc') changes.type = 'style'; } return changes; } /** * Suggest how to split a large changeset */ async suggestCommitSplit(status) { const files = status.files || []; if (files.length < 5) return null; const groups = { tests: [], config: [], docs: [], styles: [], core: [], }; files.forEach(file => { const f = typeof file === 'string' ? file : file.path; if (!f) return; if (f.includes('.test.') || f.includes('.spec.') || f.includes('__tests__')) { groups.tests.push(f); } else if (f.includes('config') || f.endsWith('.json') || f.endsWith('.yml') || f.endsWith('.yaml') || f.includes('rc')) { groups.config.push(f); } else if (f.endsWith('.md') || f.includes('docs/') || f.includes('README')) { groups.docs.push(f); } else if (f.endsWith('.css') || f.endsWith('.scss') || f.endsWith('.sass')) { groups.styles.push(f); } else { groups.core.push(f); } }); const suggestions = []; if (groups.tests.length > 0) suggestions.push({ type: 'test', files: groups.tests, message: 'test: add/update tests' }); if (groups.config.length > 0) suggestions.push({ type: 'chore', files: groups.config, message: 'chore: update configuration' }); if (groups.docs.length > 0) suggestions.push({ type: 'docs', files: groups.docs, message: 'docs: update documentation' }); if (groups.styles.length > 0) suggestions.push({ type: 'style', files: groups.styles, message: 'style: update styles' }); if (groups.core.length > 0) suggestions.push({ type: 'feat', files: groups.core, message: 'feat: update core functionality' }); return suggestions.length > 1 ? suggestions : null; } /** * Get session statistics */ async getSessionStats() { try { const log = await this.git.log({ maxCount: 1 }); const lastCommit = log.all[0]; let timeSinceLastCommit = 'No commits yet'; let lastCommitMessage = null; if (lastCommit) { const lastCommitDate = new Date(lastCommit.date); const now = new Date(); const diffMs = now - lastCommitDate; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffDays > 0) { timeSinceLastCommit = `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; } else if (diffHours > 0) { timeSinceLastCommit = `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; } else if (diffMins > 0) { timeSinceLastCommit = `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`; } else { timeSinceLastCommit = 'Just now'; } lastCommitMessage = lastCommit.message.split('\n')[0]; } // Get today's commits const today = new Date(); today.setHours(0, 0, 0, 0); const todayLog = await this.git.log({ '--since': today.toISOString() }); const todayCommits = todayLog.all.length; return { timeSinceLastCommit, lastCommitMessage, commitsToday: todayCommits, }; } catch (error) { return { timeSinceLastCommit: 'Unknown', lastCommitMessage: null, commitsToday: 0, }; } } /** * Detect branch context for smarter commit messages */ async detectBranchContext() { try { const branch = await this.git.branch(); const currentBranch = branch.current; // Common patterns: feat/xyz, fix/xyz, feature/xyz, bugfix/xyz, hotfix/xyz const patterns = [ { regex: /^feat(?:ure)?\/(.+)$/i, type: 'feat', scope: null }, { regex: /^fix\/(.+)$/i, type: 'fix', scope: null }, { regex: /^bugfix\/(.+)$/i, type: 'fix', scope: null }, { regex: /^hotfix\/(.+)$/i, type: 'fix', scope: null }, { regex: /^docs?\/(.+)$/i, type: 'docs', scope: null }, { regex: /^refactor\/(.+)$/i, type: 'refactor', scope: null }, { regex: /^test\/(.+)$/i, type: 'test', scope: null }, { regex: /^chore\/(.+)$/i, type: 'chore', scope: null }, { regex: /^style\/(.+)$/i, type: 'style', scope: null }, ]; for (const pattern of patterns) { const match = currentBranch.match(pattern.regex); if (match) { // Extract potential scope and description from branch name const branchPart = match[1].replace(/-/g, ' ').replace(/_/g, ' '); return { branch: currentBranch, type: pattern.type, description: branchPart, detected: true, }; } } // Check for issue references like PROJ-123 or #123 const issueMatch = currentBranch.match(/([A-Z]+-\d+|#\d+)/i); if (issueMatch) { return { branch: currentBranch, issueRef: issueMatch[1], detected: true, }; } return { branch: currentBranch, detected: false, }; } catch (error) { return { branch: 'unknown', detected: false }; } } /** * Get commit statistics for the user */ async getCommitStats(days = 30) { try { const since = new Date(); since.setDate(since.getDate() - days); const log = await this.git.log({ '--since': since.toISOString() }); const commits = log.all; if (commits.length === 0) { return { totalCommits: 0, hasData: false }; } // Group by day const byDay = {}; commits.forEach(c => { const day = new Date(c.date).toLocaleDateString(); byDay[day] = (byDay[day] || 0) + 1; }); // Calculate streaks const sortedDays = Object.keys(byDay).sort((a, b) => new Date(b) - new Date(a)); let currentStreak = 0; let longestStreak = 0; let tempStreak = 0; const today = new Date().toLocaleDateString(); const yesterday = new Date(Date.now() - 86400000).toLocaleDateString(); // Check if today or yesterday has commits if (byDay[today] || byDay[yesterday]) { sortedDays.forEach((day, i) => { if (i === 0) { tempStreak = 1; } else { const prevDate = new Date(sortedDays[i - 1]); const currDate = new Date(day); const diffDays = Math.round((prevDate - currDate) / 86400000); if (diffDays === 1) { tempStreak++; } else { if (tempStreak > longestStreak) longestStreak = tempStreak; tempStreak = 1; } } }); if (tempStreak > longestStreak) longestStreak = tempStreak; currentStreak = byDay[today] ? tempStreak : (byDay[yesterday] ? tempStreak : 0); } // Commit types breakdown const typeBreakdown = { feat: 0, fix: 0, docs: 0, style: 0, refactor: 0, test: 0, chore: 0, other: 0 }; const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|perf|build|ci|revert)/i; commits.forEach(c => { const match = c.message.match(conventionalPattern); if (match) { const type = match[1].toLowerCase(); if (typeBreakdown.hasOwnProperty(type)) { typeBreakdown[type]++; } else { typeBreakdown.other++; } } else { typeBreakdown.other++; } }); return { hasData: true, totalCommits: commits.length, daysActive: Object.keys(byDay).length, avgPerDay: (commits.length / days).toFixed(1), currentStreak, longestStreak, typeBreakdown, topDay: Object.entries(byDay).sort((a, b) => b[1] - a[1])[0], }; } catch (error) { return { hasData: false, error: error.message }; } } } module.exports = { Intelligence };