1 | 'use strict';
|
2 |
|
3 | const la = require('lazy-ass');
|
4 | const check = require('check-more-types');
|
5 | const ggit = require('ggit');
|
6 |
|
7 | var child = require('child_process');
|
8 | var path = require('path');
|
9 | var fs = require('fs');
|
10 |
|
11 | const log = require('debug')('pre-git');
|
12 |
|
13 | var Promise = require('bluebird');
|
14 |
|
15 | var label = 'pre-commit:';
|
16 |
|
17 | var gitPrefix = process.env.GIT_PREFIX || '';
|
18 |
|
19 | function isAtRoot(dir) {
|
20 | return dir === '/';
|
21 | }
|
22 |
|
23 | function isPackageAmongFiles(dir) {
|
24 | var files = fs.readdirSync(dir);
|
25 | return files.indexOf('package.json') >= 0;
|
26 | }
|
27 |
|
28 | function verifyValidDirectory(dir) {
|
29 | la(check.unemptyString(dir), 'missing dir');
|
30 |
|
31 | var cwd = process.cwd();
|
32 | if (isAtRoot(dir)) {
|
33 | throw new Error('Could not find package.json starting from ' + cwd);
|
34 | } else if (!dir || dir === '.') {
|
35 | throw new Error('Cannot find package.json from unspecified directory via ' + cwd);
|
36 | }
|
37 | }
|
38 |
|
39 | function findPackage(dir) {
|
40 | var cwd = process.cwd();
|
41 | if (! dir) {
|
42 | dir = path.join(cwd, gitPrefix);
|
43 | }
|
44 |
|
45 | if (isPackageAmongFiles(dir)) {
|
46 | log('found package in folder', dir);
|
47 | return path.join(dir, 'package.json');
|
48 | }
|
49 |
|
50 | verifyValidDirectory(dir);
|
51 |
|
52 |
|
53 | var parentPath = path.dirname(dir);
|
54 | if (parentPath === dir) {
|
55 | throw new Error('Cannot got up the folder to find package.json from ' + cwd);
|
56 | }
|
57 | return findPackage(parentPath);
|
58 | }
|
59 |
|
60 | function getPackage() {
|
61 | var filename = findPackage();
|
62 | la(check.unemptyString(filename), 'could not find package');
|
63 | var pkg = require(filename);
|
64 | return pkg;
|
65 | }
|
66 |
|
67 |
|
68 |
|
69 | function getProjRoot() {
|
70 | return new Promise(function (resolve, reject) {
|
71 | child.exec('git rev-parse --show-toplevel', function onRoot(err, output) {
|
72 | if (err) {
|
73 | console.error('');
|
74 | console.error(label, 'Failed to find git root. Cannot run the tests.');
|
75 | console.error(err);
|
76 | console.error('');
|
77 | return reject(new Error('Failed to find git in the project root'));
|
78 | }
|
79 |
|
80 | var gitRoot = output.trim();
|
81 | var projRoot = path.join(gitRoot, gitPrefix);
|
82 | var pkg;
|
83 | try {
|
84 | var file = findPackage();
|
85 | pkg = require(file);
|
86 | projRoot = path.dirname(file);
|
87 | }
|
88 | catch (e) {
|
89 | return resolve(gitRoot);
|
90 | }
|
91 |
|
92 | if (pkg['pre-git-cwd']) {
|
93 | projRoot = path.resolve(path.join(gitRoot, pkg['pre-git-cwd']));
|
94 | }
|
95 | return resolve(projRoot);
|
96 | });
|
97 | });
|
98 | }
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | function failure(label, err) {
|
107 | console.error('');
|
108 | console.error(label, 'You\'ve failed to pass all the hooks.');
|
109 | console.error(label);
|
110 |
|
111 | const chalk = require('chalk');
|
112 | if (err instanceof Error) {
|
113 | console.error(label, 'An Error was thrown from command');
|
114 | if (err.ran) {
|
115 | console.error(chalk.supportsColor ? chalk.bold.yellow(err.ran) : err.ran);
|
116 | }
|
117 |
|
118 | const stack = err.stack.split('\n');
|
119 | const firstLine = stack.shift();
|
120 | console.error(chalk.supportsColor ? chalk.red(firstLine) : firstLine);
|
121 | console.error(label);
|
122 | stack.forEach(function trace(line) {
|
123 | console.error(label, ' ' + line.trim());
|
124 | });
|
125 | } else {
|
126 | console.error(label, chalk.supportsColor ? chalk.red(err) : err);
|
127 | }
|
128 |
|
129 | const skipOption = label === 'pre-push' ? '--no-verify' : '-n (--no-verify)';
|
130 | const skipOptionText = chalk.supportsColor ? chalk.bold(skipOption) : skipOption;
|
131 | console.error(label);
|
132 | console.error(label, 'You can skip the git hook by running with', skipOptionText);
|
133 | console.error(label);
|
134 | console.error(label, 'But this is not advised as your tests are obviously failing.');
|
135 | console.error('');
|
136 |
|
137 | process.exit(1);
|
138 | }
|
139 |
|
140 | function getConfig() {
|
141 | const packageName = 'pre-git';
|
142 | const pkg = getPackage();
|
143 | return pkg.config && pkg.config[packageName];
|
144 | }
|
145 |
|
146 | function getConfigProperty(propertyName) {
|
147 | const config = getConfig();
|
148 | if (!config) {
|
149 | return false;
|
150 | }
|
151 | const property = config[propertyName];
|
152 |
|
153 | if (!property) {
|
154 | return false;
|
155 | }
|
156 |
|
157 | return property;
|
158 | }
|
159 |
|
160 | function hasEnabledOption(config) {
|
161 | return 'enabled' in config;
|
162 | }
|
163 |
|
164 | function getTasks(label) {
|
165 | var pkg = getPackage();
|
166 | la(check.object(pkg), 'missing package', pkg);
|
167 |
|
168 | const config = getConfig();
|
169 | if (!config) {
|
170 | return;
|
171 | }
|
172 |
|
173 | if (hasEnabledOption(config) && !config.enabled) {
|
174 | return;
|
175 | }
|
176 |
|
177 | var run = pkg[label] ||
|
178 | config &&
|
179 | config[label];
|
180 |
|
181 | if (check.string(run)) {
|
182 | run = [run];
|
183 | }
|
184 | log('tasks for label "%s" are', label, run);
|
185 | return run;
|
186 | }
|
187 |
|
188 | function hasUntrackedFiles() {
|
189 | const config = getConfig();
|
190 | if (!config) {
|
191 | return Promise.resolve(false);
|
192 | }
|
193 | if(config['allow-untracked-files']) {
|
194 | return Promise.resolve(false);
|
195 | }
|
196 | return ggit.untrackedFiles()
|
197 | .then(function (names) {
|
198 | return check.unempty(names);
|
199 | });
|
200 | }
|
201 |
|
202 | function runTask(root, task) {
|
203 | console.log('executing task "' + task + '"');
|
204 |
|
205 | const options = {
|
206 | cwd: root,
|
207 | env: process.env
|
208 | };
|
209 |
|
210 | return new Promise(function (resolve, reject) {
|
211 | const proc = child.exec(task, options);
|
212 | proc.stdout.on('data', process.stdout.write.bind(process.stdout));
|
213 | proc.stderr.on('data', process.stderr.write.bind(process.stderr));
|
214 | proc.on('close', function onTaskFinished(code) {
|
215 | if (code > 0) {
|
216 | let err = new Error(task + ' closed with code ' + code);
|
217 | err.ran = task;
|
218 | return reject(err);
|
219 | }
|
220 | return resolve('task "' + task + '" passed');
|
221 | });
|
222 | });
|
223 | }
|
224 |
|
225 | function checkInputs(label) {
|
226 | if (typeof label !== 'string' || !label) {
|
227 | throw new Error('Expected string label (pre-commit, pre-push)');
|
228 | }
|
229 | }
|
230 |
|
231 | function skipPrecommit() {
|
232 | return process.argv[2] !== 'origin';
|
233 | }
|
234 |
|
235 | function getSkipTest(label) {
|
236 | const skipConditions = {
|
237 | 'pre-push': skipPrecommit
|
238 | };
|
239 | function dontSkip() {
|
240 | return false;
|
241 | }
|
242 | const skip = skipConditions[label] || dontSkip;
|
243 | return skip;
|
244 | }
|
245 |
|
246 |
|
247 | function runAtRoot(root, label) {
|
248 | log('running %s at root %s', label, root);
|
249 | log('cli arguments', process.argv);
|
250 | la(check.unemptyString(label), 'missing label', label);
|
251 | const skip = getSkipTest(label);
|
252 | if (skip()) {
|
253 | log('skipping tasks for', label);
|
254 | return Promise.resolve();
|
255 | }
|
256 |
|
257 | function showError(message) {
|
258 | console.error('');
|
259 | console.error(label, message);
|
260 | console.error('');
|
261 | return Promise.reject(new Error(message));
|
262 | }
|
263 |
|
264 | function noUntrackedFiles(foundUntrackedFiles) {
|
265 | if (foundUntrackedFiles) {
|
266 | return showError('Cannot commit with untracked files present.');
|
267 | }
|
268 | }
|
269 |
|
270 | if (!root) {
|
271 | return showError('Failed to find git root. Cannot run the tests.');
|
272 | }
|
273 |
|
274 | function runTasksForLabel() {
|
275 | var tasks = getTasks(label);
|
276 | log('tasks for %s', label, tasks);
|
277 |
|
278 | if (!tasks || !tasks.length) {
|
279 | console.log('');
|
280 | console.log(label, 'Nothing the hook needs to do. Bailing out.');
|
281 | console.log('');
|
282 | return Promise.resolve('Nothing to do for ' + label);
|
283 | }
|
284 |
|
285 | const runTaskAt = runTask.bind(null, root);
|
286 | return Promise.each(tasks, runTaskAt);
|
287 | }
|
288 |
|
289 | if (label === 'pre-commit') {
|
290 | return hasUntrackedFiles()
|
291 | .then(noUntrackedFiles)
|
292 | .then(runTasksForLabel);
|
293 | }
|
294 | return runTasksForLabel();
|
295 | }
|
296 |
|
297 | function run(hookLabel) {
|
298 | log('running', hookLabel);
|
299 | checkInputs(hookLabel);
|
300 |
|
301 | label = hookLabel;
|
302 |
|
303 |
|
304 | return getProjRoot()
|
305 | .tap((root) => log('running', hookLabel, 'in', root))
|
306 | .then((root) => runAtRoot(root, hookLabel))
|
307 | .catch((err) => failure(hookLabel, err));
|
308 | }
|
309 |
|
310 | function errorMessage(err) {
|
311 | return err instanceof Error ? err.message : err;
|
312 | }
|
313 |
|
314 | function printError(x) {
|
315 | console.error(errorMessage(x) || 'Unknown error');
|
316 | }
|
317 |
|
318 | function isBuiltInWizardName(name) {
|
319 | la(check.unemptyString(name), 'invalid name', name);
|
320 | const builtIn = {
|
321 | simple: true,
|
322 | conventional: true,
|
323 | 'cz-conventional-changelog': true
|
324 | };
|
325 | return builtIn[name];
|
326 | }
|
327 |
|
328 | function loadWizard(name) {
|
329 | la(check.unemptyString(name), 'missing commit wizard name', name);
|
330 | const moduleNames = {
|
331 | simple: 'simple-commit-message',
|
332 | conventional: 'conventional-commit-message',
|
333 | 'cz-conventional-changelog': 'conventional-commit-message'
|
334 | };
|
335 | const loadName = moduleNames[name];
|
336 | la(check.unemptyString(loadName),
|
337 | 'Unknown commit message wizard name', name);
|
338 | log('loading wizard', loadName, 'for name', name);
|
339 | return require(loadName);
|
340 | }
|
341 |
|
342 | function getWizardName() {
|
343 | const config = getConfig();
|
344 | const defaultName = 'simple';
|
345 | log('commit message wizard name from', config);
|
346 | if (!config) {
|
347 | log('no config, using default name', defaultName);
|
348 | return defaultName;
|
349 | }
|
350 | if (config.wizard) {
|
351 | la(check.unemptyString(config.wizard), 'expected wizard name', config.wizard);
|
352 | log('using wizard name', config.wizard);
|
353 | return config.wizard;
|
354 | }
|
355 |
|
356 | const value = config['commit-msg'];
|
357 | if (check.unemptyString(value)) {
|
358 | log('using config commit-msg property', value);
|
359 | return value;
|
360 | }
|
361 | if (check.array(value) && value.length === 1) {
|
362 | log('using config commit-msg single value', value);
|
363 | return value[0];
|
364 | }
|
365 | }
|
366 |
|
367 | function pickWizard() {
|
368 | const wizardName = getWizardName();
|
369 | if (!wizardName) {
|
370 | log('no wizard name set');
|
371 | return;
|
372 | }
|
373 | log('using commit message wizard %s', wizardName);
|
374 |
|
375 | const wiz = isBuiltInWizardName(wizardName) ?
|
376 | loadWizard(wizardName) : require(wizardName);
|
377 | la(check.fn(wiz.prompter), 'missing wizard prompter', wizardName, wiz);
|
378 | return wiz;
|
379 | }
|
380 |
|
381 | function customCommitMsgPattern() {
|
382 | return getConfigProperty('msg-pattern');
|
383 |
|
384 | }
|
385 |
|
386 | function customCommitMsgPatternError() {
|
387 | return getConfigProperty('msg-pattern-error');
|
388 | }
|
389 |
|
390 | module.exports = {
|
391 | run: run,
|
392 | getTasks: getTasks,
|
393 | getProjRoot: getProjRoot,
|
394 | printError: printError,
|
395 | wizard: pickWizard,
|
396 | hasUntrackedFiles: hasUntrackedFiles,
|
397 | customMsgPattern: customCommitMsgPattern,
|
398 | customMsgPatternError: customCommitMsgPatternError
|
399 | };
|
400 |
|
401 | if (!module.parent) {
|
402 | run('demo-error', () => true)
|
403 | .then(() => log('finished all tasks'))
|
404 | .done();
|
405 | }
|