UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

450 lines 16.4 kB
/** * V3 Git Commit Hook * * TypeScript conversion of V2 git-commit-hook.sh. * Provides conventional commit formatting, JIRA ticket extraction, * co-author addition, and commit message validation. * * @module v3/shared/hooks/safety/git-commit */ import { HookEvent, HookPriority, } from '../types.js'; const COMMIT_TYPE_PATTERNS = [ { keywords: ['add', 'implement', 'create', 'introduce', 'new'], type: 'feat', description: 'A new feature', }, { keywords: ['fix', 'resolve', 'repair', 'patch', 'correct', 'bug'], type: 'fix', description: 'A bug fix', }, { keywords: ['doc', 'docs', 'readme', 'comment', 'documentation'], type: 'docs', description: 'Documentation changes', }, { keywords: ['style', 'format', 'lint', 'whitespace', 'prettier'], type: 'style', description: 'Code style changes', }, { keywords: ['refactor', 'restructure', 'reorganize', 'extract', 'simplify'], type: 'refactor', description: 'Code refactoring', }, { keywords: ['perf', 'performance', 'optimize', 'speed', 'faster'], type: 'perf', description: 'Performance improvements', }, { keywords: ['test', 'tests', 'spec', 'coverage', 'unittest'], type: 'test', description: 'Adding or updating tests', }, { keywords: ['build', 'webpack', 'rollup', 'vite', 'esbuild', 'package'], type: 'build', description: 'Build system changes', }, { keywords: ['ci', 'github action', 'workflow', 'pipeline', 'travis', 'jenkins'], type: 'ci', description: 'CI/CD changes', }, { keywords: ['chore', 'update', 'upgrade', 'bump', 'dependency', 'deps'], type: 'chore', description: 'Maintenance tasks', }, { keywords: ['revert', 'rollback', 'undo'], type: 'revert', description: 'Reverting changes', }, ]; /** * Ticket patterns (JIRA, GitHub, etc.) */ const TICKET_PATTERNS = [ { name: 'JIRA', pattern: /([A-Z]{2,10}-\d+)/, format: (match) => match[1], }, { name: 'GitHub Issue', pattern: /#(\d+)/, format: (match) => `#${match[1]}`, }, { name: 'Linear', pattern: /([A-Z]{2,10}-[A-Z0-9]+)/, format: (match) => match[1], }, ]; const DEFAULT_CO_AUTHOR = { name: 'Claude Opus 4.5', email: 'noreply@anthropic.com', }; const DEFAULT_CONFIG = { maxSubjectLength: 72, maxBodyLength: 100, requireConventional: true, addCoAuthor: true, coAuthor: DEFAULT_CO_AUTHOR, addClaudeReference: true, }; /** * Git Commit Hook Manager */ export class GitCommitHook { registry; config; constructor(registry, config) { this.registry = registry; this.config = { ...DEFAULT_CONFIG, ...config }; this.registerHooks(); } /** * Register git commit hooks */ registerHooks() { // We use PreCommand hook since there's no specific commit hook event // In practice, this would be called when detecting git commit commands this.registry.register(HookEvent.PreCommand, this.handlePreCommit.bind(this), HookPriority.Normal, { name: 'git-commit:pre-commit' }); } /** * Handle pre-commit (when a git commit command is detected) */ async handlePreCommit(context) { const command = context.command?.command || ''; // Only process git commit commands if (!command.includes('git commit')) { return { success: true }; } // Extract message from command if present const messageMatch = command.match(/-m\s+["']([^"']+)["']/); if (!messageMatch) { return { success: true }; // No message to process } const message = messageMatch[1]; const branchName = context.metadata?.branchName; const result = await this.processCommitMessage(message, branchName); // Modify the command with the new message if (result.success && result.modifiedMessage !== message) { const modifiedCommand = command.replace(/-m\s+["'][^"']+["']/, `-m "${result.modifiedMessage.replace(/"/g, '\\"')}"`); return { ...result, data: { command: { ...context.command, command: modifiedCommand, }, }, }; } return result; } /** * Process commit message */ async processCommitMessage(message, branchName) { const originalMessage = message; let modifiedMessage = message; const validationIssues = []; const suggestions = []; // Parse existing message structure const { subject, body, footer } = this.parseMessage(message); // Detect commit type const commitType = this.detectCommitType(subject); // Add commit type prefix if not present and type was detected if (commitType && !this.hasConventionalPrefix(subject)) { modifiedMessage = `${commitType}: ${this.lowercaseFirstLetter(subject)}`; suggestions.push(`Added conventional commit prefix: ${commitType}`); } else if (!commitType && this.config.requireConventional && !this.hasConventionalPrefix(subject)) { // No type detected but conventional commits are required - suggest adding a prefix suggestions.push('Consider adding a conventional commit prefix (feat:, fix:, docs:, etc.)'); } // Validate subject length if (subject.length > this.config.maxSubjectLength) { validationIssues.push({ type: 'length', severity: 'warning', description: `Subject line exceeds ${this.config.maxSubjectLength} characters`, suggestedFix: 'Shorten the subject line', }); } // Extract ticket reference from branch name let ticketReference; if (branchName) { ticketReference = this.extractTicket(branchName); if (ticketReference && !modifiedMessage.includes(ticketReference)) { modifiedMessage = this.addTicketReference(modifiedMessage, ticketReference); suggestions.push(`Added ticket reference: ${ticketReference}`); } } // Add Claude Code reference and co-author let coAuthorAdded = false; if (this.config.addClaudeReference || this.config.addCoAuthor) { const additions = []; if (this.config.addClaudeReference) { additions.push('\n\nGenerated with [Claude Code](https://claude.com/claude-code)'); } if (this.config.addCoAuthor) { additions.push(`\n\nCo-Authored-By: ${this.config.coAuthor.name} <${this.config.coAuthor.email}>`); coAuthorAdded = true; } // Only add if not already present for (const addition of additions) { const searchStr = addition.trim().split('\n')[0]; if (!modifiedMessage.includes(searchStr)) { modifiedMessage += addition; } } } // Validate conventional commit format if (this.config.requireConventional) { const conventionalIssues = this.validateConventional(modifiedMessage); validationIssues.push(...conventionalIssues); } return { success: true, originalMessage, modifiedMessage, commitType, ticketReference, coAuthorAdded, validationIssues: validationIssues.length > 0 ? validationIssues : undefined, suggestions: suggestions.length > 0 ? suggestions : undefined, }; } /** * Parse commit message into parts */ parseMessage(message) { const parts = message.split('\n\n'); return { subject: parts[0] || '', body: parts[1], footer: parts.slice(2).join('\n\n'), }; } /** * Detect commit type from message */ detectCommitType(message) { const lowerMessage = message.toLowerCase(); // First check if message already has conventional prefix const prefixMatch = lowerMessage.match(/^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?:/); if (prefixMatch) { return prefixMatch[1]; } // Score each commit type based on keyword matches // More specific/unique keywords get higher weight const scores = new Map(); // High-priority patterns (check these first as they're more specific) const priorityPatterns = [ // Test patterns - high priority because "add tests" should be 'test' not 'feat' { pattern: /\b(test|tests|spec|specs|unittest|unit test|testing)\b/i, type: 'test', weight: 3 }, // Docs patterns { pattern: /\b(doc|docs|documentation|readme|comment|comments)\b/i, type: 'docs', weight: 3 }, // Revert patterns { pattern: /\b(revert|rollback|undo)\b/i, type: 'revert', weight: 3 }, // Fix patterns (bug-specific) { pattern: /\b(fix|bug|bugfix|resolve|patch|hotfix)\b/i, type: 'fix', weight: 2 }, // CI patterns { pattern: /\b(ci|github action|workflow|pipeline|travis|jenkins|circleci)\b/i, type: 'ci', weight: 3 }, // Build patterns { pattern: /\b(build|webpack|rollup|vite|esbuild|bundler|package\.json)\b/i, type: 'build', weight: 2 }, // Perf patterns { pattern: /\b(perf|performance|optimize|speed|faster|slow)\b/i, type: 'perf', weight: 2 }, // Refactor patterns { pattern: /\b(refactor|restructure|reorganize|extract|simplify|clean)\b/i, type: 'refactor', weight: 2 }, // Style patterns { pattern: /\b(style|format|lint|whitespace|prettier|eslint)\b/i, type: 'style', weight: 2 }, // Chore patterns - specifically for dependencies { pattern: /\b(dependency|dependencies|deps|bump|upgrade version)\b/i, type: 'chore', weight: 2 }, // Generic update is lower priority (could be chore or other) { pattern: /\b(update)\b/i, type: 'chore', weight: 1 }, // Feat patterns (generic add/create/implement) { pattern: /\b(add|implement|create|introduce|new feature)\b/i, type: 'feat', weight: 1 }, ]; // Calculate scores for each pattern for (const { pattern, type, weight } of priorityPatterns) { if (pattern.test(lowerMessage)) { const currentScore = scores.get(type) || 0; scores.set(type, currentScore + weight); } } // Find highest scoring type let maxScore = 0; let detectedType; for (const [type, score] of scores) { if (score > maxScore) { maxScore = score; detectedType = type; } } return detectedType; } /** * Check if message has conventional commit prefix */ hasConventionalPrefix(message) { return /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?:/.test(message.toLowerCase()); } /** * Lowercase first letter of a string */ lowercaseFirstLetter(str) { // Don't lowercase if it's an acronym or proper noun if (/^[A-Z]{2,}/.test(str)) { return str; } return str.charAt(0).toLowerCase() + str.slice(1); } /** * Extract ticket reference from branch name */ extractTicket(branchName) { for (const { pattern, format } of TICKET_PATTERNS) { const match = pattern.exec(branchName); if (match) { return format(match); } } return undefined; } /** * Add ticket reference to message */ addTicketReference(message, ticket) { const parts = message.split('\n\n'); const subject = parts[0]; const rest = parts.slice(1).join('\n\n'); // Add refs line if (rest) { return `${subject}\n\nRefs: ${ticket}\n\n${rest}`; } return `${subject}\n\nRefs: ${ticket}`; } /** * Validate conventional commit format */ validateConventional(message) { const issues = []; const lines = message.split('\n'); const subject = lines[0] || ''; // Check for conventional prefix if (!this.hasConventionalPrefix(subject)) { issues.push({ type: 'format', severity: 'warning', description: 'Missing conventional commit prefix', suggestedFix: 'Add a prefix like feat:, fix:, docs:, etc.', }); } // Check subject line starts with lowercase (after prefix) const afterPrefix = subject.replace(/^[a-z]+(\(.+\))?: /, ''); if (afterPrefix && /^[A-Z]/.test(afterPrefix) && !/^[A-Z]{2,}/.test(afterPrefix)) { issues.push({ type: 'format', severity: 'info', description: 'Subject should start with lowercase (conventional style)', suggestedFix: 'Use lowercase for the first word after the prefix', }); } // Check for period at end of subject if (subject.endsWith('.')) { issues.push({ type: 'format', severity: 'info', description: 'Subject line should not end with a period', suggestedFix: 'Remove the trailing period', }); } // Check body line lengths for (let i = 1; i < lines.length; i++) { const line = lines[i]; if (line.length > this.config.maxBodyLength && !line.startsWith('Co-Authored-By:')) { issues.push({ type: 'body', severity: 'info', description: `Line ${i + 1} exceeds ${this.config.maxBodyLength} characters`, suggestedFix: 'Wrap long lines in the commit body', }); break; // Only report first occurrence } } // Check for breaking change indicator if (subject.includes('!:') || message.includes('BREAKING CHANGE:')) { issues.push({ type: 'breaking', severity: 'info', description: 'Breaking change detected - ensure changelog is updated', }); } return issues; } /** * Process commit message manually */ async process(message, branchName) { return this.processCommitMessage(message, branchName); } /** * Format a commit message with heredoc-style for git */ formatForGit(message) { // Escape for heredoc usage return `$(cat <<'EOF' ${message} EOF )`; } /** * Generate a commit command with formatted message */ generateCommitCommand(message) { return `git commit -m "${this.formatForGit(message)}"`; } /** * Get commit type description */ getCommitTypeDescription(type) { const pattern = COMMIT_TYPE_PATTERNS.find(p => p.type === type); return pattern?.description || 'Unknown commit type'; } /** * Get all available commit types */ getAllCommitTypes() { return COMMIT_TYPE_PATTERNS.map(p => ({ type: p.type, description: p.description, })); } /** * Update configuration */ setConfig(config) { this.config = { ...this.config, ...config }; } /** * Get current configuration */ getConfig() { return { ...this.config }; } } /** * Create git commit hook */ export function createGitCommitHook(registry, config) { return new GitCommitHook(registry, config); } //# sourceMappingURL=git-commit.js.map