diff --git a/lib/commands/bootstrap.js b/lib/commands/bootstrap.js index 7b4ae67..6082f04 100644 --- a/lib/commands/bootstrap.js +++ b/lib/commands/bootstrap.js @@ -13,10 +13,9 @@ const exec = require( '../utils/exec' ); module.exports = { /** * @param {Object} data - * @param {Object} data.parameters Additional arguments provided by the user. * @param {String} data.packageName Name of current package to process. * @param {Options} data.options The options object. - * @param {Repository|null} data.repository + * @param {Repository} data.repository * @returns {Promise} */ execute( data ) { @@ -25,19 +24,24 @@ module.exports = { return new Promise( ( resolve, reject ) => { const destinationPath = path.join( data.options.packages, data.repository.directory ); + let promise; + // Package is already cloned. if ( fs.existsSync( destinationPath ) ) { - log.info( `Package "${ data.packageName }" is already cloned. Skipping.` ); + log.info( `Package "${ data.packageName }" is already cloned.` ); - return resolve( { logs: log.all() } ); - } + promise = Promise.resolve(); + } else { + const command = [ + `git clone --progress ${ data.repository.url } ${ destinationPath }`, + `cd ${ destinationPath }`, + `git checkout --quiet ${ data.repository.branch }` + ].join( ' && ' ); - const command = - `git clone --progress ${ data.repository.url } ${ destinationPath } && ` + - `cd ${ destinationPath } && ` + - `git checkout --quiet ${ data.repository.branch }`; + promise = exec( command ); + } - exec( command ) + promise .then( ( output ) => { log.info( output ); @@ -47,14 +51,14 @@ module.exports = { if ( data.options.recursive ) { const packageJson = require( path.join( destinationPath, 'package.json' ) ); - let packages = []; + const packages = []; if ( packageJson.dependencies ) { - packages = packages.concat( Object.keys( packageJson.dependencies ) ); + packages.push( ...Object.keys( packageJson.dependencies ) ); } if ( packageJson.devDependencies ) { - packages = packages.concat( Object.keys( packageJson.devDependencies ) ); + packages.push( ...Object.keys( packageJson.devDependencies ) ); } commandOutput.packages = packages; diff --git a/lib/commands/exec.js b/lib/commands/exec.js index c464872..07dd630 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -11,20 +11,20 @@ const exec = require( '../utils/exec' ); module.exports = { /** - * @param {Array.<String>} parameters Arguments that user provided calling the mgit. + * @param {Array.<String>} args Arguments that user provided calling the mgit. */ - beforeExecute( parameters ) { - if ( parameters.length === 1 ) { + beforeExecute( args ) { + if ( args.length === 1 ) { throw new Error( 'Missing command to execute. Use: mgit exec [command-to-execute].' ); } }, /** * @param {Object} data - * @param {Object} data.parameters Additional arguments provided by the user. + * @param {Object} data.arguments Arguments that user provided calling the mgit. * @param {String} data.packageName Name of current package to process. * @param {Options} data.options The options object. - * @param {Repository|null} data.repository + * @param {Repository} data.repository * @returns {Promise} */ execute( data ) { @@ -42,7 +42,7 @@ module.exports = { process.chdir( newCwd ); - exec( data.parameters[ 0 ] ) + exec( data.arguments[ 0 ] ) .then( ( stdout ) => { process.chdir( data.options.cwd ); diff --git a/lib/commands/savehashes.js b/lib/commands/savehashes.js index 0d1beed..8b24df0 100644 --- a/lib/commands/savehashes.js +++ b/lib/commands/savehashes.js @@ -28,7 +28,7 @@ module.exports = { commit: commitHash }; - log.info( `Commit: ${ commitHash }.` ); + log.info( `Commit: "${ commitHash }".` ); resolve( { response: commandResponse, @@ -44,7 +44,7 @@ module.exports = { function getExecData( command ) { return Object.assign( {}, data, { - parameters: [ command ] + arguments: [ command ] } ); } }, @@ -56,7 +56,7 @@ module.exports = { * @param {Set} commandResponses Results of executed command for each package. */ afterExecute( processedPackages, commandResponses ) { - const cwd = require( '../utils/getcwd.js' )(); + const cwd = require( '../utils/getcwd' )(); const mgitJsonPath = path.join( cwd, 'mgit.json' ); updateJsonFile( mgitJsonPath, ( json ) => { diff --git a/lib/commands/update.js b/lib/commands/update.js index 95aa314..bc88da9 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -12,10 +12,9 @@ const chalk = require( 'chalk' ); module.exports = { /** * @param {Object} data - * @param {Object} data.parameters Additional arguments provided by the user. * @param {String} data.packageName Name of current package to process. * @param {Options} data.options The options object. - * @param {Repository|null} data.repository + * @param {Repository} data.repository * @returns {Promise} */ execute( data ) { @@ -33,7 +32,6 @@ module.exports = { const bootstrapOptions = { options: data.options, packageName: data.packageName, - mgit: data.mgit, repository: data.repository }; @@ -62,7 +60,10 @@ module.exports = { log.concat( response.logs ); } ) .then( () => { - return execCommand.execute( getExecData( `git checkout ${ data.repository.branch }` ) ); + return execCommand.execute( getExecData( `git checkout ${ data.repository.branch }` ) ) + .catch( ( response ) => { + throw new Error( response.logs.error[ 0 ].replace( /^error\: /, '' ) ); + } ); } ) .then( ( response ) => { log.concat( response.logs ); @@ -72,7 +73,7 @@ module.exports = { } ) .then( ( response ) => { const stdout = response.logs.info.join( '\n' ).trim(); - const isOnBranchRegexp = /HEAD detached at [\w\d]+/; + const isOnBranchRegexp = /HEAD detached at+/; // If on a detached commit, mgit must not pull the changes. if ( isOnBranchRegexp.test( stdout ) ) { @@ -81,7 +82,10 @@ module.exports = { return resolve( { logs: log.all() } ); } - return execCommand.execute( getExecData( `git pull origin ${ data.repository.branch }` ) ); + return execCommand.execute( getExecData( `git pull origin ${ data.repository.branch }` ) ) + .catch( ( response ) => { + throw new Error( response.logs.error[ 0 ] ); + } ); } ) .then( ( response ) => { log.concat( response.logs ); @@ -97,7 +101,7 @@ module.exports = { function getExecData( command ) { return Object.assign( {}, data, { - parameters: [ command ] + arguments: [ command ] } ); } }, diff --git a/lib/index.js b/lib/index.js index 1ae622c..4b6e831 100644 --- a/lib/index.js +++ b/lib/index.js @@ -11,10 +11,10 @@ const logDisplay = require( './utils/displaylog' ); const getOptions = require( './utils/getoptions' ); /** - * @param {Array.<String>} parameters Arguments that the user provided. + * @param {Array.<String>} args Arguments that the user provided. * @param {Options} options The options object. It will be extended with the default options. */ -module.exports = function( parameters, options ) { +module.exports = function( args, options ) { const startTime = process.hrtime(); const forkPool = createForkPool( path.join( __dirname, 'utils', 'child-process.js' ) ); @@ -23,13 +23,13 @@ module.exports = function( parameters, options ) { const resolver = require( options.resolverPath ); // Remove all dashes from command name. - parameters[ 0 ] = parameters[ 0 ].replace( /-/g, '' ); + args[ 0 ] = args[ 0 ].replace( /-/g, '' ); - const commandPath = path.join( __dirname, 'commands', parameters[ 0 ] ); + const commandPath = path.join( __dirname, 'commands', args[ 0 ] ); const command = require( commandPath ); if ( typeof command.beforeExecute == 'function' ) { - command.beforeExecute( parameters ); + command.beforeExecute( args ); } const processedPackages = new Set(); @@ -52,7 +52,7 @@ module.exports = function( parameters, options ) { const data = { command: commandPath, - parameters: parameters.slice( 1 ), + arguments: args.slice( 1 ), options, packageName: packageName, repository: resolver( packageName, options ) diff --git a/lib/utils/log.js b/lib/utils/log.js index 5d427be..3b5d057 100644 --- a/lib/utils/log.js +++ b/lib/utils/log.js @@ -25,6 +25,10 @@ module.exports = function log() { }, log( type, msg ) { + if ( !msg ) { + return; + } + msg = msg.trim(); if ( !msg ) { diff --git a/package.json b/package.json index 9fc6f23..dc13ccf 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "guppy-pre-commit": "^0.4.0", "istanbul": "^0.4.5", "mocha": "^3.2.0", + "mockery": "^2.0.0", "sinon": "^1.17.7" }, "repository": { diff --git a/tests/commands/bootstrap.js b/tests/commands/bootstrap.js new file mode 100644 index 0000000..5fc6f06 --- /dev/null +++ b/tests/commands/bootstrap.js @@ -0,0 +1,156 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* jshint mocha:true */ + +'use strict'; + +const fs = require( 'fs' ); +const path = require( 'path' ); +const sinon = require( 'sinon' ); +const mockery = require( 'mockery' ); +const expect = require( 'chai' ).expect; + +describe( 'commands/bootstrap', () => { + let bootstrapCommand, sandbox, stubs, data; + + beforeEach( () => { + sandbox = sinon.sandbox.create(); + + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + exec: sandbox.stub(), + fs: { + existsSync: sandbox.stub( fs, 'existsSync' ) + }, + path: { + join: sandbox.stub( path, 'join', ( ...chunks ) => chunks.join( '/' ) ) + } + }; + + data = { + packageName: 'test-package', + options: { + cwd: __dirname, + packages: 'packages' + }, + repository: { + directory: 'test-package', + url: 'git@github.com/organization/test-package.git', + branch: 'master' + } + }; + + mockery.registerMock( '../utils/exec', stubs.exec ); + + bootstrapCommand = require( '../../lib/commands/bootstrap' ); + } ); + + afterEach( () => { + sandbox.restore(); + mockery.disable(); + } ); + + describe( 'execute()', () => { + it( 'rejects promise if something went wrong', () => { + const error = new Error( 'Unexpected error.' ); + + stubs.fs.existsSync.returns( false ); + stubs.exec.returns( Promise.reject( error ) ); + + return bootstrapCommand.execute( data ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + ( response ) => { + expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( `Error: ${ error.message }` ); + } + ); + } ); + + it( 'clones a repository if is not available', () => { + stubs.fs.existsSync.returns( false ); + stubs.exec.returns( Promise.resolve( 'Git clone log.' ) ); + + return bootstrapCommand.execute( data ) + .then( ( response ) => { + expect( stubs.exec.calledOnce ).to.equal( true ); + + const cloneCommand = stubs.exec.firstCall.args[ 0 ].split( ' && ' ); + + // Clone the repository. + expect( cloneCommand[ 0 ] ).to.equal( 'git clone --progress git@github.com/organization/test-package.git packages/test-package' ); + // Change the directory to cloned package. + expect( cloneCommand[ 1 ] ).to.equal( 'cd packages/test-package' ); + // And check out to proper branch. + expect( cloneCommand[ 2 ] ).to.equal( 'git checkout --quiet master' ); + + expect( response.logs.info[ 0 ] ).to.equal( 'Git clone log.' ); + } ); + } ); + + it( 'does not clone a repository if is available', () => { + stubs.fs.existsSync.returns( true ); + + return bootstrapCommand.execute( data ) + .then( ( response ) => { + expect( stubs.exec.called ).to.equal( false ); + + expect( response.logs.info[ 0 ] ).to.equal( 'Package "test-package" is already cloned.' ); + } ); + } ); + + it( 'installs dependencies of cloned package', () => { + data.options.recursive = true; + data.options.packages = __dirname + '/../fixtures'; + data.repository.directory = 'project-a'; + + stubs.fs.existsSync.returns( true ); + + return bootstrapCommand.execute( data ) + .then( ( response ) => { + expect( response.packages ).is.an( 'array' ); + expect( response.packages ).to.deep.equal( [ 'test-foo' ] ); + } ); + } ); + + it( 'installs devDependencies of cloned package', () => { + data.options.recursive = true; + data.options.packages = __dirname + '/../fixtures'; + data.repository.directory = 'project-with-options-in-mgitjson'; + + stubs.fs.existsSync.returns( true ); + + return bootstrapCommand.execute( data ) + .then( ( response ) => { + expect( response.packages ).is.an( 'array' ); + expect( response.packages ).to.deep.equal( [ 'test-bar' ] ); + } ); + } ); + } ); + + describe( 'afterExecute()', () => { + it( 'informs about number of processed packages', () => { + const consoleLog = sandbox.stub( console, 'log' ); + + const processedPackages = new Set(); + processedPackages.add( 'package-1' ); + processedPackages.add( 'package-2' ); + + bootstrapCommand.afterExecute( processedPackages ); + + expect( consoleLog.calledOnce ).to.equal( true ); + expect( consoleLog.firstCall.args[ 0 ] ).to.match( /2 packages have been processed\./ ); + + consoleLog.restore(); + } ); + } ); +} ); diff --git a/tests/commands/exec.js b/tests/commands/exec.js new file mode 100644 index 0000000..001bc70 --- /dev/null +++ b/tests/commands/exec.js @@ -0,0 +1,131 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* jshint mocha:true */ + +'use strict'; + +const fs = require( 'fs' ); +const path = require( 'path' ); +const sinon = require( 'sinon' ); +const mockery = require( 'mockery' ); +const expect = require( 'chai' ).expect; + +describe( 'commands/exec', () => { + let execCommand, sandbox, stubs, data; + + beforeEach( () => { + sandbox = sinon.sandbox.create(); + + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + exec: sandbox.stub(), + fs: { + existsSync: sandbox.stub( fs, 'existsSync' ) + }, + path: { + join: sandbox.stub( path, 'join', ( ...chunks ) => chunks.join( '/' ) ) + }, + process: { + chdir: sandbox.stub( process, 'chdir' ) + } + }; + + data = { + // `execute` is called without the "exec" command (`mgit exec first-cmd other-cmd` => [ 'first-cmd', 'other-cmd' ]). + arguments: [ 'pwd' ], + packageName: 'test-package', + options: { + cwd: __dirname, + packages: 'packages' + }, + repository: { + directory: 'test-package' + } + }; + + mockery.registerMock( '../utils/exec', stubs.exec ); + + execCommand = require( '../../lib/commands/exec' ); + } ); + + afterEach( () => { + sandbox.restore(); + mockery.disable(); + } ); + + describe( 'beforeExecute()', () => { + it( 'throws an error if command to execute is not specified', () => { + expect( () => { + // `beforeExecute` is called with full user's input (mgit exec [command-to-execute]). + execCommand.beforeExecute( [ 'exec' ] ); + } ).to.throw( Error, 'Missing command to execute. Use: mgit exec [command-to-execute].' ); + } ); + + it( 'does nothing if command is specified', () => { + expect( () => { + execCommand.beforeExecute( [ 'exec', 'pwd' ] ); + } ).to.not.throw( Error ); + } ); + } ); + + describe( 'execute()', () => { + it( 'does not execute the command if package is not available', () => { + stubs.fs.existsSync.returns( false ); + + return execCommand.execute( data ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + ( response ) => { + expect( stubs.path.join.calledOnce ).to.equal( true ); + + const err = 'Package "test-package" is not available. Run "mgit bootstrap" in order to download the package.'; + expect( response.logs.error[ 0 ] ).to.equal( err ); + } + ); + } ); + + it( 'rejects promise if something went wrong', () => { + const error = new Error( 'Unexpected error.' ); + + stubs.fs.existsSync.returns( true ); + stubs.exec.returns( Promise.reject( error ) ); + + return execCommand.execute( data ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + ( response ) => { + expect( stubs.process.chdir.calledTwice ).to.equal( true ); + expect( stubs.process.chdir.firstCall.args[ 0 ] ).to.equal( 'packages/test-package' ); + expect( stubs.process.chdir.secondCall.args[ 0 ] ).to.equal( __dirname ); + expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( `Error: ${ error.message }` ); + } + ); + } ); + + it( 'resolves promise if command has been executed', () => { + const pwd = '/packages/test-package'; + stubs.fs.existsSync.returns( true ); + stubs.exec.returns( Promise.resolve( pwd ) ); + + return execCommand.execute( data ) + .then( ( response ) => { + expect( stubs.process.chdir.calledTwice ).to.equal( true ); + expect( stubs.process.chdir.firstCall.args[ 0 ] ).to.equal( 'packages/test-package' ); + expect( stubs.process.chdir.secondCall.args[ 0 ] ).to.equal( __dirname ); + expect( response.logs.info[ 0 ] ).to.equal( pwd ); + } ); + } ); + } ); +} ); diff --git a/tests/commands/savehashes.js b/tests/commands/savehashes.js new file mode 100644 index 0000000..d68f417 --- /dev/null +++ b/tests/commands/savehashes.js @@ -0,0 +1,140 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* jshint mocha:true */ + +'use strict'; + +const path = require( 'path' ); +const sinon = require( 'sinon' ); +const mockery = require( 'mockery' ); +const expect = require( 'chai' ).expect; + +describe( 'commands/savehashes', () => { + let saveHashesCommand, sandbox, stubs, data, mgitJsonPath, updateFunction; + + beforeEach( () => { + sandbox = sinon.sandbox.create(); + + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + execCommand: { + execute: sandbox.stub() + }, + path: { + join: sandbox.stub( path, 'join', ( ...chunks ) => chunks.join( '/' ) ) + } + }; + + data = { + packageName: 'test-package', + }; + + mockery.registerMock( './exec', stubs.execCommand ); + mockery.registerMock( '../utils/updatejsonfile', ( pathToFile, callback ) => { + mgitJsonPath = pathToFile; + updateFunction = callback; + } ); + mockery.registerMock( '../utils/getcwd', () => { + return __dirname; + } ); + + saveHashesCommand = require( '../../lib/commands/savehashes' ); + } ); + + afterEach( () => { + sandbox.restore(); + mockery.disable(); + } ); + + describe( 'execute()', () => { + it( 'rejects promise if called command returned an error', () => { + const error = new Error( 'Unexpected error.' ); + + stubs.execCommand.execute.returns( Promise.reject( error ) ); + + return saveHashesCommand.execute( data ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + ( response ) => { + expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( `Error: ${ error.message }` ); + } + ); + } ); + + it( 'resolves promise with last commit id', () => { + const execCommandResponse = { + logs: { + info: [ '584f34152d782cc2f26453e10b93c4a16ef01925' ] + } + }; + + stubs.execCommand.execute.returns( Promise.resolve( execCommandResponse ) ); + + return saveHashesCommand.execute( data ) + .then( ( commandResponse ) => { + expect( stubs.execCommand.execute.calledOnce ).to.equal( true ); + expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { + packageName: data.packageName, + arguments: [ 'git rev-parse HEAD' ] + } ); + + expect( commandResponse.response ).to.deep.equal( { + packageName: data.packageName, + commit: '584f34152d782cc2f26453e10b93c4a16ef01925' + } ); + + expect( commandResponse.logs.info[ 0 ] ).to.equal( 'Commit: "584f34152d782cc2f26453e10b93c4a16ef01925".' ); + } ); + } ); + } ); + + describe( 'afterExecute()', () => { + it( 'updates collected hashes in "mgit.json"', () => { + const processedPackages = new Set(); + const commandResponses = new Set(); + + processedPackages.add( 'test-package' ); + processedPackages.add( 'package-test' ); + + commandResponses.add( { + packageName: 'test-package', + commit: '584f34152d782cc2f26453e10b93c4a16ef01925' + } ); + commandResponses.add( { + packageName: 'package-test', + commit: '52910fe61a4c39b01e35462f2cc287d25143f485' + } ); + + saveHashesCommand.afterExecute( processedPackages, commandResponses ); + + let json = { + dependencies: { + 'test-package': 'organization/test-package', + 'package-test': 'organization/package-test', + 'other-package': 'organization/other-package' + } + }; + + expect( mgitJsonPath ).to.equal( __dirname + '/mgit.json' ); + expect( updateFunction ).to.be.a( 'function' ); + + json = updateFunction( json ); + + expect( json.dependencies ).to.deep.equal( { + 'test-package': 'organization/test-package#584f34152d782cc2f26453e10b93c4a16ef01925', + 'package-test': 'organization/package-test#52910fe61a4c39b01e35462f2cc287d25143f485', + 'other-package': 'organization/other-package' + } ); + } ); + } ); +} ); diff --git a/tests/commands/update.js b/tests/commands/update.js new file mode 100644 index 0000000..b45539a --- /dev/null +++ b/tests/commands/update.js @@ -0,0 +1,299 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* jshint mocha:true */ + +'use strict'; + +const fs = require( 'fs' ); +const path = require( 'path' ); +const sinon = require( 'sinon' ); +const mockery = require( 'mockery' ); +const expect = require( 'chai' ).expect; + +describe( 'commands/update', () => { + let updateCommand, sandbox, stubs, data; + + beforeEach( () => { + sandbox = sinon.sandbox.create(); + + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + exec: sandbox.stub(), + fs: { + existsSync: sandbox.stub( fs, 'existsSync' ) + }, + path: { + join: sandbox.stub( path, 'join', ( ...chunks ) => chunks.join( '/' ) ) + }, + bootstrapCommand: { + execute: sandbox.stub() + }, + execCommand: { + execute: sandbox.stub() + } + }; + + data = { + packageName: 'test-package', + options: { + cwd: __dirname, + packages: 'packages' + }, + repository: { + directory: 'test-package', + url: 'git@github.com/organization/test-package.git', + branch: 'master' + } + }; + + mockery.registerMock( './exec', stubs.execCommand ); + mockery.registerMock( './bootstrap', stubs.bootstrapCommand ); + + updateCommand = require( '../../lib/commands/update' ); + } ); + + afterEach( () => { + sandbox.restore(); + mockery.disable(); + } ); + + describe( 'execute()', () => { + it( 'clones a package if is not available', () => { + stubs.fs.existsSync.returns( false ); + stubs.bootstrapCommand.execute.returns( Promise.resolve( { + logs: getCommandLogs( 'Cloned.' ) + } ) ); + + return updateCommand.execute( data ) + .then( ( response ) => { + expect( response.logs.info ).to.deep.equal( [ + 'Package "test-package" was not found. Cloning...', + 'Cloned.' + ] ); + + expect( stubs.bootstrapCommand.execute.calledOnce ).to.equal( true ); + } ); + } ); + + it( 'resolves promise after pulling the changes', () => { + stubs.fs.existsSync.returns( true ); + + const exec = stubs.execCommand.execute; + + exec.onCall( 0 ).returns( Promise.resolve( { + logs: getCommandLogs( '' ) + } ) ); + + exec.onCall( 1 ).returns( Promise.resolve( { + logs: getCommandLogs( '' ) + } ) ); + + exec.onCall( 2 ).returns( Promise.resolve( { + logs: getCommandLogs( 'Already on \'master\'.' ) + } ) ); + + exec.onCall( 3 ).returns( Promise.resolve( { + logs: getCommandLogs( '* master\n remotes/origin/master' ) + } ) ); + + exec.onCall( 4 ).returns( Promise.resolve( { + logs: getCommandLogs( 'Already up-to-date.' ) + } ) ); + + return updateCommand.execute( data ) + .then( ( response ) => { + expect( exec.getCall( 0 ).args[ 0 ].arguments[ 0 ] ).to.equal( 'git status -s' ); + expect( exec.getCall( 1 ).args[ 0 ].arguments[ 0 ] ).to.equal( 'git fetch' ); + expect( exec.getCall( 2 ).args[ 0 ].arguments[ 0 ] ).to.equal( 'git checkout master' ); + expect( exec.getCall( 3 ).args[ 0 ].arguments[ 0 ] ).to.equal( 'git branch' ); + expect( exec.getCall( 4 ).args[ 0 ].arguments[ 0 ] ).to.equal( 'git pull origin master' ); + + expect( response.logs.info ).to.deep.equal( [ + 'Already on \'master\'.', + 'Already up-to-date.' + ] ); + + expect( exec.callCount ).to.equal( 5 ); + } ); + } ); + + it( 'aborts if package has uncommitted changes', () => { + stubs.fs.existsSync.returns( true ); + + const exec = stubs.execCommand.execute; + + exec.returns( Promise.resolve( { + logs: getCommandLogs( ' M first-file.js\n ?? second-file.js' ) + } ) ); + + return updateCommand.execute( data ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + ( response ) => { + const errMsg = 'Error: Package "test-package" has uncommitted changes. Aborted.'; + + expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( errMsg ); + expect( exec.firstCall.args[ 0 ].arguments[ 0 ] ).to.equal( 'git status -s' ); + } + ); + } ); + + it( 'does not pull the changes if detached on a commit or a tag', () => { + stubs.fs.existsSync.returns( true ); + + data.repository.branch = '1a0ff0a2ee60549656177cd2a18b057764ec2146'; + + const exec = stubs.execCommand.execute; + + exec.onCall( 0 ).returns( Promise.resolve( { + logs: getCommandLogs( '' ) + } ) ); + + exec.onCall( 1 ).returns( Promise.resolve( { + logs: getCommandLogs( '' ) + } ) ); + + exec.onCall( 2 ).returns( Promise.resolve( { + logs: getCommandLogs( 'Note: checking out \'1a0ff0a2ee60549656177cd2a18b057764ec2146\'.' ) + } ) ); + + exec.onCall( 3 ).returns( Promise.resolve( { + logs: getCommandLogs( [ + '* (HEAD detached at 1a0ff0a2ee60549656177cd2a18b057764ec2146)', + ' master', + ' remotes/origin/master' + ].join( '\n' ) ) + } ) ); + + return updateCommand.execute( data ) + .then( ( response ) => { + expect( response.logs.info ).to.deep.equal( [ + 'Note: checking out \'1a0ff0a2ee60549656177cd2a18b057764ec2146\'.', + 'Package "test-package" is on a detached commit.' + ] ); + + expect( exec.callCount ).to.equal( 4 ); + } ); + } ); + + it( 'aborts if user wants to pull changes from non-existing branch', () => { + stubs.fs.existsSync.returns( true ); + + data.repository.branch = 'develop'; + + const exec = stubs.execCommand.execute; + + exec.onCall( 0 ).returns( Promise.resolve( { + logs: getCommandLogs( '' ) + } ) ); + + exec.onCall( 1 ).returns( Promise.resolve( { + logs: getCommandLogs( '' ) + } ) ); + + exec.onCall( 2 ).returns( Promise.resolve( { + logs: getCommandLogs( 'Already on \'develop\'.' ) + } ) ); + + exec.onCall( 3 ).returns( Promise.resolve( { + logs: getCommandLogs( '* develop' ) + } ) ); + + exec.onCall( 4 ).returns( Promise.reject( { + logs: getCommandLogs( 'fatal: Couldn\'t find remote ref develop', true ) + } ) ); + + return updateCommand.execute( data ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + ( response ) => { + expect( response.logs.info ).to.deep.equal( [ + 'Already on \'develop\'.' + ] ); + + const errMsg = 'Error: fatal: Couldn\'t find remote ref develop'; + expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( errMsg ); + + expect( exec.callCount ).to.equal( 5 ); + } + ); + } ); + + it( 'aborts if user wants to check out to non-existing branch', () => { + stubs.fs.existsSync.returns( true ); + + data.repository.branch = 'non-existing-branch'; + + const exec = stubs.execCommand.execute; + + exec.onCall( 0 ).returns( Promise.resolve( { + logs: getCommandLogs( '' ) + } ) ); + + exec.onCall( 1 ).returns( Promise.resolve( { + logs: getCommandLogs( '' ) + } ) ); + + exec.onCall( 2 ).returns( Promise.reject( { + logs: getCommandLogs( 'error: pathspec \'ggdfgd\' did not match any file(s) known to git.', true ), + } ) ); + + return updateCommand.execute( data ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + ( response ) => { + const errMsg = 'Error: pathspec \'ggdfgd\' did not match any file(s) known to git.'; + expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( errMsg ); + + expect( exec.callCount ).to.equal( 3 ); + } + ); + } ); + } ); + + describe( 'afterExecute()', () => { + it( 'informs about number of processed packages', () => { + const consoleLog = sandbox.stub( console, 'log' ); + + const processedPackages = new Set(); + processedPackages.add( 'package-1' ); + processedPackages.add( 'package-2' ); + + updateCommand.afterExecute( processedPackages ); + + expect( consoleLog.calledOnce ).to.equal( true ); + expect( consoleLog.firstCall.args[ 0 ] ).to.match( /2 packages have been processed\./ ); + + consoleLog.restore(); + } ); + } ); + + function getCommandLogs( msg, isError = false ) { + const logs = { + error: [], + info: [] + }; + + if ( isError ) { + logs.error.push( msg ); + } else { + logs.info.push( msg ); + } + + return logs; + } +} ); diff --git a/tests/fixtures/project-a/package.json b/tests/fixtures/project-a/package.json new file mode 100644 index 0000000..2293083 --- /dev/null +++ b/tests/fixtures/project-a/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "test-foo": "organization/test-foo" + } +} diff --git a/tests/fixtures/project-with-options-in-mgitjson/package.json b/tests/fixtures/project-with-options-in-mgitjson/package.json new file mode 100644 index 0000000..0ff9259 --- /dev/null +++ b/tests/fixtures/project-with-options-in-mgitjson/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "test-bar": "organization/test-bar" + } +}