diff --git a/lib/commands/status.js b/lib/commands/status.js index efb556e..b1e88e6 100644 --- a/lib/commands/status.js +++ b/lib/commands/status.js @@ -37,38 +37,38 @@ module.exports = { * @param {CommandData} data * @returns {Promise} */ - execute( data ) { + async execute( data ) { const execCommand = require( './exec' ); - const promises = [ - execCommand.execute( getExecData( 'git rev-parse HEAD' ) ), - execCommand.execute( getExecData( 'git status --branch --porcelain' ) ), - execCommand.execute( getExecData( 'git describe --abbrev=0 --tags' ) ), - execCommand.execute( getExecData( 'git log --tags --simplify-by-decoration --pretty="%S"' ) ) - ]; + let latestTag = null; + let currentTag = null; + let packageName = data.packageName; - return Promise.all( promises ) - .then( ( [ hashResponse, currentBranchStatusResponse, currentTagStatusResponse, latestTagStatusResponse ] ) => { - let packageName = data.packageName; + const hashResponse = await execCommand.execute( getExecData( 'git rev-parse HEAD' ) ); + const currentBranchStatusResponse = await execCommand.execute( getExecData( 'git status --branch --porcelain' ) ); + const latestTagStatusResponse = await execCommand.execute( getExecData( 'git log --tags --simplify-by-decoration --pretty="%S"' ) ); - const currentTag = currentTagStatusResponse.logs.info[ 0 ]; - const latestTag = latestTagStatusResponse.logs.info[ 0 ].trim().split( '\n' ).shift(); + if ( latestTagStatusResponse.logs.info.length ) { + const currentTagStatusResponse = await execCommand.execute( getExecData( 'git describe --abbrev=0 --tags' ) ); - for ( const packagePrefix of data.toolOptions.packagesPrefix ) { - packageName = packageName.replace( new RegExp( '^' + packagePrefix ), '' ); - } + latestTag = latestTagStatusResponse.logs.info[ 0 ].trim().split( '\n' ).shift(); + currentTag = currentTagStatusResponse.logs.info[ 0 ]; + } - const commandResponse = { - packageName, - status: gitStatusParser( currentBranchStatusResponse.logs.info[ 0 ], currentTag ), - commit: hashResponse.logs.info[ 0 ].slice( 0, 7 ), // Short version of the commit hash. - mrgitBranch: data.repository.branch, - mrgitTag: data.repository.tag, - latestTag - }; + for ( const packagePrefix of data.toolOptions.packagesPrefix ) { + packageName = packageName.replace( new RegExp( '^' + packagePrefix ), '' ); + } - return { response: commandResponse }; - } ); + const commandResponse = { + packageName, + status: gitStatusParser( currentBranchStatusResponse.logs.info[ 0 ], currentTag ), + commit: hashResponse.logs.info[ 0 ].slice( 0, 7 ), // Short version of the commit hash. + mrgitBranch: data.repository.branch, + mrgitTag: data.repository.tag, + latestTag + }; + + return { response: commandResponse }; function getExecData( command ) { return Object.assign( {}, data, { diff --git a/lib/commands/sync.js b/lib/commands/sync.js index bf582da..09ffc27 100644 --- a/lib/commands/sync.js +++ b/lib/commands/sync.js @@ -83,6 +83,11 @@ module.exports = { const commandOutput = await execCommand.execute( getExecData( 'git log --tags --simplify-by-decoration --pretty="%S"' ) ); + + if ( !commandOutput.logs.info.length ) { + throw new Error( `Can't check out the latest tag as package "${ data.packageName }" has no tags. Aborted.` ); + } + const latestTag = commandOutput.logs.info[ 0 ].trim().split( '\n' ).shift(); checkoutValue = 'tags/' + latestTag.trim(); diff --git a/lib/utils/log.js b/lib/utils/log.js index cdd7c85..3f8b805 100644 --- a/lib/utils/log.js +++ b/lib/utils/log.js @@ -31,7 +31,6 @@ module.exports = function log() { msg = msg.trim(); - /* istanbul ignore if */ if ( !msg ) { return; } diff --git a/tests/commands/status.js b/tests/commands/status.js index 85ac4c3..5f8ebef 100644 --- a/tests/commands/status.js +++ b/tests/commands/status.js @@ -142,10 +142,10 @@ describe( 'commands/status', () => { logs: { info: [ 'Response returned by "git status" command.' ] } } ); stubs.execCommand.execute.onCall( 2 ).resolves( { - logs: { info: [ 'Response returned by "git describe" command.' ] } + logs: { info: [ '\nv35.3.2\nv35.3.1\nv35.3.0\nv35.2.1\nv35.2.0' ] } } ); stubs.execCommand.execute.onCall( 3 ).resolves( { - logs: { info: [ '\nv35.3.2\nv35.3.1\nv35.3.0\nv35.2.1\nv35.2.0' ] } + logs: { info: [ 'Response returned by "git describe" command.' ] } } ); stubs.gitStatusParser.returns( { response: 'Parsed response.' } ); @@ -160,10 +160,10 @@ describe( 'commands/status', () => { getCommandArguments( 'git status --branch --porcelain' ) ); expect( stubs.execCommand.execute.getCall( 2 ).args[ 0 ] ).to.deep.equal( - getCommandArguments( 'git describe --abbrev=0 --tags' ) + getCommandArguments( 'git log --tags --simplify-by-decoration --pretty="%S"' ) ); expect( stubs.execCommand.execute.getCall( 3 ).args[ 0 ] ).to.deep.equal( - getCommandArguments( 'git log --tags --simplify-by-decoration --pretty="%S"' ) + getCommandArguments( 'git describe --abbrev=0 --tags' ) ); expect( stubs.gitStatusParser.calledOnce ).to.equal( true ); @@ -181,6 +181,47 @@ describe( 'commands/status', () => { } ); } ); + it( 'works properly for repositories without tags', () => { + stubs.execCommand.execute.onCall( 0 ).resolves( { + logs: { info: [ '6bfd379a56a32c9f8b6e58bf08e39c124cdbae10' ] } + } ); + stubs.execCommand.execute.onCall( 1 ).resolves( { + logs: { info: [ 'Response returned by "git status" command.' ] } + } ); + stubs.execCommand.execute.onCall( 2 ).resolves( { + logs: { info: [] } + } ); + + stubs.gitStatusParser.returns( { response: 'Parsed response.' } ); + + return statusCommand.execute( commandData ) + .then( statusResponse => { + expect( stubs.execCommand.execute.callCount ).to.equal( 3 ); + expect( stubs.execCommand.execute.getCall( 0 ).args[ 0 ] ).to.deep.equal( + getCommandArguments( 'git rev-parse HEAD' ) + ); + expect( stubs.execCommand.execute.getCall( 1 ).args[ 0 ] ).to.deep.equal( + getCommandArguments( 'git status --branch --porcelain' ) + ); + expect( stubs.execCommand.execute.getCall( 2 ).args[ 0 ] ).to.deep.equal( + getCommandArguments( 'git log --tags --simplify-by-decoration --pretty="%S"' ) + ); + + expect( stubs.gitStatusParser.calledOnce ).to.equal( true ); + expect( stubs.gitStatusParser.firstCall.args[ 0 ] ).to.equal( 'Response returned by "git status" command.' ); + expect( stubs.gitStatusParser.firstCall.args[ 1 ] ).to.equal( null ); + + expect( statusResponse.response ).to.deep.equal( { + packageName: 'test-package', + status: { response: 'Parsed response.' }, + commit: '6bfd379', + mrgitBranch: 'master', + mrgitTag: undefined, + latestTag: null + } ); + } ); + } ); + it( 'modifies the package name if "packagesPrefix" is an array', () => { commandData.toolOptions.packagesPrefix = [ '@ckeditor/ckeditor-', @@ -194,10 +235,10 @@ describe( 'commands/status', () => { logs: { info: [ 'Response returned by "git status" command.' ] } } ); stubs.execCommand.execute.onCall( 2 ).resolves( { - logs: { info: [ 'Response returned by "git describe" command.' ] } + logs: { info: [ 'v35.3.2\nv35.3.1\nv35.3.0\nv35.2.1\nv35.2.0\n' ] } } ); stubs.execCommand.execute.onCall( 3 ).resolves( { - logs: { info: [ 'v35.3.2\nv35.3.1\nv35.3.0\nv35.2.1\nv35.2.0\n' ] } + logs: { info: [ 'Response returned by "git describe" command.' ] } } ); stubs.gitStatusParser.returns( { response: 'Parsed response.' } ); @@ -212,10 +253,10 @@ describe( 'commands/status', () => { getCommandArguments( 'git status --branch --porcelain' ) ); expect( stubs.execCommand.execute.getCall( 2 ).args[ 0 ] ).to.deep.equal( - getCommandArguments( 'git describe --abbrev=0 --tags' ) + getCommandArguments( 'git log --tags --simplify-by-decoration --pretty="%S"' ) ); expect( stubs.execCommand.execute.getCall( 3 ).args[ 0 ] ).to.deep.equal( - getCommandArguments( 'git log --tags --simplify-by-decoration --pretty="%S"' ) + getCommandArguments( 'git describe --abbrev=0 --tags' ) ); expect( stubs.gitStatusParser.calledOnce ).to.equal( true ); @@ -244,10 +285,10 @@ describe( 'commands/status', () => { logs: { info: [ 'Response returned by "git status" command.' ] } } ); stubs.execCommand.execute.onCall( 2 ).resolves( { - logs: { info: [ 'Response returned by "git describe" command.' ] } + logs: { info: [ '\nv35.3.2\nv35.3.1\nv35.3.0\nv35.2.1\nv35.2.0' ] } } ); stubs.execCommand.execute.onCall( 3 ).resolves( { - logs: { info: [ '\nv35.3.2\nv35.3.1\nv35.3.0\nv35.2.1\nv35.2.0' ] } + logs: { info: [ 'Response returned by "git describe" command.' ] } } ); stubs.gitStatusParser.returns( { response: 'Parsed response.' } ); @@ -262,10 +303,10 @@ describe( 'commands/status', () => { getCommandArguments( 'git status --branch --porcelain' ) ); expect( stubs.execCommand.execute.getCall( 2 ).args[ 0 ] ).to.deep.equal( - getCommandArguments( 'git describe --abbrev=0 --tags' ) + getCommandArguments( 'git log --tags --simplify-by-decoration --pretty="%S"' ) ); expect( stubs.execCommand.execute.getCall( 3 ).args[ 0 ] ).to.deep.equal( - getCommandArguments( 'git log --tags --simplify-by-decoration --pretty="%S"' ) + getCommandArguments( 'git describe --abbrev=0 --tags' ) ); expect( stubs.gitStatusParser.calledOnce ).to.equal( true ); diff --git a/tests/commands/sync.js b/tests/commands/sync.js index 74fffb6..62c7af7 100644 --- a/tests/commands/sync.js +++ b/tests/commands/sync.js @@ -466,6 +466,44 @@ describe( 'commands/sync', () => { } ); } ); + it( 'throws an error when trying to check out the latest tag in repository without tags', () => { + commandData.repository.tag = 'latest'; + + 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() + } ) ); + + return syncCommand.execute( commandData ) + .then( () => { + throw new Error( 'Expected to throw' ); + } ) + .catch( 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 log --tags --simplify-by-decoration --pretty="%S"' + ); + + expect( exec.callCount ).to.equal( 3 ); + + expect( response.logs.error[ 0 ] ).to.equal( + 'Can\'t check out the latest tag as package "test-package" has no tags. Aborted.' + ); + } ); + } ); + it( 'aborts if package has uncommitted changes', () => { stubs.fs.existsSync.returns( true ); @@ -758,7 +796,7 @@ describe( 'commands/sync', () => { if ( isError ) { logs.error.push( msg ); - } else { + } else if ( msg ) { logs.info.push( msg ); } diff --git a/tests/utils/log.js b/tests/utils/log.js new file mode 100644 index 0000000..1cb3ba1 --- /dev/null +++ b/tests/utils/log.js @@ -0,0 +1,199 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* jshint mocha:true */ + +'use strict'; + +const sinon = require( 'sinon' ); +const expect = require( 'chai' ).expect; + +describe( 'utils/log', () => { + let log, stubs; + + beforeEach( () => { + log = require( '../../lib/utils/log' ); + + stubs = { + info: sinon.stub(), + error: sinon.stub(), + log: sinon.stub() + }; + } ); + + afterEach( () => { + sinon.restore(); + } ); + + describe( 'log()', () => { + it( 'returns the logger', () => { + const logger = log(); + + expect( logger ).to.be.an( 'object' ); + expect( logger.info ).to.be.a( 'function' ); + expect( logger.error ).to.be.a( 'function' ); + expect( logger.log ).to.be.a( 'function' ); + expect( logger.concat ).to.be.a( 'function' ); + expect( logger.all ).to.be.a( 'function' ); + } ); + } ); + + describe( 'logger', () => { + let logger; + + beforeEach( () => { + logger = log(); + } ); + + describe( 'info()', () => { + it( 'calls the log() function with the received message and the type set to "info"', () => { + logger.log = stubs.log; + + logger.info( 'Info message.' ); + + expect( stubs.log.callCount ).to.equal( 1 ); + expect( stubs.log.getCall( 0 ).args[ 0 ] ).to.equal( 'info' ); + expect( stubs.log.getCall( 0 ).args[ 1 ] ).to.equal( 'Info message.' ); + } ); + } ); + + describe( 'error()', () => { + it( 'calls the log() function with the received message and the type set to "error"', () => { + logger.log = stubs.log; + + logger.error( 'Error message.' ); + + expect( stubs.log.callCount ).to.equal( 1 ); + expect( stubs.log.getCall( 0 ).args[ 0 ] ).to.equal( 'error' ); + expect( stubs.log.getCall( 0 ).args[ 1 ] ).to.equal( 'Error message.' ); + } ); + + it( 'calls the log() function with the stack trace of the received error and the type set to "error"', () => { + logger.log = stubs.log; + + const errorStack = [ + '-Error: Error message.', + '- at foo (path/to/foo.js:10:20)', + '- at bar (path/to/bar.js:30:40)' + ].join( '\n' ); + + const error = new Error( 'Error message.' ); + error.stack = errorStack; + + logger.error( error ); + + expect( stubs.log.callCount ).to.equal( 1 ); + expect( stubs.log.getCall( 0 ).args[ 0 ] ).to.equal( 'error' ); + expect( stubs.log.getCall( 0 ).args[ 1 ] ).to.equal( errorStack ); + } ); + } ); + + describe( 'log()', () => { + it( 'stores messages of the "info" type', () => { + expect( logger.all() ).to.deep.equal( { + error: [], + info: [] + } ); + + logger.log( 'info', 'Info message.' ); + + expect( logger.all() ).to.deep.equal( { + error: [], + info: [ 'Info message.' ] + } ); + } ); + + it( 'stores messages of the "error" type', () => { + expect( logger.all() ).to.deep.equal( { + error: [], + info: [] + } ); + + logger.log( 'error', 'Error message.' ); + + expect( logger.all() ).to.deep.equal( { + error: [ 'Error message.' ], + info: [] + } ); + } ); + + it( 'trims whitespaces from received messages', () => { + expect( logger.all() ).to.deep.equal( { + error: [], + info: [] + } ); + + logger.log( 'info', ' Info message.\n ' ); + + expect( logger.all() ).to.deep.equal( { + error: [], + info: [ 'Info message.' ] + } ); + } ); + + it( 'ignores the "undefined" value passed as the message', () => { + expect( logger.all() ).to.deep.equal( { + error: [], + info: [] + } ); + + logger.log( 'info', undefined ); + + expect( logger.all() ).to.deep.equal( { + error: [], + info: [] + } ); + } ); + + it( 'ignores messages consisting of whitespace alone', () => { + expect( logger.all() ).to.deep.equal( { + error: [], + info: [] + } ); + + logger.log( 'info', ' ' ); + + expect( logger.all() ).to.deep.equal( { + error: [], + info: [] + } ); + } ); + } ); + + describe( 'concat()', () => { + it( 'passes messages of the respective types to the info() and error() functions', () => { + logger.info = stubs.info; + logger.error = stubs.error; + + logger.concat( { + info: [ 'Info message 1.', 'Info message 2.' ], + error: [ 'Error message 1.', 'Error message 2.' ] + } ); + + expect( stubs.info.callCount ).to.equal( 2 ); + expect( stubs.info.getCall( 0 ).args[ 0 ] ).to.equal( 'Info message 1.' ); + expect( stubs.info.getCall( 1 ).args[ 0 ] ).to.equal( 'Info message 2.' ); + + expect( stubs.error.callCount ).to.equal( 2 ); + expect( stubs.error.getCall( 0 ).args[ 0 ] ).to.equal( 'Error message 1.' ); + expect( stubs.error.getCall( 1 ).args[ 0 ] ).to.equal( 'Error message 2.' ); + } ); + } ); + + describe( 'all()', () => { + it( 'returns all stored messages', () => { + logger.concat( { + info: [ 'Info message 1.', 'Info message 2.' ], + error: [ 'Error message 1.', 'Error message 2.' ] + } ); + + expect( logger.all() ).to.deep.equal( { + info: [ 'Info message 1.', 'Info message 2.' ], + error: [ 'Error message 1.', 'Error message 2.' ] + } ); + } ); + } ); + } ); +} );