diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6aedc..e157279 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Changelog ========= +## [0.9.0](https://github.com/cksource/mgit2/compare/v0.8.1...v0.9.0) (2018-11-22) + +### Features + +* The `mgit bootstrap` and `mgit update` commands will try pulling changes twice in case of a network hang-up. Closes [#87](https://github.com/cksource/mgit2/issues/87). ([47e6840](https://github.com/cksource/mgit2/commit/47e6840)) + + ## [0.8.1](https://github.com/cksource/mgit2/compare/v0.8.0...v0.8.1) (2018-11-19) ### Bug fixes diff --git a/lib/commands/sync.js b/lib/commands/sync.js index f56c92d..11ef77d 100644 --- a/lib/commands/sync.js +++ b/lib/commands/sync.js @@ -50,12 +50,14 @@ module.exports = { // Package is not cloned. if ( !fs.existsSync( destinationPath ) ) { + log.info( `Package "${ data.packageName }" was not found. Cloning...` ); + return this._clonePackage( { path: destinationPath, name: data.packageName, url: data.repository.url, branch: data.repository.branch - }, data.mgitOptions ); + }, data.mgitOptions, { log } ); } return execCommand.execute( getExecData( 'git status -s' ) ) @@ -178,12 +180,13 @@ module.exports = { * @param {String} packageDetails.path An absolute path where the package should be cloned. * @param {String} packageDetails.branch A branch on which the repository will be checked out after cloning. * @param {Options} mgitOptions Options resolved by mgit. + * @param {Object} options Additional options which aren't related to mgit. + * @param {Logger} options.log Logger + * @param {Boolean} [options.doNotTryAgain=false] If set to `true`, bootstrap command won't be executed again. * @returns {Promise} */ - _clonePackage( packageDetails, mgitOptions ) { - const log = require( '../utils/log' )(); - - log.info( `Package "${ packageDetails.name }" was not found. Cloning...` ); + _clonePackage( packageDetails, mgitOptions, options ) { + const log = options.log; const command = [ `git clone --progress "${ packageDetails.url }" "${ packageDetails.path }"`, @@ -215,6 +218,42 @@ module.exports = { } return commandOutput; + } ) + .catch( error => { + if ( isRemoteHungUpError( error ) && !options.doNotTryAgain ) { + return delay( 5000 ).then( () => { + return this._clonePackage( packageDetails, mgitOptions, { log, doNotTryAgain: true } ); + } ); + } + + log.error( error ); + + return Promise.reject( { logs: log.all() } ); } ); } }; + +// See: https://github.com/cksource/mgit2/issues/87 +function isRemoteHungUpError( error ) { + if ( typeof error != 'string' ) { + error = error.toString(); + } + + const fatalErrors = error.split( '\n' ) + .filter( message => message.startsWith( 'fatal:' ) ) + .map( message => message.trim() ); + + if ( fatalErrors.length !== 3 ) { + return false; + } + + return fatalErrors[ 0 ] == 'fatal: The remote end hung up unexpectedly' && + fatalErrors[ 1 ] == 'fatal: early EOF' && + fatalErrors[ 2 ] == 'fatal: index-pack failed'; +} + +function delay( ms ) { + return new Promise( resolve => { + setTimeout( resolve, ms ); + } ); +} diff --git a/lib/utils/log.js b/lib/utils/log.js index f04d5d0..3d91063 100644 --- a/lib/utils/log.js +++ b/lib/utils/log.js @@ -59,6 +59,14 @@ module.exports = function log() { return logger; }; +/** + * @typedef {Object} Logger + * + * @property {Function} info A function that informs about process. + * + * @property {Function} error A function that informs about errors. + */ + /** * @typedef {Object} Logs * diff --git a/package.json b/package.json index a60a129..d4ee93d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mgit2", - "version": "0.8.1", + "version": "0.9.0", "description": "A tool for managing projects build using multiple repositories.", "keywords": [ "git", diff --git a/tests/commands/sync.js b/tests/commands/sync.js index 4850291..1103023 100644 --- a/tests/commands/sync.js +++ b/tests/commands/sync.js @@ -43,8 +43,8 @@ describe( 'commands/sync', () => { }; mgitOptions = { - cwd: __dirname, - packages: __dirname + '/packages', + cwd: '/tmp', + packages: '/tmp/packages', resolverPath: 'PATH_TO_RESOLVER' }; @@ -92,10 +92,10 @@ describe( 'commands/sync', () => { // Clone the repository. expect( cloneCommand[ 0 ] ).to.equal( - `git clone --progress "git@github.com/organization/test-package.git" "${ __dirname }/packages/test-package"` + 'git clone --progress "git@github.com/organization/test-package.git" "/tmp/packages/test-package"' ); // Change the directory to cloned package. - expect( cloneCommand[ 1 ] ).to.equal( `cd "${ __dirname }/packages/test-package"` ); + expect( cloneCommand[ 1 ] ).to.equal( 'cd "/tmp/packages/test-package"' ); // And check out to proper branch. expect( cloneCommand[ 2 ] ).to.equal( 'git checkout --quiet master' ); @@ -135,6 +135,55 @@ describe( 'commands/sync', () => { expect( response.packages ).to.deep.equal( [ 'test-bar' ] ); } ); } ); + + it( 'tries to install missing packages once again if git ends with unexpected error', function() { + this.timeout( 5500 ); + + stubs.fs.existsSync.returns( false ); + + stubs.shell.onFirstCall().returns( Promise.reject( [ + 'exec: Cloning into \'/some/path\'...', + 'remote: Enumerating objects: 6, done.', + 'remote: Counting objects: 100% (6/6), done.', + 'remote: Compressing objects: 100% (6/6), done.', + 'packet_write_wait: Connection to 000.00.000.000 port 22: Broken pipe', + 'fatal: The remote end hung up unexpectedly', + 'fatal: early EOF', + 'fatal: index-pack failed' + ].join( '\n' ) ) ); + + stubs.shell.onSecondCall().returns( Promise.resolve( 'Git clone log.' ) ); + + return syncCommand.execute( commandData ) + .then( response => { + expect( stubs.shell.calledTwice ).to.equal( true ); + + const firstCommand = stubs.shell.firstCall.args[ 0 ].split( ' && ' ); + + // Clone the repository for the first time. It failed. + expect( firstCommand[ 0 ] ) + .to.equal( 'git clone --progress "git@github.com/organization/test-package.git" "/tmp/packages/test-package"' ); + // Change the directory to cloned package. + expect( firstCommand[ 1 ] ).to.equal( 'cd "/tmp/packages/test-package"' ); + // And check out to proper branch. + expect( firstCommand[ 2 ] ).to.equal( 'git checkout --quiet master' ); + + const secondCommand = stubs.shell.secondCall.args[ 0 ].split( ' && ' ); + + // Clone the repository for the second time. It succeed. + expect( secondCommand[ 0 ] ) + .to.equal( 'git clone --progress "git@github.com/organization/test-package.git" "/tmp/packages/test-package"' ); + // Change the directory to cloned package. + expect( secondCommand[ 1 ] ).to.equal( 'cd "/tmp/packages/test-package"' ); + // And check out to proper branch. + expect( secondCommand[ 2 ] ).to.equal( 'git checkout --quiet master' ); + + expect( response.logs.info ).to.deep.equal( [ + 'Package "test-package" was not found. Cloning...', + 'Git clone log.' + ] ); + } ); + } ); } ); describe( 'the package is already installed', () => { @@ -359,15 +408,14 @@ describe( 'commands/sync', () => { } ); syncCommand.afterExecute( processedPackages, null, mgitOptions ); + consoleLog.restore(); - expect( consoleLog.callCount ).to.equal( 3 ); + expect( consoleLog.callCount ).to.equal( 4 ); expect( consoleLog.firstCall.args[ 0 ] ).to.match( /2 packages have been processed\./ ); expect( consoleLog.secondCall.args[ 0 ] ).to.match( /Paths to directories listed below are skipped by mgit because they are not defined in "mgit\.json":/ ); expect( consoleLog.thirdCall.args[ 0 ] ).to.match( / {2}- .*\/packages\/package-3/ ); - - consoleLog.restore(); } ); it( 'informs about differences between packages in directory and defined in mgit.json for scopes packages', () => { @@ -418,15 +466,14 @@ describe( 'commands/sync', () => { } ); syncCommand.afterExecute( processedPackages, null, mgitOptions ); + consoleLog.restore(); - expect( consoleLog.callCount ).to.equal( 3 ); + expect( consoleLog.callCount ).to.equal( 5 ); expect( consoleLog.firstCall.args[ 0 ] ).to.match( /2 packages have been processed\./ ); expect( consoleLog.secondCall.args[ 0 ] ).to.match( /Paths to directories listed below are skipped by mgit because they are not defined in "mgit\.json":/ ); - expect( consoleLog.thirdCall.args[ 0 ] ).to.match( / {2}- .*\/packages\/@foo\/package-3/ ); - - consoleLog.restore(); + expect( consoleLog.getCall( 3 ).args[ 0 ] ).to.match( / {2}- .*\/packages\/@foo\/package-3/ ); } ); it( 'does not inform about differences between packages in directory and defined in mgit.json if everything seems to be ok', () => {