UNPKG

10.1 kBJavaScriptView Raw
1'use strict';
2
3const la = require('lazy-ass');
4const check = require('check-more-types');
5const ggit = require('ggit');
6
7var child = require('child_process');
8var path = require('path');
9var fs = require('fs');
10
11const log = require('debug')('pre-git');
12/* jshint -W079 */
13var Promise = require('bluebird');
14
15var label = 'pre-commit:';
16
17var gitPrefix = process.env.GIT_PREFIX || '';
18
19function isAtRoot(dir) {
20 return dir === '/';
21}
22
23function isPackageAmongFiles(dir) {
24 var files = fs.readdirSync(dir);
25 return files.indexOf('package.json') >= 0;
26}
27
28function 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
39function 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 // go to the parent folder and look there
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
60function 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// returns a promise
68// Can we use ggit for this?
69function 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 * You've failed on some of the scripts, output how much you've sucked today.
102 *
103 * @param {Error} err The actual error.
104 * @api private
105 */
106function 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
140function getConfig() {
141 const packageName = 'pre-git';
142 const pkg = getPackage();
143 return pkg.config && pkg.config[packageName];
144}
145
146function 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
160function hasEnabledOption(config) {
161 return 'enabled' in config;
162}
163
164function 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
188function 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
202function 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
225function checkInputs(label) {
226 if (typeof label !== 'string' || !label) {
227 throw new Error('Expected string label (pre-commit, pre-push)');
228 }
229}
230
231function skipPrecommit() {
232 return process.argv[2] !== 'origin';
233}
234
235function 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// returns a promise
247function 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
297function run(hookLabel) {
298 log('running', hookLabel);
299 checkInputs(hookLabel);
300
301 label = hookLabel;
302
303 // TODO should the failure action be outside?
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
310function errorMessage(err) {
311 return err instanceof Error ? err.message : err;
312}
313
314function printError(x) {
315 console.error(errorMessage(x) || 'Unknown error');
316}
317
318function 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
328function 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
342function 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
367function 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
381function customCommitMsgPattern() {
382 return getConfigProperty('msg-pattern');
383
384}
385
386function customCommitMsgPatternError() {
387 return getConfigProperty('msg-pattern-error');
388}
389
390module.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
401if (!module.parent) {
402 run('demo-error', () => true)
403 .then(() => log('finished all tasks'))
404 .done();
405}