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

154 lines
4.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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();
}