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