@@ -4,7 +4,8 @@ var spawn = require('cross-spawn')
4
4
, which = require ( 'which' )
5
5
, path = require ( 'path' )
6
6
, util = require ( 'util' )
7
- , tty = require ( 'tty' ) ;
7
+ , tty = require ( 'tty' )
8
+ , async = require ( 'async' ) ;
8
9
9
10
/**
10
11
* Representation of a hook runner.
@@ -18,8 +19,10 @@ function Hook(fn, options) {
18
19
if ( ! this ) return new Hook ( fn , options ) ;
19
20
options = options || { } ;
20
21
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`.
23
26
this . json = { } ; // Actual content of the `package.json`.
24
27
this . npm = '' ; // The location of the `npm` binary.
25
28
this . git = '' ; // The location of the `git` binary.
@@ -204,37 +207,111 @@ Hook.prototype.initialize = function initialize() {
204
207
if ( ! this . config . run ) return this . log ( Hook . log . run , 0 ) ;
205
208
} ;
206
209
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
+
207
277
/**
208
278
* Run the specified hooks.
209
279
*
210
280
* @api public
211
281
*/
212
282
Hook . prototype . run = function runner ( ) {
213
283
var hooked = this ;
284
+ var scripts = hooked . config . run . slice ( 0 ) ;
214
285
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
+ } ) ;
236
313
} ) ;
237
- } ) ( hooked . config . run . slice ( 0 ) ) ;
314
+ } ) ;
238
315
} ;
239
316
240
317
/**
0 commit comments