bookworm-smart-assistant/hooks/commit-message-lint.js

154 lines
4.3 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* PreToolUse Hook: git commit 消息规范检查
* 匹配器: Bash
* 触发: 检测到 git commit -m 验证消息格式
* 退出码: 0=放行, 2=格式不符(ask确认)
*
* 规范:
* - 不得为空
* - 首行不超过 72 字符
* - 建议包含类型前缀: feat/fix/refactor/docs/test/chore/style/perf/ci
* - 允许中英文混合
* - Co-Authored-By 行不计入首行长度
*/
const COMMIT_TYPES = [
'feat', 'fix', 'refactor', 'docs', 'test', 'chore',
'style', 'perf', 'ci', 'build', 'revert', 'hotfix'
];
const TYPE_PATTERN = new RegExp(
`^(${COMMIT_TYPES.join('|')})(\\(.+\\))?[!]?:\\s.+`,
'i'
);
const readStdin = require('./lib/read-stdin.js');
/**
* bash 命令中提取 git commit 消息
* @param {string} command - bash 命令字符串
* @returns {string} 提取到的消息无法提取则返回空字符串
*/
function extractCommitMessage(command) {
if (!command || !command.match(/git\s+commit/i)) {
return '';
}
// 匹配 heredoc 格式 (支持任意分隔符: EOF, COMMIT_MSG, END 等)
const heredocMatch = command.match(/cat\s+<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1/);
if (heredocMatch) {
return heredocMatch[2].trim();
}
// 匹配 -m "message" 格式(支持转义引号)
const doubleQuoteMatch = command.match(/git\s+commit\s+.*-m\s+"((?:[^"\\]|\\.)*)"/);
const singleQuoteMatch = command.match(/git\s+commit\s+.*-m\s+'([^']*)'/);
const simpleMatch = doubleQuoteMatch || singleQuoteMatch;
if (simpleMatch) {
return simpleMatch[1].replace(/\\(.)/g, '$1').trim();
}
return '';
}
/**
* commit 消息进行规范检查
* @param {string} message - commit 消息
* @returns {string[]} 警告列表空数组表示通过
*/
function lintMessage(message) {
const warnings = [];
const lines = (message || '').split('\n');
const firstLine = lines[0].trim();
if (firstLine.length === 0) {
warnings.push('commit 消息首行不得为空');
}
if (firstLine.length > 72) {
warnings.push(`首行 ${firstLine.length} 字符,建议不超过 72 字符`);
}
// 检查类型前缀 (建议但不强制)
if (!TYPE_PATTERN.test(firstLine)) {
// 中文消息也允许,只要不是太短
if (firstLine.length < 4) {
warnings.push('commit 消息过短,请提供更详细的描述');
}
}
return warnings;
}
function main() {
readStdin({ maxSize: 1024 * 1024 }).then(input => {
if (input.tool_name !== 'Bash') {
process.exit(0);
}
const command = (input.tool_input && input.tool_input.command) || '';
const message = extractCommitMessage(command);
if (!message) {
process.exit(0);
}
const warnings = lintMessage(message);
if (warnings.length === 0) {
process.exit(0);
}
const firstLine = message.split('\n')[0].trim();
const output = {
hookSpecificOutput: {
permissionDecision: 'ask'
},
systemMessage: `[commit-lint] ${warnings.join('; ')}。当前消息: "${firstLine.substring(0, 50)}${firstLine.length > 50 ? '...' : ''}"`
};
process.stderr.write(JSON.stringify(output));
process.exit(2);
}).catch((e) => {
// PreToolUse: 异常时 fail-closed (ask),不静默放行
process.stderr.write(JSON.stringify({
hookSpecificOutput: {
permissionDecision: 'ask'
},
systemMessage: `[commit-lint] Hook 解析异常: ${e.message}`
}));
process.exit(2);
});
}
/**
* 可导出的 commit 消息检查函数 ( dispatcher 调用)
* @param {string} command - bash 命令
* @param {object} input - 完整的 hook stdin 输入
* @returns {object|null} 检查结果null 表示放行
* { decision: 'ask', message: string }
*/
function checkCommit(command, input) {
const message = extractCommitMessage(command);
if (!message) return null;
const warnings = lintMessage(message);
if (warnings.length === 0) return null;
const firstLine = message.split('\n')[0].trim();
return {
decision: 'ask',
message: `[commit-lint] ${warnings.join('; ')}。当前消息: "${firstLine.substring(0, 50)}${firstLine.length > 50 ? '...' : ''}"`,
};
}
// 导出纯函数供单元测试和 dispatcher 使用
if (typeof module !== 'undefined') {
module.exports = { COMMIT_TYPES, TYPE_PATTERN, extractCommitMessage, lintMessage, checkCommit };
}
if (require.main === module) {
main();
}