@@ -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 */
212282Hook . 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/**
0 commit comments