All files / hooks commit-msg.js

100% Statements 66/66
100% Branches 31/31
100% Functions 5/5
100% Lines 66/66
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186            1x 1x 1x   1x     1x 1x 1x   1x 1x                                                                       13x 14x     13x   13x 2x 2x     11x   11x 1x 1x     10x 2x 2x     8x   8x 2x 2x   6x 6x 6x   6x 6x 6x   6x   6x 2x 2x     6x 1x 1x     6x 1x 1x     6x 1x 1x     6x 1x 1x                           8x         1x   1x 1x   1x       1x 1x         1x 1x 1x                   1x       1x 1x   3x       1x             8x     1x 1x    
#!/usr/bin/env node
 
/**
 * 代码参考了 npm package: validate-commit-msg
 */
 
var fs = require('fs')
var path = require('path')
var util = require('util')
 
var findup = require('../libs/fs/findup')
 
// 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 IGNORED_PATTERN = new RegExp(util.format('(^WIP)|(^%s$)', require('semver-regex')().source))
 
var isPackageFileExists = false
var config = {
  command: __filename,
  warnOnFail: false,
  showHelp: false,
  maxSubjectLength: 80,
  subjectPattern: '.+',
  types: ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'revert']
}
 
// istanbul ignore next
if (module.parent === null) {
  // spawn('git', ['rev-parse',  '--show-toplevel'], { stdio: ['ignore', 'pipe', 2] });
  var dir = findup.git()
 
  try {
    var pkg = require(findup.pkg())
    var pkgConfig = pkg.config && pkg.config.hooks && pkg.config.hooks['commit-msg']
    if (typeof pkgConfig === 'object') for (var k in pkgConfig) config[k] = pkgConfig[k]
    isPackageFileExists = true
  } catch (e) {}
 
  var commitMsgFile = process.argv[2] || path.join(dir, 'COMMIT_EDITMSG')
  var commitErrFile = commitMsgFile.replace('COMMIT_EDITMSG', 'ERROR_COMMIT_EDITMSG')
 
  var msg = fs.readFileSync(commitMsgFile).toString()
 
  if (validate(msg)) {
    process.exit(0)
  } else {
    outputHelp()
    fs.appendFileSync(commitErrFile, msg + '\n')
    process.exit(1)
  }
}
 
function validate(raw) {
  var messageWithBody = (raw || '').split(/\r?\n/).filter(function(str) {
    return str.indexOf('#') !== 0
  }).join('\n')
 
  var message = messageWithBody.split('\n').shift().trim()
 
  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_PATTERN.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].trim()
    var firstLetter = subject[0]
    var lastLetter = subject[subject.length - 1]
 
    var SUBJECT_PATTERN = new RegExp(config.subjectPattern)
 
    if (firstLine.length > config.maxSubjectLength && !squashing) {
      error('is longer than %d characters !', config.maxSubjectLength)
      isValid = false
    }
 
    if (config.types !== '*' && config.types.indexOf(type) === -1) {
      error('"%s" is not allowed type !', type)
      isValid = false
    }
 
    if (!SUBJECT_PATTERN.exec(subject)) {
      error('subject does not match subject pattern !')
      isValid = false
    }
 
    if (firstLetter.toLowerCase() !== firstLetter) {
      error('don\'t capitalize first letter !')
      isValid = false
    }
 
    if (/[.,!;]/.test(lastLetter)) {
      error('no punctuation mark at the end !')
      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
 
  return isValid || config.warnOnFail
}
 
function outputHelp() {
  // istanbul ignore if
  if (!config.showHelp) return
 
  console.log('\x1b[90m\nCurrent commit-msg hook config:')
  console.log(JSON.stringify(formatObject(config), null, 4).replace(/^|\n/g, '\n    '))
  // istanbul ignore if
  if (isPackageFileExists) {
    console.log('\nYou can overwrite the config use `config.hooks.commit-msg` in package.json')
  }
 
  console.log('\n============================================================================\n')
  console.log(
    'Git Commit Message Guides: \n'
    + '  https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit\n'
    + '  http://chris.beams.io/posts/git-commit/'
  )
  console.log('\nChangelog keywrods: Added, Changed, Breaks, Deprecated, Removed, Fixed, Security')
  console.log('\nExample:\n')
  console.log(
    '  fix($compile): couple of unit tests for IE9\n'
    + '  \n'
    + '  Older IEs serialize html uppercased, but IE9 does not...\n'
    + '  Would be better to expect case insensitive, unfortunately jasmine does\n'
    + '  not allow to user regexps for throw expectations.\n'
    + '  \n'
    + '  Closes #392, #400\n'
    + '  Breaks foo.bar api, foo.baz should be used instead'
  )
  console.log('\x1b[0m')
}
 
function formatObject(obj) {
  obj = JSON.parse(JSON.stringify(obj))
  for (var key in obj) {
    // istanbul ignore if
    if (Array.isArray(obj[key])) obj[key] = '[ ' + obj[key].join(', ') + ' ]'
    // istanbul ignore next
    else if (typeof obj[key] === 'object') obj[key] = formatObject(obj[key])
  }
  return obj
}
 
function error() {
  // 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 MSG: ' + util.format.apply(null, arguments))
}
 
module.exports = validate
module.exports.help = outputHelp