Skip to content

Commit 4c76ce7

Browse files
committed
stash before pre-commit hook to avoid false positives; closes #4
- `precommit.stash` is now configurable in `package.json`; set to `false` to avoid stashing
1 parent 886496a commit 4c76ce7

File tree

3 files changed

+139
-25
lines changed

3 files changed

+139
-25
lines changed

index.js

Lines changed: 119 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.
@@ -80,7 +81,7 @@ Hook.prototype.parse = function parse() {
8081
var pre = this.json['pre-commit'] || this.json.precommit
8182
, config = !Array.isArray(pre) && 'object' === typeof pre ? pre : {};
8283

83-
['silent', 'colors', 'template'].forEach(function each(flag) {
84+
['silent', 'colors', 'template', 'stash'].forEach(function each(flag) {
8485
var value;
8586

8687
if (flag in config) value = config[flag];
@@ -204,37 +205,130 @@ Hook.prototype.initialize = function initialize() {
204205
if (!this.config.run) return this.log(Hook.log.run, 0);
205206
};
206207

208+
/**
209+
* Stashes unstaged changes.
210+
*
211+
* @param {Function} done Callback
212+
* @api private
213+
*/
214+
Hook.prototype._stash = function stash(done) {
215+
var hooked = this;
216+
217+
spawn(hooked.git, ['stash', '--keep-index', '--include-untracked'], {
218+
env: process.env,
219+
cwd: hooked.root,
220+
stdio: [0, 1, 2]
221+
}).once('close', function() {
222+
// a nonzero here may be that there are no unstaged changes.
223+
done();
224+
});
225+
};
226+
227+
/**
228+
* Unstashes changes ostensibly stashed by {@link Hook#_stash}.
229+
*
230+
* @param {Function} done Callback
231+
* @api private
232+
*/
233+
Hook.prototype._unstash = function unstash(done) {
234+
var hooked = this;
235+
236+
spawn(hooked.git, ['stash', 'pop'], {
237+
env: process.env,
238+
cwd: hooked.root,
239+
stdio: [0, 1, 2]
240+
}).once('close', function(code) {
241+
if (code) done(code);
242+
done();
243+
});
244+
};
245+
246+
/**
247+
* Runs a hook script.
248+
*
249+
* @param {string} script Script name (as in package.json)
250+
* @param {Function} done Callback
251+
* @api private
252+
*/
253+
Hook.prototype._runScript = function runScript(script, done) {
254+
var hooked = this;
255+
256+
// There's a reason on why we're using an async `spawn` here instead of the
257+
// `shelljs.exec`. The sync `exec` is a hack that writes writes a file to
258+
// disk and they poll with sync fs calls to see for results. The problem is
259+
// that the way they capture the output which us using input redirection and
260+
// this doesn't have the required `isAtty` information that libraries use to
261+
// output colors resulting in script output that doesn't have any color.
262+
//
263+
spawn(hooked.npm, ['run', script, '--silent'], {
264+
env: process.env,
265+
cwd: hooked.root,
266+
stdio: [0, 1, 2]
267+
}).once('close', function closed(code) {
268+
// failures return an object with message referencing script which failed
269+
// plus its exit code. its exit code will be used to exit this program.
270+
if (code) return done({message: script, code: code});
271+
done();
272+
});
273+
};
274+
207275
/**
208276
* Run the specified hooks.
209277
*
210278
* @api public
211279
*/
212280
Hook.prototype.run = function runner() {
213281
var hooked = this;
282+
var scripts = hooked.config.run.slice(0);
283+
284+
if (!scripts.length) return hooked.exit(0);
285+
286+
function error(msg, code) {
287+
return hooked.log(hooked.format(Hook.log.failure, msg, code));
288+
}
289+
290+
function cleanup(errObj) {
291+
var errObjs = [];
292+
// keep error for reporting
293+
if (errObj) errObjs.push(errObj);
294+
295+
// cleanup; unstash changes before exiting.
296+
if (hooked.config.stash === false) {
297+
done(errObjs);
298+
} else {
299+
hooked._unstash(function(code) {
300+
if (code) errObjs.unshift({
301+
message: '"git stash pop" failed',
302+
code: code
303+
});
304+
305+
done(errObjs);
306+
});
307+
}
308+
}
309+
310+
function done(errObjs) {
311+
// exit with the code of the failed script, or if all scripts exited with
312+
// codes of 0 and "git stash pop" failed, then use its exit code.
313+
if (errObjs.length) return error(errObjs.map(function(err) {
314+
return err.message;
315+
}).join('\n'), errObjs[errObjs.length - 1].code);
214316

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);
236-
});
237-
})(hooked.config.run.slice(0));
317+
hooked.exit(0);
318+
}
319+
320+
function runScripts() {
321+
// run each script in series. upon completion or nonzero exit code,
322+
// the callback is executed
323+
async.eachSeries(scripts, hooked._runScript.bind(hooked), cleanup);
324+
}
325+
326+
if (this.config.stash === false) {
327+
runScripts();
328+
} else {
329+
// attempt to stash changes not on stage
330+
hooked._stash(runScripts);
331+
}
238332
};
239333

240334
/**

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)