UNPKG

5.82 kBJavaScriptView Raw
1
2/**
3 * Git COMMIT-MSG hook for validating commit message
4 * See https://docs.google.com/document/d/1rk04jEuGfk9kYzfqCuOlPTSJw3hEDZJTBN5E5f1SALo/edit
5 *
6 * Installation:
7 * >> cd <angular-repo>
8 * >> ln -s ../../validate-commit-msg.js .git/hooks/commit-msg
9 */
10
11'use strict';
12
13var fs = require('fs');
14var util = require('util');
15var resolve = require('path').resolve;
16var findup = require('findup');
17var semverRegex = require('semver-regex')
18
19var config = getConfig();
20var MAX_LENGTH = config.maxSubjectLength || 100;
21var IGNORED = new RegExp(util.format('(^WIP)|(^%s$)', semverRegex().source));
22
23// 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
24var PATTERN = /^((fixup! |squash! )?(\w+)(?:\(([^\)\s]+)\))?: (.+))(?:\n|$)/;
25var MERGE_COMMIT_PATTERN = /^Merge /;
26var error = function() {
27 // gitx does not display it
28 // http://gitx.lighthouseapp.com/projects/17830/tickets/294-feature-display-hook-error-message-when-hook-fails
29 // https://groups.google.com/group/gitx/browse_thread/thread/a03bcab60844b812
30 console[config.warnOnFail
31 ? 'warn'
32 : 'error']('INVALID COMMIT MSG: ' + util.format.apply(null, arguments));
33};
34
35var validateMessage = function(raw) {
36 var types = config.types = config.types || 'conventional-commit-types';
37
38 // resolve types from a module
39 if (typeof types === 'string' && types !== '*') {
40 types = Object.keys(require(types).types);
41 }
42
43 var messageWithBody = (raw || '').split('\n').filter(function(str) {
44 return str.indexOf('#') !== 0;
45 }).join('\n');
46
47 var message = messageWithBody.split('\n').shift();
48
49 if (message === '') {
50 console.log('Aborting commit due to empty commit message.');
51 return false;
52 }
53
54 var isValid = true;
55
56 if (MERGE_COMMIT_PATTERN.test(message)) {
57 console.log('Merge commit detected.');
58 return true
59 }
60
61 if (IGNORED.test(message)) {
62 console.log('Commit message validation ignored.');
63 return true;
64 }
65
66 var match = PATTERN.exec(message);
67
68 if (!match) {
69 error('提交信息格式不通过,具体格式请按照:"<type>(<scope>): <subject>" !');
70 isValid = false;
71 } else {
72 var firstLine = match[1];
73 var squashing = !!match[2];
74 var type = match[3];
75 var scope = match[4];
76 var subject = match[5];
77
78 var SUBJECT_PATTERN = new RegExp(config.subjectPattern || '.+');
79 var SUBJECT_PATTERN_ERROR_MSG = config.subjectPatternErrorMsg || 'subject does not match subject pattern!';
80
81 if (firstLine.length > MAX_LENGTH && !squashing) {
82 error('is longer than %d characters !', MAX_LENGTH);
83 isValid = false;
84 }
85
86 if (types !== '*' && types.indexOf(type) === -1) {
87 error('"%s" is not allowed type ! Valid types are: %s', type, types.join(', '));
88 isValid = false;
89 }
90
91 if (!SUBJECT_PATTERN.exec(subject)) {
92 error(SUBJECT_PATTERN_ERROR_MSG);
93 isValid = false;
94 }
95 }
96
97 // Some more ideas, do want anything like this ?
98 // - Validate the rest of the message (body, footer, BREAKING CHANGE annotations)
99 // - allow only specific scopes (eg. fix(docs) should not be allowed ?
100 // - auto correct the type to lower case ?
101 // - auto correct first letter of the subject to lower case ?
102 // - auto add empty line after subject ?
103 // - auto remove empty () ?
104 // - auto correct typos in type ?
105 // - store incorrect messages, so that we can learn
106
107 isValid = isValid || config.warnOnFail;
108
109 if (isValid) { // exit early and skip messaging logics
110 return true;
111 }
112
113 var argInHelp = config.helpMessage && config.helpMessage.indexOf('%s') !== -1;
114
115 if (argInHelp) {
116 console.log(config.helpMessage, messageWithBody);
117 } else if (message) {
118 console.log(message);
119 }
120
121 if (!argInHelp && config.helpMessage) {
122 console.log(config.helpMessage);
123 }
124
125 return false;
126};
127
128// publish for testing
129exports.validateMessage = validateMessage;
130exports.getGitFolder = getGitFolder;
131exports.config = config;
132
133// hacky start if not run by mocha :-D
134// istanbul ignore next
135if (process.argv.join('').indexOf('mocha') === -1) {
136
137 var commitMsgFile = process.argv[2] || getGitFolder() + '/COMMIT_EDITMSG';
138 var incorrectLogFile = commitMsgFile.replace('COMMIT_EDITMSG', 'logs/incorrect-commit-msgs');
139
140 var hasToString = function hasToString(x) {
141 return x && typeof x.toString === 'function';
142 };
143
144 fs.readFile(commitMsgFile, function(err, buffer) {
145 var msg = getCommitMessage(buffer);
146
147 if (!validateMessage(msg)) {
148 fs.appendFile(incorrectLogFile, msg + '\n', function() {
149 process.exit(1);
150 });
151 } else {
152 process.exit(0);
153 }
154
155 function getCommitMessage(buffer) {
156 return hasToString(buffer) && buffer.toString();
157 }
158 });
159}
160
161function getConfig() {
162 var pkgFile = findup.sync(process.cwd(), 'package.json');
163 var pkg = JSON.parse(fs.readFileSync(resolve(pkgFile, 'package.json')));
164 return pkg && pkg.config && pkg.config['validate-commit-msg'] || {};
165}
166
167function getGitFolder() {
168 var gitDirLocation = './.git';
169 if (!fs.existsSync(gitDirLocation)) {
170 throw new Error('Cannot find file ' + gitDirLocation);
171 }
172
173 if (!fs.lstatSync(gitDirLocation).isDirectory()) {
174 var unparsedText = '' + fs.readFileSync(gitDirLocation);
175 gitDirLocation = unparsedText.substring('gitdir: '.length).trim();
176 }
177
178 if (!fs.existsSync(gitDirLocation)) {
179 throw new Error('Cannot find file ' + gitDirLocation);
180 }
181
182 return gitDirLocation;
183}