我来为你提供一个完整的 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" 修改提交消息

这套方案提供了:

  • 强制格式校验 - 确保所有提交符合规范
  • 清晰的错误提示 - 帮助开发者快速修正
  • 灵活的配置 - 支持自定义类型和范围
  • 易于安装 - 一键安装/卸载钩子
  • 友好的帮助信息 - 指导正确格式用法
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐