From 638e5fb076b2ad38805f7352bdde62360d4ebea1 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Thu, 9 Feb 2017 14:22:15 +0100 Subject: [PATCH 01/12] Fix: Prevent to pulling changes from not-existing branches. --- lib/commands/update.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/commands/update.js b/lib/commands/update.js index 95aa314..0043563 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -68,11 +68,11 @@ module.exports = { log.concat( response.logs ); } ) .then( () => { - return execCommand.execute( getExecData( 'git branch' ) ); + return execCommand.execute( getExecData( 'git branch -a' ) ); } ) .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,6 +81,13 @@ module.exports = { return resolve( { logs: log.all() } ); } + const isRemoteBranchAvailableRegexp = new RegExp( `remotes\\\/origin\\\/${ data.repository.branch }` ); + + // Check whether the remote branch is available. + if ( !stdout.match( isRemoteBranchAvailableRegexp ) ) { + throw new Error( `Branch "${ data.repository.branch }" is not available on server.` ); + } + return execCommand.execute( getExecData( `git pull origin ${ data.repository.branch }` ) ); } ) .then( ( response ) => { From 584f34152d782cc2f26453e10b93c4a16ef01925 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Feb 2017 09:21:49 +0100 Subject: [PATCH 02/12] Tests: Added test for commands/exec. --- lib/commands/exec.js | 10 ++-- package.json | 1 + tests/commands/exec.js | 129 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 tests/commands/exec.js diff --git a/lib/commands/exec.js b/lib/commands/exec.js index c464872..1172cec 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -11,17 +11,17 @@ const exec = require( '../utils/exec' ); module.exports = { /** - * @param {Array.} parameters Arguments that user provided calling the mgit. + * @param {Array.} 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.args 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 @@ -42,7 +42,7 @@ module.exports = { process.chdir( newCwd ); - exec( data.parameters[ 0 ] ) + exec( data.args[ 0 ] ) .then( ( stdout ) => { process.chdir( data.options.cwd ); 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/exec.js b/tests/commands/exec.js new file mode 100644 index 0000000..c0e619b --- /dev/null +++ b/tests/commands/exec.js @@ -0,0 +1,129 @@ +/** + * @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 = { + args: [ 'exec', '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( () => { + 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 ); + } ); + } ); + } ); +} ); From 8f48c0637e1a90141f6482e95448ee47b1367cda Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Feb 2017 10:41:02 +0100 Subject: [PATCH 03/12] Tests: Added test for commands/savehashes. --- lib/commands/savehashes.js | 4 +- tests/commands/savehashes.js | 134 +++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 tests/commands/savehashes.js diff --git a/lib/commands/savehashes.js b/lib/commands/savehashes.js index 0d1beed..009f537 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, @@ -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/tests/commands/savehashes.js b/tests/commands/savehashes.js new file mode 100644 index 0000000..c098a9d --- /dev/null +++ b/tests/commands/savehashes.js @@ -0,0 +1,134 @@ +/** + * @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( 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' + } ); + } ); + } ); +} ); From e42e16d14ed8a831a48dd12831cc2a5bbf04a0fd Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Feb 2017 11:45:33 +0100 Subject: [PATCH 04/12] Tests: Added test for command/bootstrap. Fixes #31. --- lib/commands/bootstrap.js | 31 ++-- tests/commands/bootstrap.js | 157 ++++++++++++++++++ tests/fixtures/project-a/package.json | 5 + .../package.json | 5 + 4 files changed, 184 insertions(+), 14 deletions(-) create mode 100644 tests/commands/bootstrap.js create mode 100644 tests/fixtures/project-a/package.json create mode 100644 tests/fixtures/project-with-options-in-mgitjson/package.json diff --git a/lib/commands/bootstrap.js b/lib/commands/bootstrap.js index 7b4ae67..ac8585d 100644 --- a/lib/commands/bootstrap.js +++ b/lib/commands/bootstrap.js @@ -25,19 +25,22 @@ module.exports = { return new Promise( ( resolve, reject ) => { const destinationPath = path.join( data.options.packages, data.repository.directory ); - // Package is already cloned. - if ( fs.existsSync( destinationPath ) ) { - log.info( `Package "${ data.packageName }" is already cloned. Skipping.` ); - - return resolve( { logs: log.all() } ); + let promise = Promise.resolve( '' ); + + // Package is not cloned. + if ( !fs.existsSync( destinationPath ) ) { + const command = [ + `git clone --progress ${ data.repository.url } ${ destinationPath }`, + `cd ${ destinationPath }`, + `git checkout --quiet ${ data.repository.branch }` + ].join( ' && ' ); + + promise = exec( command ); + } else { + log.info( `Package "${ data.packageName }" is already cloned.` ); } - const command = - `git clone --progress ${ data.repository.url } ${ destinationPath } && ` + - `cd ${ destinationPath } && ` + - `git checkout --quiet ${ data.repository.branch }`; - - exec( command ) + promise .then( ( output ) => { log.info( output ); @@ -47,14 +50,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/tests/commands/bootstrap.js b/tests/commands/bootstrap.js new file mode 100644 index 0000000..4b1d88b --- /dev/null +++ b/tests/commands/bootstrap.js @@ -0,0 +1,157 @@ +/** + * @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 = { + args: [ 'exec', 'pwd' ], + 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 dependencies 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/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" + } +} From 890dc8fe410ef89b158f952d5ff4eb19e4856364 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Feb 2017 11:59:20 +0100 Subject: [PATCH 05/12] Fix: Removed "parameters" variable. Introduced "args" and "arguments" instead. Closes #3. --- lib/commands/bootstrap.js | 1 - lib/commands/exec.js | 4 ++-- lib/commands/savehashes.js | 2 +- lib/commands/update.js | 3 +-- lib/index.js | 12 ++++++------ tests/commands/exec.js | 4 +++- tests/commands/savehashes.js | 6 ++++++ 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/commands/bootstrap.js b/lib/commands/bootstrap.js index ac8585d..d77c98a 100644 --- a/lib/commands/bootstrap.js +++ b/lib/commands/bootstrap.js @@ -13,7 +13,6 @@ 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 diff --git a/lib/commands/exec.js b/lib/commands/exec.js index 1172cec..4418df6 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -21,7 +21,7 @@ module.exports = { /** * @param {Object} data - * @param {Object} data.args Arguments that user provided calling the mgit. + * @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 @@ -42,7 +42,7 @@ module.exports = { process.chdir( newCwd ); - exec( data.args[ 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 009f537..8b24df0 100644 --- a/lib/commands/savehashes.js +++ b/lib/commands/savehashes.js @@ -44,7 +44,7 @@ module.exports = { function getExecData( command ) { return Object.assign( {}, data, { - parameters: [ command ] + arguments: [ command ] } ); } }, diff --git a/lib/commands/update.js b/lib/commands/update.js index 95aa314..e242c04 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -12,7 +12,6 @@ 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 @@ -97,7 +96,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.} parameters Arguments that the user provided. + * @param {Array.} 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/tests/commands/exec.js b/tests/commands/exec.js index c0e619b..001bc70 100644 --- a/tests/commands/exec.js +++ b/tests/commands/exec.js @@ -39,7 +39,8 @@ describe( 'commands/exec', () => { }; data = { - args: [ 'exec', 'pwd' ], + // `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, @@ -63,6 +64,7 @@ describe( 'commands/exec', () => { 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].' ); } ); diff --git a/tests/commands/savehashes.js b/tests/commands/savehashes.js index c098a9d..d68f417 100644 --- a/tests/commands/savehashes.js +++ b/tests/commands/savehashes.js @@ -82,6 +82,12 @@ describe( 'commands/savehashes', () => { 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' From 284376e88ba81bc5fd84e38d202610ac2fa30027 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Feb 2017 13:56:39 +0100 Subject: [PATCH 06/12] Tests: Added test for command/update. Closes #41. --- lib/commands/bootstrap.js | 2 +- lib/commands/exec.js | 2 +- lib/commands/update.js | 3 +- tests/commands/bootstrap.js | 1 - tests/commands/update.js | 262 ++++++++++++++++++++++++++++++++++++ 5 files changed, 265 insertions(+), 5 deletions(-) create mode 100644 tests/commands/update.js diff --git a/lib/commands/bootstrap.js b/lib/commands/bootstrap.js index d77c98a..d6b3acc 100644 --- a/lib/commands/bootstrap.js +++ b/lib/commands/bootstrap.js @@ -15,7 +15,7 @@ module.exports = { * @param {Object} data * @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 ) { diff --git a/lib/commands/exec.js b/lib/commands/exec.js index 4418df6..07dd630 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -24,7 +24,7 @@ module.exports = { * @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 ) { diff --git a/lib/commands/update.js b/lib/commands/update.js index 16e5b5b..83a784e 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -14,7 +14,7 @@ module.exports = { * @param {Object} data * @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 ) { @@ -32,7 +32,6 @@ module.exports = { const bootstrapOptions = { options: data.options, packageName: data.packageName, - mgit: data.mgit, repository: data.repository }; diff --git a/tests/commands/bootstrap.js b/tests/commands/bootstrap.js index 4b1d88b..683b9f7 100644 --- a/tests/commands/bootstrap.js +++ b/tests/commands/bootstrap.js @@ -36,7 +36,6 @@ describe( 'commands/bootstrap', () => { }; data = { - args: [ 'exec', 'pwd' ], packageName: 'test-package', options: { cwd: __dirname, diff --git a/tests/commands/update.js b/tests/commands/update.js new file mode 100644 index 0000000..992417e --- /dev/null +++ b/tests/commands/update.js @@ -0,0 +1,262 @@ +/** + * @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 -a' ); + 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 uncommmitted changes', () => { + stubs.fs.existsSync.returns( true ); + + const exec = stubs.execCommand.execute; + + exec.returns( Promise.resolve( { + logs: getCommandLogs( ' M first-file.js\ ?? 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 a remote branch does not exist anymore', () => { + 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\n master\n remotes/origin/master' ) + } ) ); + + 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: Branch "develop" is not available on server.'; + expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( errMsg ); + + expect( exec.callCount ).to.equal( 4 ); + } + ); + } ); + } ); + + 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; + } +} ); From cc8e24fcb801f55a3f3c588cfd961ad5053ecd2e Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Feb 2017 14:04:40 +0100 Subject: [PATCH 07/12] Tests: Fixed invalid test name. --- tests/commands/bootstrap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/bootstrap.js b/tests/commands/bootstrap.js index 683b9f7..5fc6f06 100644 --- a/tests/commands/bootstrap.js +++ b/tests/commands/bootstrap.js @@ -122,7 +122,7 @@ describe( 'commands/bootstrap', () => { } ); } ); - it( 'installs dependencies of cloned package', () => { + it( 'installs devDependencies of cloned package', () => { data.options.recursive = true; data.options.packages = __dirname + '/../fixtures'; data.repository.directory = 'project-with-options-in-mgitjson'; From 8407aaede3e8f7a8fb8444ec33471a4142597682 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Feb 2017 14:07:09 +0100 Subject: [PATCH 08/12] Fix: Logger handles "undefined" message. --- lib/commands/bootstrap.js | 2 +- lib/utils/log.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/commands/bootstrap.js b/lib/commands/bootstrap.js index d6b3acc..b5aa6b1 100644 --- a/lib/commands/bootstrap.js +++ b/lib/commands/bootstrap.js @@ -24,7 +24,7 @@ module.exports = { return new Promise( ( resolve, reject ) => { const destinationPath = path.join( data.options.packages, data.repository.directory ); - let promise = Promise.resolve( '' ); + let promise = Promise.resolve(); // Package is not cloned. if ( !fs.existsSync( destinationPath ) ) { 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 ) { From 1ef53da1b52b92315fedecd3317fc82d57fa384d Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Feb 2017 14:13:51 +0100 Subject: [PATCH 09/12] Internal: Swap order in "if condition". --- lib/commands/bootstrap.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/commands/bootstrap.js b/lib/commands/bootstrap.js index b5aa6b1..ce903a4 100644 --- a/lib/commands/bootstrap.js +++ b/lib/commands/bootstrap.js @@ -27,7 +27,9 @@ module.exports = { let promise = Promise.resolve(); // Package is not cloned. - if ( !fs.existsSync( destinationPath ) ) { + if ( fs.existsSync( destinationPath ) ) { + log.info( `Package "${ data.packageName }" is already cloned.` ); + } else { const command = [ `git clone --progress ${ data.repository.url } ${ destinationPath }`, `cd ${ destinationPath }`, @@ -35,8 +37,6 @@ module.exports = { ].join( ' && ' ); promise = exec( command ); - } else { - log.info( `Package "${ data.packageName }" is already cloned.` ); } promise From 3fdc2e7f2f8f513f210f8f0b777bc7bd93352fde Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Feb 2017 14:22:51 +0100 Subject: [PATCH 10/12] Tests: Addes missing new line in output for "git status". --- tests/commands/update.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/update.js b/tests/commands/update.js index 992417e..e674739 100644 --- a/tests/commands/update.js +++ b/tests/commands/update.js @@ -131,7 +131,7 @@ describe( 'commands/update', () => { const exec = stubs.execCommand.execute; exec.returns( Promise.resolve( { - logs: getCommandLogs( ' M first-file.js\ ?? second-file.js' ) + logs: getCommandLogs( ' M first-file.js\n ?? second-file.js' ) } ) ); return updateCommand.execute( data ) From 880628645f56a79975c9e29bd3227845fc7a262a Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Feb 2017 14:24:04 +0100 Subject: [PATCH 11/12] Fix: Invalid comment. Promise.resolve will be used if required. --- lib/commands/bootstrap.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/commands/bootstrap.js b/lib/commands/bootstrap.js index ce903a4..6082f04 100644 --- a/lib/commands/bootstrap.js +++ b/lib/commands/bootstrap.js @@ -24,11 +24,13 @@ module.exports = { return new Promise( ( resolve, reject ) => { const destinationPath = path.join( data.options.packages, data.repository.directory ); - let promise = Promise.resolve(); + let promise; - // Package is not cloned. + // Package is already cloned. if ( fs.existsSync( destinationPath ) ) { log.info( `Package "${ data.packageName }" is already cloned.` ); + + promise = Promise.resolve(); } else { const command = [ `git clone --progress ${ data.repository.url } ${ destinationPath }`, From f271fb03ddd3dbb0c87d49a9483ef7376052b242 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Feb 2017 15:08:17 +0100 Subject: [PATCH 12/12] Fix: Prevent to checking out to non-existing branch. --- lib/commands/update.js | 19 ++++++++-------- tests/commands/update.js | 49 +++++++++++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/lib/commands/update.js b/lib/commands/update.js index 83a784e..bc88da9 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -60,13 +60,16 @@ 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 ); } ) .then( () => { - return execCommand.execute( getExecData( 'git branch -a' ) ); + return execCommand.execute( getExecData( 'git branch' ) ); } ) .then( ( response ) => { const stdout = response.logs.info.join( '\n' ).trim(); @@ -79,14 +82,10 @@ module.exports = { return resolve( { logs: log.all() } ); } - const isRemoteBranchAvailableRegexp = new RegExp( `remotes\\\/origin\\\/${ data.repository.branch }` ); - - // Check whether the remote branch is available. - if ( !stdout.match( isRemoteBranchAvailableRegexp ) ) { - throw new Error( `Branch "${ data.repository.branch }" is not available on server.` ); - } - - 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 ); diff --git a/tests/commands/update.js b/tests/commands/update.js index e674739..b45539a 100644 --- a/tests/commands/update.js +++ b/tests/commands/update.js @@ -113,7 +113,7 @@ describe( 'commands/update', () => { 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 -a' ); + 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( [ @@ -125,7 +125,7 @@ describe( 'commands/update', () => { } ); } ); - it( 'aborts if package has uncommmitted changes', () => { + it( 'aborts if package has uncommitted changes', () => { stubs.fs.existsSync.returns( true ); const exec = stubs.execCommand.execute; @@ -186,7 +186,7 @@ describe( 'commands/update', () => { } ); } ); - it( 'aborts if a remote branch does not exist anymore', () => { + it( 'aborts if user wants to pull changes from non-existing branch', () => { stubs.fs.existsSync.returns( true ); data.repository.branch = 'develop'; @@ -206,7 +206,11 @@ describe( 'commands/update', () => { } ) ); exec.onCall( 3 ).returns( Promise.resolve( { - logs: getCommandLogs( '* develop\n master\n remotes/origin/master' ) + logs: getCommandLogs( '* develop' ) + } ) ); + + exec.onCall( 4 ).returns( Promise.reject( { + logs: getCommandLogs( 'fatal: Couldn\'t find remote ref develop', true ) } ) ); return updateCommand.execute( data ) @@ -219,10 +223,43 @@ describe( 'commands/update', () => { 'Already on \'develop\'.' ] ); - const errMsg = 'Error: Branch "develop" is not available on server.'; + 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( 4 ); + expect( exec.callCount ).to.equal( 3 ); } ); } );