UNPKG

git-commit-mcp

Version:

MCP server for standardizing Git commit messages

490 lines (489 loc) 20.7 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { simpleGit } from "simple-git"; const server = new McpServer({ name: "git-commit-mcp", version: "0.1.16" }); const git = simpleGit(); const commitTypeMap = { "feat": "FEAT", // 新功能:添加新特性或功能 "fix": "FIX", // 修复:解决bug或问题 "docs": "DOCS", // 文档:更新文档或注释 "style": "STYLE", // 样式:代码格式调整,不影响功能 "refactor": "REFACTOR", // 重构:代码重构,不新增功能也不修复bug "perf": "PERF", // 性能:性能优化相关更改 "test": "TEST", // 测试:添加或修改测试用例 "build": "BUILD", // 构建:影响构建系统或外部依赖的更改 "ci": "CI", // 持续集成:CI配置文件和脚本的更改 "chore": "CHORE", // 杂务:不修改源代码或测试的其他更改 "revert": "REVERT", // 回滚:撤销之前的提交 "update": "UPDATE" // 更新:更新现有功能或依赖 }; // 公共方法:分析历史提交风格 async function analyzeCommitStyle(projectPath) { const gitInstance = projectPath ? simpleGit(projectPath) : git; let commitStyleAnalysis = { avgLength: 20, preferredTypes: ['feat'], commonPatterns: [], hasPrefix: false, prefixStyle: '', descriptionStyle: 'simple', userCommitsFound: false, totalCommitsAnalyzed: 0 }; try { // 获取当前Git用户信息 let currentUser = ''; try { const userConfig = await gitInstance.getConfig('user.name'); currentUser = (userConfig && typeof userConfig === 'string') ? userConfig : ''; } catch (error) { console.warn('无法获取当前用户信息:', error); } let commits; let commitMessages = []; // 优先获取当前用户的提交记录 if (currentUser) { try { commits = await gitInstance.log({ n: 50, author: currentUser }); const userCommits = commits.all.slice(0, 10); if (userCommits.length > 0) { commitMessages = userCommits.map((commit) => commit.message); commitStyleAnalysis.userCommitsFound = true; commitStyleAnalysis.totalCommitsAnalyzed = userCommits.length; } } catch (error) { console.warn('获取用户提交记录失败:', error); } } // 如果没有找到当前用户的提交,则获取整个项目的提交记录 if (commitMessages.length === 0) { try { commits = await gitInstance.log({ n: 8 }); commitMessages = commits.all.map((commit) => commit.message); commitStyleAnalysis.userCommitsFound = false; commitStyleAnalysis.totalCommitsAnalyzed = commitMessages.length; } catch (error) { console.warn('获取项目提交记录失败:', error); } } if (commitMessages.length > 0) { // 分析平均长度 commitStyleAnalysis.avgLength = Math.round(commitMessages.reduce((acc, msg) => acc + msg.length, 0) / commitMessages.length); // 分析提交类型偏好 const typePattern = /^\[?(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|update)\]?/i; const types = commitMessages .map(msg => { const match = msg.match(typePattern); return match ? match[1].toLowerCase() : null; }) .filter(Boolean); if (types.length > 0) { const typeCount = types.reduce((acc, type) => { if (type) { acc[type] = (acc[type] || 0) + 1; } return acc; }, {}); commitStyleAnalysis.preferredTypes = Object.keys(typeCount) .sort((a, b) => typeCount[b] - typeCount[a]) .slice(0, 3); } // 分析是否使用前缀格式 const hasPrefix = commitMessages.some(msg => /^\[.+\]/.test(msg)); commitStyleAnalysis.hasPrefix = hasPrefix; if (hasPrefix) { const prefixMatch = commitMessages.find(msg => /^\[.+\]/.test(msg))?.match(/^\[(.+?)\]/); commitStyleAnalysis.prefixStyle = prefixMatch ? prefixMatch[1] : ''; } // 分析描述风格(简洁 vs 详细) const avgWordsPerCommit = commitMessages.reduce((acc, msg) => { const cleanMsg = msg.replace(/^\[.+?\]\s*/, ''); // 移除前缀 return acc + cleanMsg.split(/\s+/).length; }, 0) / commitMessages.length; commitStyleAnalysis.descriptionStyle = avgWordsPerCommit > 5 ? 'detailed' : 'simple'; } } catch (error) { console.warn('分析提交风格时出错:', error); } return commitStyleAnalysis; } // 公共方法:生成长度指导信息 function generateLengthGuidance(currentMessage, commitStyleAnalysis) { const currentLength = currentMessage.length; const targetLength = commitStyleAnalysis.avgLength; const lengthDiff = currentLength - targetLength; if (commitStyleAnalysis.totalCommitsAnalyzed > 0) { if (Math.abs(lengthDiff) <= 5) { return `长度${currentLength}字符,符合历史风格${targetLength}字符`; } else if (lengthDiff > 5) { return `长度${currentLength}字符,比历史平均${targetLength}字符长${lengthDiff}字符`; } else { return `长度${currentLength}字符,比历史平均${targetLength}字符短${Math.abs(lengthDiff)}字符`; } } else { return `当前长度${currentLength}字符`; } } // 分析Git变更 server.tool("analyze_git_changes", { projectPath: z.string().optional().describe("项目路径,默认为当前目录") }, async (args) => { try { const { projectPath } = args; if (projectPath) { git.cwd(projectPath); } const status = await git.status(); if (!status) { return { content: [ { type: "text", text: "当前目录不是有效的Git仓库" } ], isError: true }; } const hasChanges = status.staged.length > 0 || status.modified.length > 0 || status.created.length > 0 || status.deleted.length > 0 || status.renamed.length > 0 || status.not_added.length > 0; // 获取变更内容,只分析已被版本控制的文件,过滤掉未版本控制的文件 let diffContent = ""; if (hasChanges) { // 处理 renamed 文件,确保每个元素都是字符串类型 const renamedFiles = status.renamed.map((renamed) => typeof renamed === 'string' ? renamed : renamed.to); // 只包含已被版本控制的文件(排除 not_added 即未版本控制的文件) const trackedChangedFiles = [ ...status.staged, ...status.modified, ...status.created, ...status.deleted, ...renamedFiles ]; // 只对已版本控制的文件进行 diff 分析 if (trackedChangedFiles.length > 0) { try { // 获取已暂存和未暂存的变更 const stagedDiff = status.staged.length > 0 ? await git.diff(['--cached']) : ''; const unstagedDiff = trackedChangedFiles.filter(file => !status.staged.includes(file)).length > 0 ? await git.diff() : ''; diffContent = [stagedDiff, unstagedDiff].filter(Boolean).join('\n---\n'); } catch (error) { console.warn('获取diff内容时出错:', error); diffContent = `无法获取变更详情: ${error}`; } } } // 使用公共方法分析提交风格 const commitStyleAnalysis = await analyzeCommitStyle(projectPath); const changedFiles = { staged: status.staged, modified: status.modified, created: status.created, deleted: status.deleted, renamed: status.renamed, not_added: status.not_added }; // 基本状态信息 const statusInfo = { isRepo: true, branch: status.current || 'main', staged: status.staged, modified: status.modified, created: status.created, deleted: status.deleted, renamed: status.renamed, not_added: status.not_added, conflicted: status.conflicted, hasChanges: hasChanges, changedFiles: changedFiles, diffContent: diffContent, summary: hasChanges ? `检测到 ${status.staged.length + status.modified.length + status.created.length + status.deleted.length + status.renamed.length + status.not_added.length} 个文件有变更` : "没有检测到需要提交的更改", commitStyleAnalysis: commitStyleAnalysis }; return { content: [ { type: "text", text: JSON.stringify(statusInfo) } ] }; } catch (error) { return { content: [ { type: "text", text: `分析Git变更时出错: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } }); // 生成提交信息 server.tool("generate_commit_message", { projectPath: z.string().optional().describe("项目路径,默认为当前目录"), commitDescription: z.string().describe("基于代码变更分析生成的提交描述(精炼地描述代码变更原因、目的及方式))"), commitType: z.string().optional().describe("提交类型,可选值包括:feat(新功能)、fix(修复)、docs(文档)、style(样式)、refactor(重构)、perf(性能)、test(测试)、build(构建)、ci(持续集成)、chore(杂务)、revert(回滚)、update(更新),默认为 feat") }, async (args) => { try { const { projectPath, commitDescription, commitType = "feat" } = args; if (projectPath) { git.cwd(projectPath); } // 使用公共方法分析提交风格 const commitStyleAnalysis = await analyzeCommitStyle(projectPath); // 动态调整提交描述,参考历史提交风格 let finalDescription = commitDescription; if (!finalDescription || finalDescription.trim().length === 0) { finalDescription = '代码更新'; } // 使用用户历史习惯决定是否使用前缀格式 const shouldUsePrefix = commitStyleAnalysis.hasPrefix; const suggestedPrefix = commitTypeMap[commitType] || 'UPD'; // 根据代码分析结果生成提交信息 let finalCommitMessage = ''; if (shouldUsePrefix) { finalCommitMessage = `[${suggestedPrefix}] ${finalDescription}`; } else { finalCommitMessage = `${commitType}: ${finalDescription}`; } // 添加日期标识(保持用户习惯) const now = new Date(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const dateStr = `${month}${day}`; if (commitStyleAnalysis.hasPrefix || shouldUsePrefix) { finalCommitMessage += ` - ${dateStr}`; } // 使用公共方法生成长度指导信息 const styleGuidance = `(${generateLengthGuidance(finalCommitMessage, commitStyleAnalysis)})`; return { content: [ { type: "text", text: JSON.stringify({ success: true, commitMessage: finalCommitMessage, commitType: commitType, description: finalDescription, commitStyleAnalysis: commitStyleAnalysis, styleGuidance: styleGuidance, styleApplied: { prefixUsed: shouldUsePrefix, lengthGuidanceProvided: styleGuidance !== '', userStyleConsidered: true } }) } ] }; } catch (error) { return { content: [ { type: "text", text: `生成提交信息时出错: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } }); server.tool("stage_and_commit", { projectPath: z.string().optional().describe("项目路径,默认为当前目录"), files: z.array(z.string()).optional().describe("要暂存的特定文件列表,如果不指定则暂存所有修改的文件"), commitMessage: z.string().optional().describe("完整的提交信息"), commitType: z.string().optional().describe("提交类型,可选值包括:feat(新功能)、fix(修复)、docs(文档)、style(样式)、refactor(重构)、perf(性能)、test(测试)、build(构建)、ci(持续集成)、chore(杂务)、revert(回滚)、update(更新)"), customMessage: z.string().optional().describe("自定义提交信息(请控制在10-20字以内)"), autoCommit: z.boolean().optional().describe("是否在暂存后自动提交,默认为 false") }, async (args) => { try { const { projectPath, files, commitMessage, commitType, customMessage, autoCommit = false } = args; if (projectPath) { git.cwd(projectPath); } const status = await git.status(); if (!status) { return { content: [ { type: "text", text: "当前目录不是有效的Git仓库" } ], isError: true }; } const renamedFiles = status.renamed.map((r) => typeof r === 'string' ? r : r.to); const unstaged = [...status.modified, ...status.created, ...status.deleted, ...renamedFiles, ...status.not_added]; if (unstaged.length === 0 && status.staged.length === 0) { return { content: [ { type: "text", text: JSON.stringify({ success: true, message: "没有需要暂存或提交的文件", stagedFiles: [], alreadyStaged: [] }) } ] }; } // 暂存文件 let stagedFiles = []; if (unstaged.length > 0) { if (files && files.length > 0) { for (const file of files) { if (unstaged.includes(file)) { await git.add(file); stagedFiles.push(file); } } } else { await git.add('.'); stagedFiles = unstaged; } } const newStatus = await git.status(); // 如果不需要提交,只返回暂存结果 if (!autoCommit || (!commitMessage && !customMessage)) { return { content: [ { type: "text", text: JSON.stringify({ success: true, message: `成功暂存 ${stagedFiles.length} 个文件`, stagedFiles: stagedFiles, totalStaged: newStatus.staged.length, remainingUnstaged: [...newStatus.modified, ...newStatus.created, ...newStatus.deleted, ...newStatus.renamed.map((r) => typeof r === 'string' ? r : r.to), ...newStatus.not_added] }) } ] }; } // 如果需要提交 if (newStatus.staged.length === 0) { return { content: [ { type: "text", text: "没有暂存的文件可提交" } ], isError: true }; } // 使用公共方法分析提交风格 const commitStyleAnalysis = await analyzeCommitStyle(projectPath); // 生成提交信息 let finalCommitMessage = commitMessage || ""; let lengthGuidance = ""; if (customMessage) { const now = new Date(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const dateStr = `${month}${day}`; const suggestedPrefix = commitTypeMap[commitType || 'feat'] || 'UPD'; if (commitStyleAnalysis.hasPrefix) { finalCommitMessage = `[${suggestedPrefix}] ${customMessage} - ${dateStr}`; } else { finalCommitMessage = `${commitType || 'feat'}: ${customMessage}`; } // 使用公共方法生成长度指导信息 lengthGuidance = generateLengthGuidance(finalCommitMessage, commitStyleAnalysis); } else if (commitMessage) { // 使用公共方法生成长度指导信息 lengthGuidance = generateLengthGuidance(commitMessage, commitStyleAnalysis); } if (!finalCommitMessage) { return { content: [ { type: "text", text: "提交时需要提供提交信息" } ], isError: true }; } // 执行提交 const commitResult = await git.commit(finalCommitMessage); return { content: [ { type: "text", text: JSON.stringify({ success: true, commitMessage: finalCommitMessage, commitHash: commitResult.commit, summary: commitResult.summary, stagedFiles: stagedFiles, lengthGuidance: lengthGuidance, commitStyleAnalysis: { avgLength: commitStyleAnalysis.avgLength, hasPrefix: commitStyleAnalysis.hasPrefix, userCommitsFound: commitStyleAnalysis.userCommitsFound, totalCommitsAnalyzed: commitStyleAnalysis.totalCommitsAnalyzed }, changedFiles: { staged: newStatus.staged, modified: newStatus.modified, created: newStatus.created, deleted: newStatus.deleted, renamed: newStatus.renamed, not_added: newStatus.not_added } }) } ] }; } catch (error) { return { content: [ { type: "text", text: `Git操作出错: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } }); async function main() { try { const transport = new StdioServerTransport(); await server.connect(transport); } catch (error) { process.exit(1); } } main().catch(() => process.exit(1));