Skip to content

Commit d396815

Browse files
committed
stash files before running pre-commit hook to avoid false positives; closes #4
1 parent 886496a commit d396815

File tree

3 files changed

+122
-25
lines changed

3 files changed

+122
-25
lines changed

index.js

Lines changed: 102 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ var spawn = require('cross-spawn')
44
, which = require('which')
55
, path = require('path')
66
, util = require('util')
7-
, tty = require('tty');
7+
, tty = require('tty')
8+
, async = require('async');
89

910
/**
1011
* Representation of a hook runner.
@@ -18,8 +19,10 @@ function Hook(fn, options) {
1819
if (!this) return new Hook(fn, options);
1920
options = options || {};
2021

21-
this.options = options; // Used for testing only. Ignore this. Don't touch.
22-
this.config = {}; // pre-commit configuration from the `package.json`.
22+
this.options = options; // Used for testing only. Ignore this. Don't
23+
// touch.
24+
this.config = {}; // pre-commit configuration from the
25+
// `package.json`.
2326
this.json = {}; // Actual content of the `package.json`.
2427
this.npm = ''; // The location of the `npm` binary.
2528
this.git = ''; // The location of the `git` binary.
@@ -204,37 +207,111 @@ Hook.prototype.initialize = function initialize() {
204207
if (!this.config.run) return this.log(Hook.log.run, 0);
205208
};
206209

210+
/**
211+
* Stashes unstaged changes.
212+
*
213+
* @param {Function} done Callback
214+
* @api private
215+
*/
216+
Hook.prototype._stash = function stash(done) {
217+
var hooked = this;
218+
219+
spawn(hooked.git, ['stash', '--keep-index', '--include-untracked'], {
220+
env: process.env,
221+
cwd: hooked.root,
222+
stdio: [0, 1, 2]
223+
}).once('close', function() {
224+
// a nonzero here may be that there are no unstaged changes.
225+
done();
226+
});
227+
};
228+
229+
/**
230+
* Unstashes changes ostensibly stashed by {@link Hook#_stash}.
231+
*
232+
* @param {Function} done Callback
233+
* @api private
234+
*/
235+
Hook.prototype._unstash = function unstash(done) {
236+
var hooked = this;
237+
238+
spawn(hooked.git, ['stash', 'pop'], {
239+
env: process.env,
240+
cwd: hooked.root,
241+
stdio: [0, 1, 2]
242+
}).once('close', function(code) {
243+
if (code) done(code);
244+
done();
245+
});
246+
};
247+
248+
/**
249+
* Runs a hook script.
250+
*
251+
* @param {string} script Script name (as in package.json)
252+
* @param {Function} done Callback
253+
* @api private
254+
*/
255+
Hook.prototype._runScript = function runScript(script, done) {
256+
var hooked = this;
257+
258+
// There's a reason on why we're using an async `spawn` here instead of the
259+
// `shelljs.exec`. The sync `exec` is a hack that writes writes a file to
260+
// disk and they poll with sync fs calls to see for results. The problem is
261+
// that the way they capture the output which us using input redirection and
262+
// this doesn't have the required `isAtty` information that libraries use to
263+
// output colors resulting in script output that doesn't have any color.
264+
//
265+
spawn(hooked.npm, ['run', script, '--silent'], {
266+
env: process.env,
267+
cwd: hooked.root,
268+
stdio: [0, 1, 2]
269+
}).once('close', function closed(code) {
270+
// failures return an object with message referencing script which failed
271+
// plus its exit code. its exit code will be used to exit this program.
272+
if (code) return done({message: script, code: code});
273+
done();
274+
});
275+
};
276+
207277
/**
208278
* Run the specified hooks.
209279
*
210280
* @api public
211281
*/
212282
Hook.prototype.run = function runner() {
213283
var hooked = this;
284+
var scripts = hooked.config.run.slice(0);
214285

215-
(function again(scripts) {
216-
if (!scripts.length) return hooked.exit(0);
217-
218-
var script = scripts.shift();
219-
220-
//
221-
// There's a reason on why we're using an async `spawn` here instead of the
222-
// `shelljs.exec`. The sync `exec` is a hack that writes writes a file to
223-
// disk and they poll with sync fs calls to see for results. The problem is
224-
// that the way they capture the output which us using input redirection and
225-
// this doesn't have the required `isAtty` information that libraries use to
226-
// output colors resulting in script output that doesn't have any color.
227-
//
228-
spawn(hooked.npm, ['run', script, '--silent'], {
229-
env: process.env,
230-
cwd: hooked.root,
231-
stdio: [0, 1, 2]
232-
}).once('close', function closed(code) {
233-
if (code) return hooked.log(hooked.format(Hook.log.failure, script, code));
234-
235-
again(scripts);
286+
if (!scripts.length) return hooked.exit(0);
287+
288+
function error(msg, code) {
289+
return hooked.log(hooked.format(Hook.log.failure, msg, code));
290+
}
291+
292+
// first, attempt to stash changes not on stage
293+
hooked._stash(function() {
294+
// run each script in series. upon completion or nonzero exit code,
295+
// the callback is executed
296+
async.eachSeries(scripts, hooked._runScript.bind(hooked), function(errObj) {
297+
var errObjs = [];
298+
// keep error for reporting
299+
if (errObj) errObjs.push(errObj);
300+
301+
// cleanup; unstash changes before exiting.
302+
hooked._unstash(function(code) {
303+
if (code) errObjs.unshift({message: '"git stash pop" failed', code: code});
304+
305+
// exit with the code of the failed script, or if all scripts exited with
306+
// codes of 0 and "git stash pop" failed, then use its exit code.
307+
if (errObjs.length) return error(errObjs.map(function(err) {
308+
return err.message;
309+
}).join('\n'), errObjs[errObjs.length - 1].code);
310+
311+
hooked.exit(0);
312+
});
236313
});
237-
})(hooked.config.run.slice(0));
314+
});
238315
};
239316

240317
/**

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"coverage": "istanbul cover ./node_modules/.bin/_mocha -- test.js",
88
"example-fail": "echo \"This is the example hook, I exit with 1\" && exit 1",
99
"example-pass": "echo \"This is the example hook, I exit with 0\" && exit 0",
10+
"example-stash": "echo \"This is the stash hook, I exit with 1 if .stash exists\" && test ! -e .stash && exit 0",
1011
"install": "node install.js",
1112
"test": "mocha test.js",
1213
"test-travis": "istanbul cover node_modules/.bin/_mocha --report lcovonly -- test.js",
@@ -30,6 +31,7 @@
3031
"homepage": "https://github.com/observing/pre-commit",
3132
"license": "MIT",
3233
"dependencies": {
34+
"async": "1.4.x",
3335
"cross-spawn": "2.0.x",
3436
"which": "1.1.x"
3537
},

test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,5 +253,23 @@ describe('pre-commit', function () {
253253
hook.config.run = ['example-fail'];
254254
hook.run();
255255
});
256+
257+
it('should stash successfully', function(next) {
258+
// if file ".stash" exists, the test will fail.
259+
var fs = require('fs');
260+
fs.writeFileSync('.stash', '', 'utf8');
261+
262+
var hook = new Hook(function (code, lines) {
263+
fs.unlinkSync('.stash');
264+
265+
assume(code).equals(0);
266+
assume(lines).is.undefined();
267+
268+
next();
269+
}, { ignorestatus: true });
270+
271+
hook.config.run = ['example-stash'];
272+
hook.run();
273+
});
256274
});
257275
});

0 commit comments

Comments
 (0)