From 4c76ce79876e8452b97ffae3ac629ff73ad0f820 Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Sat, 12 Sep 2015 13:50:56 -0700 Subject: [PATCH] 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 --- index.js | 144 ++++++++++++++++++++++++++++++++++++++++++--------- package.json | 2 + test.js | 18 +++++++ 3 files changed, 139 insertions(+), 25 deletions(-) diff --git a/index.js b/index.js index a20646d..218659d 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,8 @@ var spawn = require('cross-spawn') , which = require('which') , path = require('path') , util = require('util') - , tty = require('tty'); + , tty = require('tty') + , async = require('async'); /** * Representation of a hook runner. @@ -80,7 +81,7 @@ Hook.prototype.parse = function parse() { var pre = this.json['pre-commit'] || this.json.precommit , config = !Array.isArray(pre) && 'object' === typeof pre ? pre : {}; - ['silent', 'colors', 'template'].forEach(function each(flag) { + ['silent', 'colors', 'template', 'stash'].forEach(function each(flag) { var value; if (flag in config) value = config[flag]; @@ -204,6 +205,73 @@ Hook.prototype.initialize = function initialize() { if (!this.config.run) return this.log(Hook.log.run, 0); }; +/** + * Stashes unstaged changes. + * + * @param {Function} done Callback + * @api private + */ +Hook.prototype._stash = function stash(done) { + var hooked = this; + + spawn(hooked.git, ['stash', '--keep-index', '--include-untracked'], { + env: process.env, + cwd: hooked.root, + stdio: [0, 1, 2] + }).once('close', function() { + // a nonzero here may be that there are no unstaged changes. + done(); + }); +}; + +/** + * Unstashes changes ostensibly stashed by {@link Hook#_stash}. + * + * @param {Function} done Callback + * @api private + */ +Hook.prototype._unstash = function unstash(done) { + var hooked = this; + + spawn(hooked.git, ['stash', 'pop'], { + env: process.env, + cwd: hooked.root, + stdio: [0, 1, 2] + }).once('close', function(code) { + if (code) done(code); + done(); + }); +}; + +/** + * Runs a hook script. + * + * @param {string} script Script name (as in package.json) + * @param {Function} done Callback + * @api private + */ +Hook.prototype._runScript = function runScript(script, done) { + var hooked = this; + + // There's a reason on why we're using an async `spawn` here instead of the + // `shelljs.exec`. The sync `exec` is a hack that writes writes a file to + // disk and they poll with sync fs calls to see for results. The problem is + // that the way they capture the output which us using input redirection and + // this doesn't have the required `isAtty` information that libraries use to + // output colors resulting in script output that doesn't have any color. + // + spawn(hooked.npm, ['run', script, '--silent'], { + env: process.env, + cwd: hooked.root, + stdio: [0, 1, 2] + }).once('close', function closed(code) { + // failures return an object with message referencing script which failed + // plus its exit code. its exit code will be used to exit this program. + if (code) return done({message: script, code: code}); + done(); + }); +}; + /** * Run the specified hooks. * @@ -211,30 +279,56 @@ Hook.prototype.initialize = function initialize() { */ Hook.prototype.run = function runner() { var hooked = this; + var scripts = hooked.config.run.slice(0); + + if (!scripts.length) return hooked.exit(0); + + function error(msg, code) { + return hooked.log(hooked.format(Hook.log.failure, msg, code)); + } + + function cleanup(errObj) { + var errObjs = []; + // keep error for reporting + if (errObj) errObjs.push(errObj); + + // cleanup; unstash changes before exiting. + if (hooked.config.stash === false) { + done(errObjs); + } else { + hooked._unstash(function(code) { + if (code) errObjs.unshift({ + message: '"git stash pop" failed', + code: code + }); + + done(errObjs); + }); + } + } + + function done(errObjs) { + // exit with the code of the failed script, or if all scripts exited with + // codes of 0 and "git stash pop" failed, then use its exit code. + if (errObjs.length) return error(errObjs.map(function(err) { + return err.message; + }).join('\n'), errObjs[errObjs.length - 1].code); - (function again(scripts) { - if (!scripts.length) return hooked.exit(0); - - var script = scripts.shift(); - - // - // There's a reason on why we're using an async `spawn` here instead of the - // `shelljs.exec`. The sync `exec` is a hack that writes writes a file to - // disk and they poll with sync fs calls to see for results. The problem is - // that the way they capture the output which us using input redirection and - // this doesn't have the required `isAtty` information that libraries use to - // output colors resulting in script output that doesn't have any color. - // - spawn(hooked.npm, ['run', script, '--silent'], { - env: process.env, - cwd: hooked.root, - stdio: [0, 1, 2] - }).once('close', function closed(code) { - if (code) return hooked.log(hooked.format(Hook.log.failure, script, code)); - - again(scripts); - }); - })(hooked.config.run.slice(0)); + hooked.exit(0); + } + + function runScripts() { + // run each script in series. upon completion or nonzero exit code, + // the callback is executed + async.eachSeries(scripts, hooked._runScript.bind(hooked), cleanup); + } + + if (this.config.stash === false) { + runScripts(); + } else { + // attempt to stash changes not on stage + hooked._stash(runScripts); + } }; /** diff --git a/package.json b/package.json index 4f2fabe..fecbcf9 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "coverage": "istanbul cover ./node_modules/.bin/_mocha -- test.js", "example-fail": "echo \"This is the example hook, I exit with 1\" && exit 1", "example-pass": "echo \"This is the example hook, I exit with 0\" && exit 0", + "example-stash": "echo \"This is the stash hook, I exit with 1 if .stash exists\" && test ! -e .stash && exit 0", "install": "node install.js", "test": "mocha test.js", "test-travis": "istanbul cover node_modules/.bin/_mocha --report lcovonly -- test.js", @@ -30,6 +31,7 @@ "homepage": "https://github.com/observing/pre-commit", "license": "MIT", "dependencies": { + "async": "1.4.x", "cross-spawn": "2.0.x", "which": "1.1.x" }, diff --git a/test.js b/test.js index 829e17f..2c59d19 100644 --- a/test.js +++ b/test.js @@ -253,5 +253,23 @@ describe('pre-commit', function () { hook.config.run = ['example-fail']; hook.run(); }); + + it('should stash successfully', function(next) { + // if file ".stash" exists, the test will fail. + var fs = require('fs'); + fs.writeFileSync('.stash', '', 'utf8'); + + var hook = new Hook(function (code, lines) { + fs.unlinkSync('.stash'); + + assume(code).equals(0); + assume(lines).is.undefined(); + + next(); + }, { ignorestatus: true }); + + hook.config.run = ['example-stash']; + hook.run(); + }); }); });