scripts/validate-commit-msg.js (149 lines of code) (raw):

#!/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 */ 'use strict' var fs = require('fs') var util = require('util') var resolve = require('path').resolve var findup = require('findup') var semverRegex = require('semver-regex') var config = getConfig() var MAX_LENGTH = config.maxSubjectLength || 100 var IGNORED = new RegExp( util.format('(^WIP)|(^v)|(^%s$)', semverRegex().source) ) /* eslint-disable no-useless-escape */ // 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 var PATTERN = /^((fixup! |squash! )?(\w+)(?:\(([^\)\s]+)\))?: (.+))(?:\n|$)/ var MERGE_COMMIT_PATTERN = /^Merge / var error = function () { // 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 console[config.warnOnFail ? 'warn' : 'error']( 'Invalid commit message: ' + util.format.apply(null, arguments) ) console.log( 'See our specific at:', 'https://github.com/alibaba/designable/blob/master/.github/GIT_COMMIT_SPECIFIC.md' ) } var validateMessage = function (raw) { var types = (config.types = config.types || 'conventional-commit-types') // resolve types from a module if (typeof types === 'string' && types !== '*') { types = Object.keys(require(types).types) } var messageWithBody = (raw || '') .split('\n') .filter(function (str) { return str.indexOf('#') !== 0 }) .join('\n') var message = messageWithBody.split('\n').shift() if (message === '') { console.log('Aborting commit due to empty commit message.') return false } var isValid = true if (MERGE_COMMIT_PATTERN.test(message)) { console.log('Merge commit detected.') return true } if (IGNORED.test(message)) { console.log('Commit message validation ignored.') return true } var match = PATTERN.exec(message) if (!match) { error('does not match "<type>(<scope>): <subject>" !') isValid = false } else { var firstLine = match[1] var squashing = !!match[2] var type = match[3] // var scope = match[4] var subject = match[5] var SUBJECT_PATTERN = new RegExp(config.subjectPattern || '.+') var SUBJECT_PATTERN_ERROR_MSG = config.subjectPatternErrorMsg || 'subject does not match subject pattern!' if (firstLine.length > MAX_LENGTH && !squashing) { error('is longer than %d characters !', MAX_LENGTH) isValid = false } if (types !== '*' && types.indexOf(type) === -1) { error( '"%s" is not allowed type ! Valid types are: %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 return true } var argInHelp = config.helpMessage && config.helpMessage.indexOf('%s') !== -1 if (argInHelp) { console.log(config.helpMessage, messageWithBody) } else if (message) { console.log(message) } if (!argInHelp && config.helpMessage) { console.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) { var commitMsgFile = process.argv[2] || getGitFolder() + '/COMMIT_EDITMSG' var incorrectLogFile = commitMsgFile.replace( 'COMMIT_EDITMSG', 'logs/incorrect-commit-msgs' ) var hasToString = function hasToString(x) { return x && typeof x.toString === 'function' } /* eslint-disable handle-callback-err */ fs.readFile(commitMsgFile, function (err, buffer) { var msg = getCommitMessage(buffer) if (!validateMessage(msg)) { fs.appendFile(incorrectLogFile, msg + '\n', function () { process.exit(1) }) } else { process.exit(0) } function getCommitMessage(buffer) { return hasToString(buffer) && buffer.toString() } }) } function getConfig() { var pkgFile = findup.sync(process.cwd(), 'package.json') var pkg = JSON.parse(fs.readFileSync(resolve(pkgFile, 'package.json'))) return (pkg && pkg.config && pkg.config['validate-commit-msg']) || {} } function getGitFolder() { var gitDirLocation = './.git' if (!fs.existsSync(gitDirLocation)) { throw new Error('Cannot find file ' + gitDirLocation) } if (!fs.lstatSync(gitDirLocation).isDirectory()) { var unparsedText = '' + fs.readFileSync(gitDirLocation) gitDirLocation = unparsedText.substring('gitdir: '.length).trim() } if (!fs.existsSync(gitDirLocation)) { throw new Error('Cannot find file ' + gitDirLocation) } return gitDirLocation }