Node.js Git提交消息校验系统:基于commit-msg钩子的强制格式校验方案
这是一个完整的Git提交消息校验解决方案,包含基础校验脚本和安装脚本。校验脚本强制要求提交消息遵循"type(scope): subject"格式,支持多种提交类型和可选范围,并对主题进行长度和格式校验。安装脚本自动查找.git目录并设置钩子。当提交消息格式错误时,会显示详细错误信息和帮助说明,帮助开发者快速修正。
·
我来为你提供一个完整的 commit-msg 钩子解决方案,用于强制校验提交消息格式。
1. 基础校验脚本
创建校验脚本 (.git/hooks/commit-msg 或 scripts/verify-commit-msg.js)
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
class CommitMessageValidator {
constructor() {
this.commitMessageFile = process.argv[2] || '.git/COMMIT_EDITMSG';
this.types = ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'perf', 'build', 'ci', 'revert'];
this.scopes = ['auth', 'ui', 'api', 'db', 'config', 'deps', 'util', 'test', 'docs', 'ci'];
}
readCommitMessage() {
try {
const message = fs.readFileSync(this.commitMessageFile, 'utf8').trim();
return message;
} catch (error) {
console.error('❌ 无法读取提交消息文件:', error.message);
process.exit(1);
}
}
validateFormat(message) {
// 基础格式: type(scope): subject
const pattern = /^(\w+)(\(([^)]+)\))?: (.+)$/;
const match = message.match(pattern);
if (!match) {
return {
valid: false,
error: `❌ 提交消息格式不正确。\n期望格式: "type(scope): subject"\n实际内容: "${message}"`
};
}
const [, type, , scope, subject] = match;
return {
valid: true,
type,
scope,
subject,
raw: message
};
}
validateType(type) {
if (!this.types.includes(type)) {
return {
valid: false,
error: `❌ 提交类型 "${type}" 无效。\n允许的类型: ${this.types.join(', ')}`
};
}
return { valid: true };
}
validateScope(scope) {
if (scope && !this.scopes.includes(scope)) {
return {
valid: false,
error: `❌ 提交范围 "${scope}" 无效。\n允许的范围: ${this.scopes.join(', ')}\n或者留空不使用范围`
};
}
return { valid: true };
}
validateSubject(subject) {
if (!subject || subject.trim().length === 0) {
return {
valid: false,
error: '❌ 提交主题不能为空'
};
}
if (subject.length > 72) {
return {
valid: false,
error: `❌ 提交主题过长 (${subject.length}/72 字符)`
};
}
if (subject[0] !== subject[0].toLowerCase()) {
return {
valid: false,
error: '❌ 提交主题首字母应该小写'
};
}
if (subject.endsWith('.')) {
return {
valid: false,
error: '❌ 提交主题结尾不要使用句号'
};
}
return { valid: true };
}
showHelp() {
console.log(`
📝 提交消息格式规范:
type(scope): subject
示例:
feat(auth): 添加用户登录功能
fix(ui): 修复按钮点击无效的问题
docs(api): 更新接口文档
📋 允许的提交类型:
${this.types.map(type => ` - ${type}`).join('\n')}
🎯 允许的范围 (可选):
${this.scopes.map(scope => ` - ${scope}`).join('\n')}
💡 规则说明:
- type: 必填,描述提交的类型
- scope: 可选,描述影响的范围
- subject: 必填,简短的描述,不超过72字符
- 首字母小写,不要以句号结尾
🔧 快速修复:
使用 "git commit --amend" 修改上次提交
或 "git reset HEAD~1" 撤销上次提交
`);
}
validate() {
const message = this.readCommitMessage();
// 跳过合并提交和 revert 提交
if (message.startsWith('Merge ') || message.startsWith('Revert ')) {
process.exit(0);
}
console.log('🔍 验证提交消息:', message);
const formatResult = this.validateFormat(message);
if (!formatResult.valid) {
console.log(formatResult.error);
this.showHelp();
process.exit(1);
}
const { type, scope, subject } = formatResult;
// 校验类型
const typeResult = this.validateType(type);
if (!typeResult.valid) {
console.log(typeResult.error);
this.showHelp();
process.exit(1);
}
// 校验范围
const scopeResult = this.validateScope(scope);
if (!scopeResult.valid) {
console.log(scopeResult.error);
this.showHelp();
process.exit(1);
}
// 校验主题
const subjectResult = this.validateSubject(subject);
if (!subjectResult.valid) {
console.log(subjectResult.error);
this.showHelp();
process.exit(1);
}
console.log('✅ 提交消息格式正确!');
console.log(`📊 类型: ${type}, 范围: ${scope || '无'}, 主题: ${subject}`);
}
}
// 执行校验
new CommitMessageValidator().validate();
2. Git Hook 安装脚本
创建安装脚本 (scripts/install-commit-hook.js)
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
class CommitHookInstaller {
constructor() {
this.gitDir = this.findGitDirectory();
this.hooksDir = path.join(this.gitDir, 'hooks');
this.commitMsgHook = path.join(this.hooksDir, 'commit-msg');
this.validatorScript = path.join(process.cwd(), 'scripts/verify-commit-msg.js');
}
findGitDirectory() {
// 查找 .git 目录
let currentDir = process.cwd();
while (currentDir !== path.parse(currentDir).root) {
const gitDir = path.join(currentDir, '.git');
if (fs.existsSync(gitDir)) {
return gitDir;
}
currentDir = path.dirname(currentDir);
}
throw new Error('未找到 .git 目录');
}
ensureHooksDirectory() {
if (!fs.existsSync(this.hooksDir)) {
fs.mkdirSync(this.hooksDir, { recursive: true });
console.log('📁 创建 Git hooks 目录:', this.hooksDir);
}
}
createHookScript() {
const hookContent = `#!/bin/sh
node "${this.validatorScript}" "$1"
`;
fs.writeFileSync(this.commitMsgHook, hookContent, { mode: 0o755 });
console.log('📝 创建 commit-msg hook:', this.commitMsgHook);
}
copyValidatorScript() {
const sourceScript = `
${fs.readFileSync(path.join(__dirname, 'verify-commit-msg.js'), 'utf8')}
`.trim();
// 确保 scripts 目录存在
const scriptsDir = path.dirname(this.validatorScript);
if (!fs.existsSync(scriptsDir)) {
fs.mkdirSync(scriptsDir, { recursive: true });
}
fs.writeFileSync(this.validatorScript, sourceScript, { mode: 0o644 });
console.log('📄 创建校验脚本:', this.validatorScript);
}
install() {
try {
console.log('🚀 开始安装 Git commit-msg hook...\n');
this.ensureHooksDirectory();
this.copyValidatorScript();
this.createHookScript();
console.log('\n✅ Git commit-msg hook 安装成功!');
console.log('💡 从现在开始,所有提交消息都将自动校验格式。');
} catch (error) {
console.error('❌ 安装失败:', error.message);
process.exit(1);
}
}
uninstall() {
try {
if (fs.existsSync(this.commitMsgHook)) {
fs.unlinkSync(this.commitMsgHook);
console.log('🗑️ 删除 commit-msg hook:', this.commitMsgHook);
}
if (fs.existsSync(this.validatorScript)) {
fs.unlinkSync(this.validatorScript);
console.log('🗑️ 删除校验脚本:', this.validatorScript);
}
console.log('\n✅ Git commit-msg hook 卸载成功!');
} catch (error) {
console.error('❌ 卸载失败:', error.message);
process.exit(1);
}
}
}
// 命令行接口
const args = process.argv.slice(2);
const installer = new CommitHookInstaller();
if (args.includes('--uninstall') || args.includes('-u')) {
installer.uninstall();
} else {
installer.install();
}
3. 高级校验配置
可配置的校验器 (scripts/commit-validator.js)
const fs = require('fs');
const path = require('path');
class ConfigurableCommitValidator {
constructor(configPath = '.commitlintrc.json') {
this.config = this.loadConfig(configPath);
}
loadConfig(configPath) {
const defaultConfig = {
types: ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'perf', 'build', 'ci', 'revert'],
scopes: ['auth', 'ui', 'api', 'db', 'config', 'deps', 'util', 'test', 'docs', 'ci'],
maxSubjectLength: 72,
allowCustomScopes: false,
requireScope: false,
rules: {
'subject-case': ['lower-case'],
'subject-full-stop': ['.', false]
}
};
try {
if (fs.existsSync(configPath)) {
const customConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
return { ...defaultConfig, ...customConfig };
}
} catch (error) {
console.warn('⚠️ 配置文件读取失败,使用默认配置:', error.message);
}
return defaultConfig;
}
validate(message) {
const results = {
valid: true,
errors: [],
warnings: [],
data: {}
};
// 跳过特殊提交
if (this.isSpecialCommit(message)) {
return results;
}
const formatCheck = this.checkFormat(message);
if (!formatCheck.valid) {
results.valid = false;
results.errors.push(formatCheck.error);
return results;
}
results.data = formatCheck.data;
// 校验类型
const typeCheck = this.checkType(formatCheck.data.type);
if (!typeCheck.valid) {
results.valid = false;
results.errors.push(typeCheck.error);
}
// 校验范围
const scopeCheck = this.checkScope(formatCheck.data.scope);
if (!scopeCheck.valid) {
if (this.config.requireScope && !formatCheck.data.scope) {
results.valid = false;
results.errors.push('❌ 提交必须包含范围');
} else if (formatCheck.data.scope) {
results.errors.push(scopeCheck.error);
}
}
// 校验主题
const subjectCheck = this.checkSubject(formatCheck.data.subject);
if (!subjectCheck.valid) {
results.valid = false;
results.errors.push(...subjectCheck.errors);
}
return results;
}
isSpecialCommit(message) {
return message.startsWith('Merge ') ||
message.startsWith('Revert ') ||
message.startsWith('Initial commit');
}
checkFormat(message) {
const pattern = /^(\w+)(\(([^)]+)\))?: (.+)$/;
const match = message.match(pattern);
if (!match) {
return {
valid: false,
error: '提交消息格式不正确。期望格式: "type(scope): subject"'
};
}
const [, type, , scope, subject] = match;
return {
valid: true,
data: { type, scope, subject, raw: message }
};
}
checkType(type) {
if (!this.config.types.includes(type)) {
return {
valid: false,
error: `类型 "${type}" 无效。允许的类型: ${this.config.types.join(', ')}`
};
}
return { valid: true };
}
checkScope(scope) {
if (scope && !this.config.scopes.includes(scope) && !this.config.allowCustomScopes) {
return {
valid: false,
error: `范围 "${scope}" 无效。允许的范围: ${this.config.scopes.join(', ')}`
};
}
return { valid: true };
}
checkSubject(subject) {
const errors = [];
if (!subject || subject.trim().length === 0) {
errors.push('提交主题不能为空');
}
if (subject.length > this.config.maxSubjectLength) {
errors.push(`提交主题过长 (${subject.length}/${this.config.maxSubjectLength} 字符)`);
}
if (this.config.rules['subject-case'] &&
this.config.rules['subject-case'][0] === 'lower-case' &&
subject[0] !== subject[0].toLowerCase()) {
errors.push('提交主题首字母应该小写');
}
if (this.config.rules['subject-full-stop'] &&
this.config.rules['subject-full-stop'][1] === false &&
subject.endsWith('.')) {
errors.push('提交主题结尾不要使用句号');
}
return {
valid: errors.length === 0,
errors
};
}
}
module.exports = ConfigurableCommitValidator;
4. 配置文件
创建配置文件 (.commitlintrc.json)
{
"types": [
"feat", "fix", "docs", "style", "refactor",
"test", "chore", "perf", "build", "ci", "revert"
],
"scopes": [
"auth", "ui", "api", "db", "config",
"deps", "util", "test", "docs", "ci", "release"
],
"maxSubjectLength": 72,
"allowCustomScopes": false,
"requireScope": false,
"rules": {
"subject-case": ["lower-case"],
"subject-full-stop": [".", false]
}
}
5. 项目集成
package.json 配置
{
"scripts": {
"install-hooks": "node scripts/install-commit-hook.js",
"uninstall-hooks": "node scripts/install-commit-hook.js --uninstall",
"verify-commit": "node scripts/verify-commit-msg.js",
"commit": "git-cz"
},
"devDependencies": {
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
Husky 集成 (备选方案)
// .husky/commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
node scripts/verify-commit-msg.js "$1"
6. 使用说明
安装钩子
# 安装 commit-msg 钩子
npm run install-hooks
# 或使用 npx
npx husky install
测试提交消息
# 测试有效提交
echo "feat(auth): 添加用户登录功能" | node scripts/verify-commit-msg.js
# 测试无效提交
echo "错误的提交消息" | node scripts/verify-commit-msg.js
实际使用
# 正确的提交
git commit -m "feat(auth): 添加用户登录功能"
git commit -m "fix(ui): 修复按钮样式问题"
# 错误的提交(会被拦截)
git commit -m "随便写个提交"
git commit -m "feat(不存在的作用域): 添加功能"
7. 错误提示示例
当提交格式错误时,系统会显示:
❌ 提交消息格式不正确。
期望格式: "type(scope): subject"
实际内容: "随便写个提交"
📝 提交消息格式规范:
type(scope): subject
📋 允许的提交类型:
- feat
- fix
- docs
- style
- refactor
- test
- chore
- perf
- build
- ci
- revert
💡 使用 "git commit --amend" 修改提交消息
这套方案提供了:
- ✅ 强制格式校验 - 确保所有提交符合规范
- ✅ 清晰的错误提示 - 帮助开发者快速修正
- ✅ 灵活的配置 - 支持自定义类型和范围
- ✅ 易于安装 - 一键安装/卸载钩子
- ✅ 友好的帮助信息 - 指导正确格式用法
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)