#!/usr/bin/env node /** * Git COMMIT-MSG hook for validating commit message * See https://docs.google.com/document/d/1rk04jEuGfk9kYzfqCuOlPTSJw3hEDZJTBN5E5f1SALo/edit * * Installation: * >> use ghooks, config in package.json */ const fs = require("fs"); const util = require("util"); const resolve = require("path").resolve; const findup = require("findup"); function semverRegex() { return /(?<=^v?|\sv?)(?:(?:0|[1-9]\d{0,9}?)\.){2}(?:0|[1-9]\d{0,9})(?:-(?:--+)?(?:0|[1-9]\d*|\d*[a-z]+\d*)){0,100}(?=$| |\+|\.)(?:(?<=-\S+)(?:\.(?:--?|[\da-z-]*[a-z-]\d*|0|[1-9]\d*)){1,100}?)?(?!\.)(?:\+(?:[\da-z]\.?-?){1,100}?(?!\w))?(?!\+)/gi; } const config = getConfig(); const MAX_LENGTH = config.maxSubjectLength || 100; // 忽略commit msg 中 以 ‘WIP、v’开头的字符串或语义化字符串 const IGNORED = new RegExp( util.format("(^WIP)|(^v)|(^%s$)", semverRegex().source) ); function log(...content) { return console.log("validate commit msg:", ...content); } log("校验 commit message"); // fixup! and squash! are part of Git, commits tagged with them are not intended // to be merged, cf. https://git-scm.com/docs/git-commit const PATTERN = /^((fixup! |squash! )?(\w+)(?:\(([^)\s]+)\))?: (.+))(?:\n|$)/; const MERGE_COMMIT_PATTERN = /^Merge /; const error = function (title, ...argv) { // gitx does not display it // http://gitx.lighthouseapp.com/projects/17830/tickets/294-feature-display-hook // - error-message-when-hook-fails // https://groups.google.com/group/gitx/browse_thread/thread/a03bcab60844b812 const type = config.warnOnFail ? "warn" : "error"; console[type](`validate commit msg: commit message 不符合规范`); console[type](`validate commit msg: ${title}`, ...argv); }; const validateMessage = function (raw) { console.log(raw); // config.types = config.types || resolve(process.cwd(), '.vsc-commitizen.json'); const types = config.types ? config.types.map((item) => item.label) : null; const scopes = config.scopes ? config.scopes.map((item) => item.label) : null; const messageWithBody = (raw || "") .split("\n") .filter((str) => str.indexOf("#") !== 0) .join("\n"); const message = messageWithBody.split("\n").shift(); if (message === "") { log("由于 commit message 为空,所以中止提交。"); return false; } let isValid = true; if (MERGE_COMMIT_PATTERN.test(message)) { log("检测到有 Merge commit。"); return true; } if (IGNORED.test(message)) { log("忽略 Commit message。"); return true; } const match = PATTERN.exec(message); if (!match) { error( `与规范不相符: <类型 *>[(<作用域>)]: [] <主题 *> [空行] [详细描述] [空行] [底部信息] 规范地址:http://confluence.pri.ibanyu.com/pages/viewpage.action?pageId=1737437#Git&Gitlab工作流-4、Git提交信息格式规范 类型、作用域 查看:./.vsc-commitizen.json` ); isValid = false; } else { const firstLine = match[1]; const squashing = !!match[2]; const type = match[3]; const scope = match[4]; const subject = match[5]; const SUBJECT_PATTERN = new RegExp(config.subjectPattern || ".+"); const SUBJECT_PATTERN_ERROR_MSG = config.subjectPatternErrorMsg || "主题不符合规范!"; if (firstLine.length > MAX_LENGTH && !squashing) { error("主题超过 %d 字符限制!", MAX_LENGTH); isValid = false; } if (scopes !== null && scope !== void 0 && scopes.indexOf(scope) === -1) { error('"%s" 是无效的scope!请选择以下范围: %s', scope, scopes.join(", ")); isValid = false; } if (types !== null && types.indexOf(type) === -1) { error('"%s" 是无效的type!请选择以下类型: %s', type, types.join(", ")); isValid = false; } if (!SUBJECT_PATTERN.exec(subject)) { error(SUBJECT_PATTERN_ERROR_MSG); isValid = false; } } // Some more ideas, do want anything like this ? // - Validate the rest of the message (body, footer, BREAKING CHANGE // annotations) // - allow only specific scopes (eg. fix(docs) should not be allowed ? // - auto correct the type to lower case ? // - auto correct first letter of the subject to lower case ? // - auto add empty line after subject ? // - auto remove empty () ? // - auto correct typos in type ? // - store incorrect messages, so that we can learn isValid = isValid || config.warnOnFail; if (isValid) { // exit early and skip messaging logics log("校验完成!👏 👏 👏"); return true; } const argInHelp = config.helpMessage && config.helpMessage.indexOf("%s") !== -1; if (argInHelp) { log(config.helpMessage, messageWithBody); } else if (message) { log(message); } if (!argInHelp && config.helpMessage) { log(config.helpMessage); } return false; }; // publish for testing exports.validateMessage = validateMessage; exports.getGitFolder = getGitFolder; exports.config = config; // hacky start if not run by mocha :-D istanbul ignore next if (process.argv.join("").indexOf("mocha") === -1) { const commitMsgFile = `${getGitFolder()}/COMMIT_EDITMSG`; // const incorrectLogFile = commitMsgFile.replace('COMMIT_EDITMSG', 'logs/incorrect-commit-msgs'); const hasToString = function hasToString(x) { return x && typeof x.toString === "function"; }; fs.readFile(commitMsgFile, (err, buffer) => { const msg = getCommitMessage(buffer); if (!validateMessage(msg)) { console.log(`提交信息:\n${msg}\n`); process.exit(1); } else { process.exit(0); } function getCommitMessage(buf) { return hasToString(buf) && buf.toString(); } }); } function getConfig() { const pkgFile = findup.sync(process.cwd(), "package.json"); const pkg = JSON.parse(fs.readFileSync(resolve(pkgFile, "package.json"))); if (pkg && pkg.config) { return pkg.config["validate-commit-msg"]; } const vsc = resolve(process.cwd(), "scripts/.vsc-commitizen.json"); const config = fs.existsSync(vsc) ? require(vsc) : {}; return config; } function getGitFolder() { let gitDirLocation = "./.git"; if (!fs.existsSync(gitDirLocation)) { throw new Error(`Cannot find file ${gitDirLocation}`); } if (!fs.lstatSync(gitDirLocation).isDirectory()) { const unparsedText = `${fs.readFileSync(gitDirLocation)}`; gitDirLocation = unparsedText.substring("gitdir: ".length).trim(); } if (!fs.existsSync(gitDirLocation)) { throw new Error(`Cannot find file ${gitDirLocation}`); } return gitDirLocation; }