From 92f93999edffc7d3aa70f0446872b3a46e25d975 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Fri, 10 Aug 2018 14:46:56 +0200 Subject: [PATCH 01/38] "gitStatusParser()" returns additional property (#anythingToCommit) which checks whether changed files could be committed. --- lib/utils/gitstatusparser.js | 4 ++++ tests/utils/gitstatusparser.js | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/lib/utils/gitstatusparser.js b/lib/utils/gitstatusparser.js index cbcea13..79c655d 100644 --- a/lib/utils/gitstatusparser.js +++ b/lib/utils/gitstatusparser.js @@ -59,6 +59,10 @@ module.exports = function gitStatusParser( response ) { } return { + get anythingToCommit() { + return [ added, modified, deleted, renamed, unmerged, staged ].some( collection => collection.length ); + }, + branch, behind, ahead, diff --git a/tests/utils/gitstatusparser.js b/tests/utils/gitstatusparser.js index 695e54b..ed55195 100644 --- a/tests/utils/gitstatusparser.js +++ b/tests/utils/gitstatusparser.js @@ -35,6 +35,43 @@ const gitStatusResponse = [ describe( 'utils', () => { describe( 'gitStatusParser()', () => { + describe( '#anythingToCommit', () => { + it( 'returns false for untracked files', () => { + const gitStatusResponse = [ + '## master...origin/master', + '?? README.md', + '?? .eslintrc.js' + ].join( '\n' ); + + const status = gitStatusParser( gitStatusResponse ); + + expect( status.anythingToCommit ).to.equal( false ); + } ); + + it( 'returns true for any tracked file', () => { + const gitStatusResponse = [ + '## master...origin/master', + ' M lib/index.js' + ].join( '\n' ); + + const status = gitStatusParser( gitStatusResponse ); + + expect( status.anythingToCommit ).to.equal( true ); + } ); + + it( 'returns true for any tracked file and some untracked', () => { + const gitStatusResponse = [ + '## master...origin/master', + ' M lib/index.js', + '?? README.md', + ].join( '\n' ); + + const status = gitStatusParser( gitStatusResponse ); + + expect( status.anythingToCommit ).to.equal( true ); + } ); + } ); + it( 'returns branch name for freshly created', () => { const status = gitStatusParser( '## master' ); From 0f5de5144a394e65ab5b42a09cdace4ab42254b3 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Fri, 10 Aug 2018 14:47:19 +0200 Subject: [PATCH 02/38] Improve docs for "gitStatusParser()" function. --- lib/utils/gitstatusparser.js | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/utils/gitstatusparser.js b/lib/utils/gitstatusparser.js index 79c655d..cedec8a 100644 --- a/lib/utils/gitstatusparser.js +++ b/lib/utils/gitstatusparser.js @@ -27,19 +27,30 @@ const UNTRACKED_SYMBOL = '??'; /** * @param {String} response An output returned by `git status -sb` command. * @returns {Object} data + * @returns {Boolean} data.anythingToCommit Returns true if any changed file could be committed using command `git commit -a`. + * @returns {String} data.branch Current branch. + * @returns {Number|null} data.behind Number of commits that branch is behind the remote upstream. + * @returns {Number|null} data.ahead Number of commits that branch is ahead the remote upstream. + * @returns {Array.} data.added List of files created files (untracked files are tracked now). + * @returns {Array.} data.modified List of tracked files that have changed. + * @returns {Array.} data.deleted List of tracked files that have deleted. + * @returns {Array.} data.renamed List of tracked files that have moved (or renamed). + * @returns {Array.} data.unmerged List of tracked files that contain (unresolved) conflicts. + * @returns {Array.} data.untracked List of untracked files which won't be committed using command `git commit -a`. + * @returns {Array.} data.staged List of files that their changes are ready to commit. */ module.exports = function gitStatusParser( response ) { const responseAsArray = response.split( '\n' ); const branchData = responseAsArray.shift(); const branch = branchData.split( '...' )[ 0 ].match( /## (.*)$/ )[ 1 ]; - const added = findFiles( [ ADDED_STAGED_SYMBOL ] ); - const modified = findFiles( [ MODIFIED_NOT_STAGED_SYMBOL, MODIFIED_STAGED_AND_NOT_STAGED_SYMBOL, DELETE_NOT_STAGED_SYMBOL ] ); - const deleted = findFiles( [ DELETE_STAGED_SYMBOL, DELETE_NOT_STAGED_SYMBOL ] ); - const renamed = findFiles( [ RENAMED_STAGED_SYMBOL ] ); - const unmerged = findFiles( UNMERGED_SYMBOLS ); - const untracked = findFiles( [ UNTRACKED_SYMBOL ] ); - const staged = findFiles( [ + const added = filterFiles( [ ADDED_STAGED_SYMBOL ] ); + const modified = filterFiles( [ MODIFIED_NOT_STAGED_SYMBOL, MODIFIED_STAGED_AND_NOT_STAGED_SYMBOL, DELETE_NOT_STAGED_SYMBOL ] ); + const deleted = filterFiles( [ DELETE_STAGED_SYMBOL, DELETE_NOT_STAGED_SYMBOL ] ); + const renamed = filterFiles( [ RENAMED_STAGED_SYMBOL ] ); + const unmerged = filterFiles( UNMERGED_SYMBOLS ); + const untracked = filterFiles( [ UNTRACKED_SYMBOL ] ); + const staged = filterFiles( [ ADDED_STAGED_SYMBOL, DELETE_STAGED_SYMBOL, MODIFIED_STAGED_SYMBOL, @@ -75,7 +86,7 @@ module.exports = function gitStatusParser( response ) { staged }; - function findFiles( prefixes ) { + function filterFiles( prefixes ) { return responseAsArray .filter( line => prefixes.some( prefix => prefix === line.substring( 0, 2 ) ) ) .map( line => line.slice( 2 ).trim() ); From b092eb885ebccba653d2a6b28886daead6b8e6fb Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Fri, 10 Aug 2018 14:48:09 +0200 Subject: [PATCH 03/38] Add "commit" command. --- lib/commands/commit.js | 111 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 lib/commands/commit.js diff --git a/lib/commands/commit.js b/lib/commands/commit.js new file mode 100644 index 0000000..b88b5a6 --- /dev/null +++ b/lib/commands/commit.js @@ -0,0 +1,111 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const chalk = require( 'chalk' ); +const buildOptions = require( 'minimist-options' ); +const minimist = require( 'minimist' ); +const gitStatusParser = require( '../utils/gitstatusparser' ); + +module.exports = { + get helpMessage() { + const u = chalk.underline; + const g = chalk.gray; + const m = chalk.magenta; + const i = chalk.italic; + + return ` + ${ u( 'Description:' ) } + Makes a commit in every repository that contains tracked files that have changed. + This command is a shorthand for: ${ i( 'mgit exec \'git commit -a\'' ) }. + + ${ u( 'Options:' ) } + ${ m( '--message' ) } (-m) Required. A message for the commit. It can be specified more then once, e.g.: + ${ g( '> mgit commit -- --message "Title of the commit." --message "Additional description."' ) } + ${ m( '--no-verify' ) } (-n) Whether to skip pre-commit and commit-msg hooks. + ${ g( '> mgit commit -- -m "Title of the commit." -n' ) } + `; + }, + + /** + * @param {Array.} args Arguments and options that a user provided calling the command. + */ + beforeExecute( args ) { + const options = this._parseArguments( args ); + + if ( !options.message.length ) { + throw new Error( 'Missing --message (-m) option. Call "mgit commit -h" in order to read more.' ); + } + }, + + /** + * @param {Object} data + * @param {Array.} data.arguments The rest of arguments provided by the user. These options will modify the `git diff` command. + * @returns {Promise} + */ + execute( data ) { + const log = require( '../utils/log' )(); + const execCommand = require( './exec' ); + + return execCommand.execute( getExecData( 'git status --branch --porcelain' ) ) + .then( execResponse => { + const status = gitStatusParser( execResponse.logs.info[ 0 ] ); + + if ( !status.anythingToCommit ) { + log.info( 'Nothing to commit.' ); + + return { + logs: log.all() + }; + } + + const options = this._parseArguments( data.arguments ); + const cliCommand = buildCliCommand( options ); + + return execCommand.execute( getExecData( cliCommand ) ); + } ); + + function getExecData( command ) { + return Object.assign( {}, data, { + arguments: [ command ] + } ); + } + + function buildCliCommand( options ) { + let command = 'git commit'; + + if ( options.message ) { + command += ' ' + options.message.map( message => `-m "${ message }"` ).join( ' ' ); + } + + if ( options[ 'no-verify' ] ) { + command += ' -n'; + } + + return command; + } + }, + + _parseArguments( argv ) { + const options = minimist( argv, buildOptions( { + message: { + type: 'string', + alias: 'm', + }, + 'no-verify': { + type: 'boolean', + alias: 'n', + default: false + } + } ) ); + + if ( !Array.isArray( options.message ) ) { + options.message = [ options.message ].filter( Boolean ); + } + + return options; + } +}; From 3390740abdf59391ac476ed2793ebaf0521e8406 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Fri, 10 Aug 2018 15:16:00 +0200 Subject: [PATCH 04/38] Add "getCommandInstance()" which returns an instance of command which is ready to execute. --- lib/utils/getcommandinstance.js | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 lib/utils/getcommandinstance.js diff --git a/lib/utils/getcommandinstance.js b/lib/utils/getcommandinstance.js new file mode 100644 index 0000000..7d00643 --- /dev/null +++ b/lib/utils/getcommandinstance.js @@ -0,0 +1,36 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const chalk = require( 'chalk' ); + +const COMMAND_ALIASES = { + ci: 'commit', + co: 'checkout', + st: 'status' +}; + +/** + * @param {String} response An output returned by `git status -sb` command. + */ +module.exports = function getCommandInstance( commandName ) { + try { + // Find full command name if used an alias or just use specified name. + const resolvedCommandName = ( COMMAND_ALIASES[ commandName ] || commandName ).replace( /-/g, '' ); + + const commandPath = require.resolve( '../commands/' + resolvedCommandName ); + const commandInstance = require( commandPath ); + + commandInstance.path = commandPath; + + return commandInstance; + } catch ( err ) { + const message = `Command "${ commandName }" does not exist. Type: "mgit --help" in order to see available commands.`; + + console.error( chalk.red( message ) ); + process.exitCode = 1; + } +}; From 54c086f6c8c5c51ff5388778b55b9075bf30043d Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Fri, 10 Aug 2018 15:17:14 +0200 Subject: [PATCH 05/38] Improve help screen. Every git command can display its help scren if command was called with -h flag. --- index.js | 118 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 48 deletions(-) diff --git a/index.js b/index.js index fa3cf7f..49c4b7b 100755 --- a/index.js +++ b/index.js @@ -7,18 +7,24 @@ 'use strict'; +const chalk = require( 'chalk' ); const meow = require( 'meow' ); const mgit = require( './lib/index' ); +const getCommandInstance = require( './lib/utils/getcommandinstance' ); const meowOptions = { + autoHelp: false, flags: { version: { alias: 'v' + }, + help: { + alias: 'h' } } }; -const cli = meow( ` +const mgitLogo = ` _ _ (_) | _ __ ___ __ _ _| |_ @@ -27,40 +33,44 @@ const cli = meow( ` |_| |_| |_|\\__, |_|\\__| __/ | |___/ - - Usage: - $ mgit [command] - - Commands: - bootstrap Installs packages (i.e. clone dependent repositories). - exec Executes shell command in each package. - update Updates packages to the latest versions (i.e. pull changes). - save-hashes Saves hashes of packages in mgit.json. It allows to easily fix project to a specific state. - status Prints a table which contains useful information about the status of repositories. - diff Prints changes from packages where something has changed. - checkout Changes branches in repositories according to the configuration file. - - Options: - --recursive Whether to install dependencies recursively. - Needs to be used together with --repository-include. Only packages - matching these patterns will be cloned recursively. - - Default: false. - - --packages Directory to which all repositories will be cloned. - - Default: '/packages/' - - --resolver-path Path to a custom repository resolver function. - - Default: '@mgit2/lib/default-resolver.js'. - - --resolver-url-template Template used to generate repository URL out of a +`; + +const u = chalk.underline; +const c = chalk.cyan; +const g = chalk.gray; +const m = chalk.magenta; +const y = chalk.yellow; + +const cli = meow( ` ${ mgitLogo } + ${ u( 'Usage:' ) } + $ mgit ${ c( 'command' ) } ${ y( '[--options]' ) } -- ${ m( '[--command-options]' ) } + + ${ u( 'Commands:' ) } + ${ c( 'bootstrap' ) } Installs packages (i.e. clone dependent repositories). + ${ c( 'exec' ) } Executes shell command in each package. + ${ c( 'update' ) } Updates packages to the latest versions (i.e. pull changes). + ${ c( 'save-hashes' ) } Saves hashes of packages in mgit.json. It allows to easily fix project to a specific state. + ${ c( 'status' ) } Prints a table which contains useful information about the status of repositories. + ${ c( 'diff' ) } Prints changes from packages where something has changed. + ${ c( 'checkout' ) } Changes branches in repositories according to the configuration file. + ${ c( 'commit' ) } Commits all changes. A shorthand for "mgit exec 'git commit -a'" + + ${ u( 'Options:' ) } + ${ y( '--recursive' ) } Whether to install dependencies recursively. Only packages matching these + patterns will be cloned recursively. + ${ g( 'Default: false' ) } + + ${ y( '--packages' ) } Directory to which all repositories will be cloned. + ${ g( 'Default: \'/packages/\'' ) } + + ${ y( '--resolver-path' ) } Path to a custom repository resolver function. + ${ g( 'Default: \'@mgit2/lib/default-resolver.js\'' ) } + + ${ y( '--resolver-url-template' ) } Template used to generate repository URL out of a simplified 'organization/repository' format of the dependencies option. + ${ g( 'Default: \'git@github.com:${ path }.git\'.' ) } - Default: 'git@github.com:\${ path }.git'. - - --resolver-directory-name Defines how the target directory (where the repository will be cloned) + ${ y( '--resolver-directory-name' ) } Defines how the target directory (where the repository will be cloned) is resolved. Supported options are: 'git' (default), 'npm'. * If 'git' was specified, then the directory name will be extracted from @@ -69,30 +79,42 @@ const cli = meow( ` This option can be useful when scoped npm packages are used and one wants to decide whether the repository will be cloned to packages/@scope/pkgname' or 'packages/pkgname'. + ${ g( 'Default: \'git\'' ) } - Default: 'git' - - --resolver-default-branch The branch name to use if not specified in mgit.json dependencies. + ${ y( '--resolver-default-branch' ) } The branch name to use if not specified in mgit.json dependencies. + ${ g( 'Default: master' ) } - Default: 'master' + ${ y( '--ignore' ) } Ignores packages which names match the given glob pattern. E.g.: + ${ g( '> mgit exec --ignore="foo*" "git status"' ) } - --ignore Ignores packages which names match the given glob pattern. + Will ignore all packages which names start from "foo". + ${ g( 'Default: null' ) } - For example: + ${ y( '--scope' ) } Restricts the command to packages which names match the given glob pattern. + ${ g( 'Default: null' ) } +`, meowOptions ); - > mgit exec --ignore="foo*" "git st" +const commandName = cli.input[ 0 ]; - Will ignore all packages which names start from "foo". +// If a user wants to see "help" screen. +if ( !commandName || cli.flags.help ) { + // Checks whether specified a command. If not, displays default help screen. + // Buf if the command is available, displays the command's help. + if ( !commandName ) { + cli.showHelp( 0 ); + } else { + const commandInstance = getCommandInstance( commandName ); - Default: null + if ( !commandInstance ) { + process.errorCode = -1; - --scope Restricts the command to packages which names match the given glob pattern. - - Default: null -`, meowOptions ); + return; + } -if ( cli.input.length === 0 ) { - cli.showHelp(); + console.log( mgitLogo ); + console.log( ` ${ u( 'Command:' ) } ${ c( commandName ) } `); + console.log( commandInstance.helpMessage ); + } } else { mgit( cli.input, cli.flags ); } From 4f840ecbc7555fa254a57c7febee99035bd6d311 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Fri, 10 Aug 2018 15:17:44 +0200 Subject: [PATCH 06/38] Mgit main file uses new util for loading command. --- lib/index.js | 46 ++++++++++++++++++---------------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/lib/index.js b/lib/index.js index cb87d76..7a6ad22 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,42 +5,33 @@ 'use strict'; -const path = require( 'upath' ); +const chalk = require( 'chalk' ); const createForkPool = require( './utils/createforkpool' ); const logDisplay = require( './utils/displaylog' ); const getOptions = require( './utils/getoptions' ); const getPackageNames = require( './utils/getpackagenames' ); -const chalk = require( 'chalk' ); +const getCommandInstance = require( './utils/getcommandinstance' ); +const getCwd = require( './utils/getcwd' ); -const aliases = { - st: 'status', - co: 'checkout' -}; +const CHILD_PROCESS_PATH = require.resolve( './utils/child-process' ); /** * @param {Array.} args Arguments that the user provided. - * @param {Options} options The options object. It will be extended with the default options. + * @param {Options} _options The options object. It will be extended with the default options. */ -module.exports = function( args, options ) { - const startTime = process.hrtime(); - const forkPool = createForkPool( path.join( __dirname, 'utils', 'child-process.js' ) ); - - options = getOptions( options, require( './utils/getcwd' )() ); - - const repositoryResolver = require( options.resolverPath ); +module.exports = function( args, _options ) { + const command = getCommandInstance( args[ 0 ] ); - // If used an alias in order to call the command - replace the alias with a full name. - if ( args[ 0 ] in aliases ) { - args[ 0 ] = aliases[ args[ 0 ] ]; + if ( !command ) { + return; } - // Remove all dashes from command name. - args[ 0 ] = args[ 0 ].replace( /-/g, '' ); - - const commandPath = path.join( __dirname, 'commands', args[ 0 ] ); - const command = require( commandPath ); + const startTime = process.hrtime(); + const options = getOptions( _options, getCwd() ); + const repositoryResolver = require( options.resolverPath ); + const forkPool = createForkPool( CHILD_PROCESS_PATH ); - if ( typeof command.beforeExecute == 'function' ) { + if ( command.beforeExecute ) { command.beforeExecute( args ); } @@ -49,15 +40,14 @@ module.exports = function( args, options ) { const packageNames = getPackageNames( options ); let allPackagesNumber = packageNames.length; + let donePackagesNumber = 0; if ( allPackagesNumber === 0 ) { - console.log( chalk.red( 'No packages found that match the given criteria.' ) ); + console.log( chalk.yellow( 'No packages found that match to specified criteria.' ) ); return onDone(); } - let donePackagesNumber = 0; - for ( const item of packageNames ) { enqueue( item ); } @@ -70,7 +60,7 @@ module.exports = function( args, options ) { processedPackages.add( packageName ); const data = { - command: commandPath, + command: command.path, arguments: args.slice( 1 ), options, packageName, @@ -118,7 +108,7 @@ module.exports = function( args, options ) { function onDone() { return forkPool.killAll() .then( () => { - if ( typeof command.afterExecute === 'function' ) { + if ( command.afterExecute ) { command.afterExecute( processedPackages, commandResponses ); } From 6b66c64feff4f05b8189267a03378d23ea68c687 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Fri, 10 Aug 2018 15:18:31 +0200 Subject: [PATCH 07/38] Improve docs in utils. --- lib/utils/displaylog.js | 2 +- lib/utils/getcwd.js | 2 +- lib/utils/getoptions.js | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/utils/displaylog.js b/lib/utils/displaylog.js index b050d85..f38c17f 100644 --- a/lib/utils/displaylog.js +++ b/lib/utils/displaylog.js @@ -17,7 +17,7 @@ const chalk = require( 'chalk' ); * @param {Object} options * @param {Number} options.current Number of packages that have been processed. * @param {Number} options.all Number of all packages that will be processed. - * @param {Number} options.command Name of executed command. + * @param {String} options.command Name of executed command. */ module.exports = function displayLog( packageName, logs, options ) { let infoLogs = logs.info.filter( l => l.length ).join( '\n' ).trim(); diff --git a/lib/utils/getcwd.js b/lib/utils/getcwd.js index 0e5fb6b..d65ad36 100644 --- a/lib/utils/getcwd.js +++ b/lib/utils/getcwd.js @@ -9,7 +9,7 @@ const fs = require( 'fs' ); const path = require( 'upath' ); /** - * Returns an absolute path to the directory with configuration file. + * Returns an absolute path to the directory that contains a configuration file. * * It scans directory tree up for `mgit.json` file. If the file won't be found, * an exception should be thrown. diff --git a/lib/utils/getoptions.js b/lib/utils/getoptions.js index 4b379d8..b891c52 100644 --- a/lib/utils/getoptions.js +++ b/lib/utils/getoptions.js @@ -9,7 +9,8 @@ const fs = require( 'fs' ); const path = require( 'upath' ); /** - * @param {Object} Call options. + * @param {Object} callOptions Call options. + * @param {String} cwd An absolute path to the directory where `mgit.json` is available. * @returns {Options} The options object. */ module.exports = function cwdResolver( callOptions, cwd ) { @@ -42,6 +43,8 @@ module.exports = function cwdResolver( callOptions, cwd ) { /** * @typedef Options * + * @property {String} cwd An absolute path to the directory which contains `mgit.json` file. + * * @property {Boolean} [recursive=false] Whether to install dependencies recursively. * Needs to be used together with --repository-include. Only packages * matching these patterns will be cloned recursively. From 03eacedda09aec086576fe148cbde7c41be41bbe Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Fri, 10 Aug 2018 15:18:45 +0200 Subject: [PATCH 08/38] Upgrade dependencies. --- package.json | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index bbe3d16..2e4449e 100644 --- a/package.json +++ b/package.json @@ -17,21 +17,23 @@ "generic-pool": "^3.4.2", "meow": "^5.0.0", "minimatch": "^3.0.4", + "minimist": "^1.2.0", + "minimist-options": "^3.0.2", "shelljs": "^0.8.2", - "upath": "^1.0.5" + "upath": "^1.1.0" }, "devDependencies": { - "@ckeditor/ckeditor5-dev-env": "^9.0.1", + "@ckeditor/ckeditor5-dev-env": "^11.1.1", "@ckeditor/ckeditor5-dev-lint": "^3.1.4", "chai": "^4.1.2", - "eslint": "^4.19.1", + "eslint": "^5.3.0", "eslint-config-ckeditor5": "^1.0.8", "husky": "^0.14.3", "istanbul": "^0.4.5", - "lint-staged": "^7.1.0", - "mocha": "^5.1.1", + "lint-staged": "^7.2.0", + "mocha": "^5.2.0", "mockery": "^2.1.0", - "sinon": "^5.0.7" + "sinon": "^6.1.4" }, "repository": { "type": "git", From 2adc38e28febdc4b902559f74f8f8e36113d8b8d Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Fri, 10 Aug 2018 15:19:04 +0200 Subject: [PATCH 09/38] Improve README. --- README.md | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 922e1a7..534b08c 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ First, create a configuration file `mgit.json`: And run `mgit bootstrap` to clone all the repositories. By default, they will be cloned to `/packages/` directory: -``` +```bash packages/ ckeditor5-engine/ mgit/ @@ -52,23 +52,18 @@ packages/ CLI options: ``` ---recursive Whether to install dependencies recursively. - Needs to be used together with --repository-include. Only packages - matching these patterns will be cloned recursively. - +--recursive Whether to install dependencies recursively. Only packages matching + these patterns will be cloned recursively. Default: false. --packages Directory to which all repositories will be cloned. - Default: '/packages/' --resolver-path Path to a custom repository resolver function. - Default: '@mgit2/lib/default-resolver.js'. --resolver-url-template Template used to generate repository URL out of a simplified 'organization/repository' format of the dependencies option. - Default: 'git@github.com:${ path }.git'. --resolver-directory-name Defines how the target directory (where the repository will be cloned) @@ -80,31 +75,24 @@ CLI options: This option can be useful when scoped npm packages are used and one wants to decide whether the repository will be cloned to packages/@scope/pkgname' or 'packages/pkgname'. - Default: 'git' --resolver-default-branch The branch name to use if not specified in mgit.json dependencies. - Default: 'master' ---ignore Ignores packages which names match the given glob pattern. - - For example: - - > mgit exec --ignore="foo*" "git st" +--ignore Ignores packages which names match the given glob pattern. E.g.: + > mgit exec --ignore="foo*" "git st" Will ignore all packages which names start from "foo". - Default: null --scope Restricts the command to packages which names match the given glob pattern. - Default: null ``` All these options can also be specified in `mgit.json` (options passed through CLI takes precedence): -```js +```json { "packages": "/workspace/modules", "resolverDirectoryName": "npm", @@ -169,7 +157,7 @@ const parseRepositoryUrl = require( 'mgit2/lib/utils/parserepositoryurl' ); module.exports = function resolver( packageName, options ) { // If package name starts with '@ckeditor/ckeditor5-*' clone it from 'ckeditor/ckeditor5-*'. if ( packageName.startsWith( '@ckeditor/ckeditor5-' ) ) { - repositoryUrl = packageName.slice( 1 ); + const repositoryUrl = packageName.slice( 1 ); return parseRepositoryUrl( repositoryUrl ); } @@ -185,18 +173,24 @@ You can also check the [default resolver](https://github.com/cksource/mgit2/blob CI servers, such as Travis, can't clone repositories using Git URLs (such as `git@github.com:cksource/mgit.git`). By default, mgit uses Git URLs because it assumes that you'll want to commit to these repositories (and don't want to be asked for a password every time). -If you need to run mgit on a CI server, then configure it to use HTTP URLs: +If you need to run mgit on a CI server, then configure it to use HTTPS URLs: ```bash mgit --resolver-url-template="https://github.com/\${ path }.git" ``` -You can also use full HTTPs URLs to configure `dependencies` in your `mgit.json`. +You can also use full HTTPS URLs to configure `dependencies` in your `mgit.json`. ## Commands +```bash +$ mgit [command] ``` -mgit [command] + +For displaying help screen for commands, type: + +```bash +$ mgit [command] --help ``` ### bootstrap From 48a7d3f3acf60e529b44a14a738fafb01e61dd66 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Sun, 12 Aug 2018 15:06:20 +0200 Subject: [PATCH 10/38] Remove "--recursive" option from mgit. It is a option for "bootstrap" and "update" commands. --- index.js | 4 ---- lib/utils/getoptions.js | 7 +------ 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/index.js b/index.js index 49c4b7b..a9c4474 100755 --- a/index.js +++ b/index.js @@ -56,10 +56,6 @@ const cli = meow( ` ${ mgitLogo } ${ c( 'commit' ) } Commits all changes. A shorthand for "mgit exec 'git commit -a'" ${ u( 'Options:' ) } - ${ y( '--recursive' ) } Whether to install dependencies recursively. Only packages matching these - patterns will be cloned recursively. - ${ g( 'Default: false' ) } - ${ y( '--packages' ) } Directory to which all repositories will be cloned. ${ g( 'Default: \'/packages/\'' ) } diff --git a/lib/utils/getoptions.js b/lib/utils/getoptions.js index b891c52..375760e 100644 --- a/lib/utils/getoptions.js +++ b/lib/utils/getoptions.js @@ -20,7 +20,6 @@ module.exports = function cwdResolver( callOptions, cwd ) { let options = { cwd, packages: 'packages', - recursive: false, resolverPath: path.resolve( __dirname, '../default-resolver.js' ), resolverUrlTemplate: 'git@github.com:${ path }.git', resolverTargetDirectory: 'git', @@ -41,14 +40,10 @@ module.exports = function cwdResolver( callOptions, cwd ) { }; /** - * @typedef Options + * @typedef {Object} Options * * @property {String} cwd An absolute path to the directory which contains `mgit.json` file. * - * @property {Boolean} [recursive=false] Whether to install dependencies recursively. - * Needs to be used together with --repository-include. Only packages - * matching these patterns will be cloned recursively. - * * @property {String} [packages='/packages/'] Directory to which all repositories will be cloned. * * @property {String} [resolverPath='mgit2/lib/default-resolver.js'] Path to a custom repository resolver function. From ad9bd3cae579e03d9ed85ecf922756a284663637 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Sun, 12 Aug 2018 15:06:57 +0200 Subject: [PATCH 11/38] Improve documentation, create Command and CommandData types. --- index.js | 2 +- lib/utils/getcommandinstance.js | 53 +++++++++++++++++++++++++++++++-- lib/utils/log.js | 11 +++++++ 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index a9c4474..9c0d222 100755 --- a/index.js +++ b/index.js @@ -108,7 +108,7 @@ if ( !commandName || cli.flags.help ) { } console.log( mgitLogo ); - console.log( ` ${ u( 'Command:' ) } ${ c( commandName ) } `); + console.log( ` ${ u( 'Command:' ) } ${ c( commandInstance.name || commandName ) } `); console.log( commandInstance.helpMessage ); } } else { diff --git a/lib/utils/getcommandinstance.js b/lib/utils/getcommandinstance.js index 7d00643..3cacb06 100644 --- a/lib/utils/getcommandinstance.js +++ b/lib/utils/getcommandinstance.js @@ -14,12 +14,13 @@ const COMMAND_ALIASES = { }; /** - * @param {String} response An output returned by `git status -sb` command. + * @param {String} commandName An alias or fully command name that should be used. + * @returns {Command|null} */ module.exports = function getCommandInstance( commandName ) { try { // Find full command name if used an alias or just use specified name. - const resolvedCommandName = ( COMMAND_ALIASES[ commandName ] || commandName ).replace( /-/g, '' ); + const resolvedCommandName = (COMMAND_ALIASES[ commandName ] || commandName).replace( /-/g, '' ); const commandPath = require.resolve( '../commands/' + resolvedCommandName ); const commandInstance = require( commandPath ); @@ -31,6 +32,52 @@ module.exports = function getCommandInstance( commandName ) { const message = `Command "${ commandName }" does not exist. Type: "mgit --help" in order to see available commands.`; console.error( chalk.red( message ) ); - process.exitCode = 1; } + + process.exitCode = 1; + + return null; }; + +/** + * @typedef {Object} Command + * + * @property {String} path An absolute path to the file that keeps the command. + * + * @property {String} helpMessage A message that explains how to use specified command. + * + * @property {Function} execute A function that is called on every repository that match to specified criteria. It receives an object + * as an argument that contains following properties + * + * @property {Boolean} [skipCounter=false] A flag that allows hiding the progress bar (number of package and number of all + * packages to process) on the screen. + * + * @property {String} [name] A name of the command. It's useful if specified command has defined an alias. + * + * @property {Function} [beforeExecute] A function that is called by mgit automatically before executing the main command's method. + * This function is called once. It receives single argument (`CommandData`) that represents an input provided by a user. + * It must returns an instance of `Promise`. The promise must resolve an object that can contains following properties: + * - `logs` - an object that matches to `Logs` object definition. + * - `response` - the entire `response` object is added to a collection that will be passed as second argument to `#afterExecute` + * function. + * - `packages` - an array of packages that mgit should process as well. + * + * @property {Function} [afterExecute] A function that is called by mgit automatically after executing the main command's method. + * This function is called once. It receives two parameters: + * - a collection (`Set`) that contains all processed packages by mgit. + * - a collection (`Set`) that contains responses returned by `#execute` function. + */ + +/** + * @typedef {Object} CommandData + * + * @property {String} packageName A name of package. + * + * @propery {Options} mgitOptions Options resolved by mgit. + * + * @property {String} commandPath An absolute path to the file that keeps the command. + * + * @property {Array.} arguments Arguments provided by the user via CLI. + * + * @property {Repository|null} repository An object that keeps data about repository for specified package. + */ diff --git a/lib/utils/log.js b/lib/utils/log.js index 2c98df0..d77e9d5 100644 --- a/lib/utils/log.js +++ b/lib/utils/log.js @@ -44,6 +44,9 @@ module.exports = function log() { responseLogs.error.forEach( err => logger.error( err ) ); }, + /** + * @returns {Logs} + */ all() { return { error: logs.get( 'error' ), @@ -54,3 +57,11 @@ module.exports = function log() { return logger; }; + +/** + * @typedef {Object} Logs + * + * @propery {Array.} error An error messages. + * + * @propery {Array.} info An information messages. + */ From a69b19fadd19aa139aa9c62b5adc954ff44f1937 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Sun, 12 Aug 2018 15:08:41 +0200 Subject: [PATCH 12/38] Minor changes in naming the variables. --- lib/commands/bootstrap.js | 27 ++++++++++++++++++++------- lib/commands/checkout.js | 4 +--- lib/commands/commit.js | 8 ++++++-- lib/commands/diff.js | 3 +-- lib/commands/exec.js | 12 ++++-------- lib/commands/savehashes.js | 3 +-- lib/commands/status.js | 10 +++------- lib/commands/update.js | 10 ++++------ lib/index.js | 34 +++++++++++++++++++++------------- lib/utils/child-process.js | 7 ++----- lib/utils/displaylog.js | 17 ++++------------- 11 files changed, 67 insertions(+), 68 deletions(-) diff --git a/lib/commands/bootstrap.js b/lib/commands/bootstrap.js index c0b7093..fa9dc54 100644 --- a/lib/commands/bootstrap.js +++ b/lib/commands/bootstrap.js @@ -7,21 +7,20 @@ const fs = require( 'fs' ); const path = require( 'upath' ); +const buildOptions = require( 'minimist-options' ); +const minimist = require( 'minimist' ); const chalk = require( 'chalk' ); const exec = require( '../utils/exec' ); module.exports = { /** - * @param {Object} data - * @param {String} data.packageName Name of current package to process. - * @param {Options} data.options The options object. - * @param {Repository} data.repository + * @param {CommandData} data * @returns {Promise} */ execute( data ) { const log = require( '../utils/log' )(); - - const destinationPath = path.join( data.options.packages, data.repository.directory ); + const options = this._parseArguments( data.arguments ); + const destinationPath = path.join( data.mgitOptions.packages, data.repository.directory ); let promise; @@ -48,7 +47,7 @@ module.exports = { logs: log.all() }; - if ( data.options.recursive ) { + if ( options.recursive ) { const packageJson = require( path.join( destinationPath, 'package.json' ) ); const packages = []; @@ -77,5 +76,19 @@ module.exports = { */ afterExecute( processedPackages ) { console.log( chalk.cyan( `${ processedPackages.size } packages have been processed.` ) ); + }, + + /** + * @private + * @param {Array.} argv List of arguments provided by the user via CLI. + * @returns {Object} + */ + _parseArguments( argv ) { + return minimist( argv, buildOptions( { + recursive: { + type: 'boolean', + alias: 'r', + } + } ) ); } }; diff --git a/lib/commands/checkout.js b/lib/commands/checkout.js index aec92ed..25ddb47 100644 --- a/lib/commands/checkout.js +++ b/lib/commands/checkout.js @@ -7,9 +7,7 @@ module.exports = { /** - * @param {Object} data - * @param {Object} data.repository - * @param {String} data.repository.branch Name of branch (or commit hash) saved in `mgit.json` file. + * @param {CommandData} data * @returns {Promise} */ execute( data ) { diff --git a/lib/commands/commit.js b/lib/commands/commit.js index b88b5a6..7505988 100644 --- a/lib/commands/commit.js +++ b/lib/commands/commit.js @@ -42,8 +42,7 @@ module.exports = { }, /** - * @param {Object} data - * @param {Array.} data.arguments The rest of arguments provided by the user. These options will modify the `git diff` command. + * @param {CommandData} data * @returns {Promise} */ execute( data ) { @@ -89,6 +88,11 @@ module.exports = { } }, + /** + * @private + * @param {Array.} argv List of arguments provided by the user via CLI. + * @returns {Object} + */ _parseArguments( argv ) { const options = minimist( argv, buildOptions( { message: { diff --git a/lib/commands/diff.js b/lib/commands/diff.js index 9c1ff3e..e3a5ba4 100644 --- a/lib/commands/diff.js +++ b/lib/commands/diff.js @@ -9,8 +9,7 @@ const chalk = require( 'chalk' ); module.exports = { /** - * @param {Object} data - * @param {Array.} data.arguments The rest of arguments provided by the user. These options will modify the `git diff` command. + * @param {CommandData} data * @returns {Promise} */ execute( data ) { diff --git a/lib/commands/exec.js b/lib/commands/exec.js index 93bc7f9..5e9a80b 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -20,18 +20,14 @@ module.exports = { }, /** - * @param {Object} data - * @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} data.repository + * @param {CommandData} data * @returns {Promise} */ execute( data ) { const log = require( '../utils/log' )(); return new Promise( ( resolve, reject ) => { - const newCwd = path.join( data.options.packages, data.repository.directory ); + const newCwd = path.join( data.mgitOptions.packages, data.repository.directory ); // Package does not exist. if ( !fs.existsSync( newCwd ) ) { @@ -44,14 +40,14 @@ module.exports = { exec( data.arguments[ 0 ] ) .then( stdout => { - process.chdir( data.options.cwd ); + process.chdir( data.mgitOptions.cwd ); log.info( stdout ); resolve( { logs: log.all() } ); } ) .catch( error => { - process.chdir( data.options.cwd ); + process.chdir( data.mgitOptions.cwd ); log.error( error ); diff --git a/lib/commands/savehashes.js b/lib/commands/savehashes.js index af375c0..2dcb4bc 100644 --- a/lib/commands/savehashes.js +++ b/lib/commands/savehashes.js @@ -10,8 +10,7 @@ const updateJsonFile = require( '../utils/updatejsonfile' ); module.exports = { /** - * @param {Object} data - * @param {String} data.packageName Name of current package to process. + * @param {CommandData} data * @returns {Promise} */ execute( data ) { diff --git a/lib/commands/status.js b/lib/commands/status.js index 97f843c..990bb12 100644 --- a/lib/commands/status.js +++ b/lib/commands/status.js @@ -15,11 +15,7 @@ module.exports = { }, /** - * @param {Object} data - * @param {Options} data.options Mgit options. - * @param {String} data.packageName Name of current package to process. - * @param {Object} data.repository - * @param {String} data.repository.branch Name of branch (or commit hash) saved in `mgit.json` file. + * @param {CommandData} data * @returns {Promise} */ execute( data ) { @@ -34,8 +30,8 @@ module.exports = { .then( ( [ hashResponse, statusResponse ] ) => { let packageName = data.packageName; - if ( data.options.packagesPrefix ) { - packageName = packageName.replace( data.options.packagesPrefix, '' ); + if ( data.mgitOptions.packagesPrefix ) { + packageName = packageName.replace( data.mgitOptions.packagesPrefix, '' ); } const commandResponse = { diff --git a/lib/commands/update.js b/lib/commands/update.js index 1f660be..2a723c7 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -11,10 +11,7 @@ const chalk = require( 'chalk' ); module.exports = { /** - * @param {Object} data - * @param {String} data.packageName Name of current package to process. - * @param {Options} data.options The options object. - * @param {Repository} data.repository + * @param {CommandData} data * @returns {Promise} */ execute( data ) { @@ -22,14 +19,15 @@ module.exports = { const bootstrapCommand = require( './bootstrap' ); const execCommand = require( './exec' ); - const destinationPath = path.join( data.options.packages, data.repository.directory ); + const destinationPath = path.join( data.mgitOptions.packages, data.repository.directory ); // Package is not cloned. if ( !fs.existsSync( destinationPath ) ) { log.info( `Package "${ data.packageName }" was not found. Cloning...` ); const bootstrapOptions = { - options: data.options, + arguments: data.arguments, + mgitOptions: data.mgitOptions, packageName: data.packageName, repository: data.repository }; diff --git a/lib/index.js b/lib/index.js index 7a6ad22..4db5b53 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,7 +7,7 @@ const chalk = require( 'chalk' ); const createForkPool = require( './utils/createforkpool' ); -const logDisplay = require( './utils/displaylog' ); +const displayLog = require( './utils/displaylog' ); const getOptions = require( './utils/getoptions' ); const getPackageNames = require( './utils/getpackagenames' ); const getCommandInstance = require( './utils/getcommandinstance' ); @@ -17,9 +17,9 @@ const CHILD_PROCESS_PATH = require.resolve( './utils/child-process' ); /** * @param {Array.} args Arguments that the user provided. - * @param {Options} _options The options object. It will be extended with the default options. + * @param {Options} options The options object. It will be extended with the default options. */ -module.exports = function( args, _options ) { +module.exports = function( args, options ) { const command = getCommandInstance( args[ 0 ] ); if ( !command ) { @@ -27,17 +27,24 @@ module.exports = function( args, _options ) { } const startTime = process.hrtime(); - const options = getOptions( _options, getCwd() ); - const repositoryResolver = require( options.resolverPath ); + const mgitOptions = getOptions( options, getCwd() ); + const repositoryResolver = require( mgitOptions.resolverPath ); const forkPool = createForkPool( CHILD_PROCESS_PATH ); if ( command.beforeExecute ) { - command.beforeExecute( args ); + try { + command.beforeExecute( args ); + } catch ( error ) { + console.log( chalk.red( error.message ) ); + process.exit( 1 ); + + return; + } } const processedPackages = new Set(); const commandResponses = new Set(); - const packageNames = getPackageNames( options ); + const packageNames = getPackageNames( mgitOptions ); let allPackagesNumber = packageNames.length; let donePackagesNumber = 0; @@ -60,11 +67,11 @@ module.exports = function( args, _options ) { processedPackages.add( packageName ); const data = { - command: command.path, - arguments: args.slice( 1 ), - options, packageName, - repository: repositoryResolver( packageName, options ) + mgitOptions, + commandPath: command.path, + arguments: args.slice( 1 ), + repository: repositoryResolver( packageName, mgitOptions ) }; forkPool.enqueue( data ) @@ -87,10 +94,11 @@ module.exports = function( args, _options ) { } if ( returnedData.logs ) { - logDisplay( packageName, returnedData.logs, { + displayLog( packageName, returnedData.logs, { current: donePackagesNumber, all: allPackagesNumber, - command: args[ 0 ] + skipCounter: command.skipCounter, + colorizeOutput: command.colorizeOutput } ); } diff --git a/lib/utils/child-process.js b/lib/utils/child-process.js index ab357d8..7b6d5c6 100644 --- a/lib/utils/child-process.js +++ b/lib/utils/child-process.js @@ -8,10 +8,7 @@ process.on( 'message', onMessage ); /** - * @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 {CommandData} data */ function onMessage( data ) { const log = require( './log' )(); @@ -22,7 +19,7 @@ function onMessage( data ) { return process.send( { logs: log.all() } ); } - const command = require( data.command ); + const command = require( data.commandPath ); command.execute( data ) .then( returnedData => { diff --git a/lib/utils/displaylog.js b/lib/utils/displaylog.js index f38c17f..53c063b 100644 --- a/lib/utils/displaylog.js +++ b/lib/utils/displaylog.js @@ -11,13 +11,11 @@ const chalk = require( 'chalk' ); * Formats the logs and writes them to the console. * * @param {String} packageName - * @param {Object} logs - * @param {Array} logs.info - * @param {Array} logs.errors + * @param {Logs} logs * @param {Object} options * @param {Number} options.current Number of packages that have been processed. * @param {Number} options.all Number of all packages that will be processed. - * @param {String} options.command Name of executed command. + * @param {Boolean} [options.skipCounter=false] A flag that allows hiding the progress bar. */ module.exports = function displayLog( packageName, logs, options ) { let infoLogs = logs.info.filter( l => l.length ).join( '\n' ).trim(); @@ -29,12 +27,7 @@ module.exports = function displayLog( packageName, logs, options ) { console.log( chalk.inverse( getPackageHeader() ) ); if ( infoLogs ) { - // For the `diff` command we do not want to modify the output (do not change the colors). - if ( options.command !== 'diff' ) { - infoLogs = chalk.gray( infoLogs ); - } - - console.log( infoLogs ); + console.log( chalk.gray( infoLogs ) ); } if ( errorLogs ) { @@ -53,9 +46,7 @@ module.exports = function displayLog( packageName, logs, options ) { ' ' ]; - // For the `diff` command we do not want to show the progress (counter) - // because we will show the output only if the command returned the changes. - if ( options.command === 'diff' ) { + if ( options.skipCounter ) { headerParts.push( ' '.repeat( progressBar.length ) ); } else { headerParts.push( progressBar ); From 33f81367197305fdbad884d53faa56908751761a Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Sun, 12 Aug 2018 15:12:31 +0200 Subject: [PATCH 13/38] Add help screen for all commands. --- lib/commands/bootstrap.js | 22 ++++++++++++++++++++++ lib/commands/checkout.js | 15 +++++++++++++++ lib/commands/commit.js | 12 ++++++++---- lib/commands/diff.js | 27 +++++++++++++++++++++++++++ lib/commands/exec.js | 16 ++++++++++++++++ lib/commands/savehashes.js | 12 ++++++++++++ lib/commands/status.js | 19 +++++++++++++++++++ lib/commands/update.js | 28 ++++++++++++++++++++++++++++ 8 files changed, 147 insertions(+), 4 deletions(-) diff --git a/lib/commands/bootstrap.js b/lib/commands/bootstrap.js index fa9dc54..0bfe9d4 100644 --- a/lib/commands/bootstrap.js +++ b/lib/commands/bootstrap.js @@ -13,6 +13,28 @@ const chalk = require( 'chalk' ); const exec = require( '../utils/exec' ); module.exports = { + get helpMessage() { + const { + gray: g, + magenta: m, + underline: u + } = chalk; + + return ` + ${ u( 'Description:' ) } + Installs packages defined in a configuration file. Packages that are already cloned will be skipped. + + ${ u( 'Options:' ) } + ${ m( '--recursive' ) } (-r) Whether to install dependencies recursively. Only packages matching these + patterns will be cloned recursively. + ${ g( 'Default: false' ) } + `; + }, + + beforeExecute() { + console.log( chalk.blue( 'Cloning missing packages...' ) ); + }, + /** * @param {CommandData} data * @returns {Promise} diff --git a/lib/commands/checkout.js b/lib/commands/checkout.js index 25ddb47..4460eea 100644 --- a/lib/commands/checkout.js +++ b/lib/commands/checkout.js @@ -5,7 +5,22 @@ 'use strict'; +const chalk = require( 'chalk' ); + module.exports = { + name: 'checkout', + + get helpMessage() { + const { + underline: u + } = chalk; + + return ` + ${ u( 'Description:' ) } + Checks out the repository to specified branch or commit saved in "mgit.json" file. + `; + }, + /** * @param {CommandData} data * @returns {Promise} diff --git a/lib/commands/commit.js b/lib/commands/commit.js index 7505988..21d2a96 100644 --- a/lib/commands/commit.js +++ b/lib/commands/commit.js @@ -11,11 +11,15 @@ const minimist = require( 'minimist' ); const gitStatusParser = require( '../utils/gitstatusparser' ); module.exports = { + name: 'commit', + get helpMessage() { - const u = chalk.underline; - const g = chalk.gray; - const m = chalk.magenta; - const i = chalk.italic; + const { + italic: i, + gray: g, + magenta: m, + underline: u + } = chalk; return ` ${ u( 'Description:' ) } diff --git a/lib/commands/diff.js b/lib/commands/diff.js index e3a5ba4..eae313e 100644 --- a/lib/commands/diff.js +++ b/lib/commands/diff.js @@ -8,6 +8,33 @@ const chalk = require( 'chalk' ); module.exports = { + skipCounter: true, + + get helpMessage() { + const { + italic: i, + gray: g, + magenta: m, + underline: u + } = chalk; + + return ` + ${ u( 'Description:' ) } + Shows changes between commits, commit and working tree, etc. Works the same as "${ i( 'git diff' ) }" command. By default a flag + "${ m( '--color' ) }" is adding. You can cancel it using option "${ m( '--no-color' ) }". + + ${ u( 'Options:' ) } + All options accepted by "${ i( 'git diff' ) }" are supported by mgit. Everything specified after "--" is passed directly to the + "${ i( 'git diff' ) }" command. + + E.g.: "${ g( 'mgit diff -- origin/master..master' ) }" will execute "${ i( 'git diff --color origin/master..master' ) }" + `; + }, + + beforeExecute() { + console.log( chalk.blue( 'Collecting changes...' ) ); + }, + /** * @param {CommandData} data * @returns {Promise} diff --git a/lib/commands/exec.js b/lib/commands/exec.js index 5e9a80b..1e154fc 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -8,8 +8,24 @@ const fs = require( 'fs' ); const path = require( 'upath' ); const exec = require( '../utils/exec' ); +const chalk = require( 'chalk' ); module.exports = { + get helpMessage() { + const { + italic: i, + gray: g, + underline: u + } = chalk; + + return ` + ${ u( 'Description:' ) } + Requires a command that will be executed on all repositories. E.g. "${ g( 'mgit exec pwd' ) }" will execute "${ i( 'pwd' ) }" + command in every repository. Commands that contain spaces must be wrapped in quotation marks, + e.g.: "${ g( 'mgit exec \"git remote\"' ) }". + `; + }, + /** * @param {Array.} args Arguments that user provided calling the mgit. */ diff --git a/lib/commands/savehashes.js b/lib/commands/savehashes.js index 2dcb4bc..e1bb0cc 100644 --- a/lib/commands/savehashes.js +++ b/lib/commands/savehashes.js @@ -6,9 +6,21 @@ 'use strict'; const path = require( 'upath' ); +const chalk = require( 'chalk' ); const updateJsonFile = require( '../utils/updatejsonfile' ); module.exports = { + get helpMessage() { + const { + underline: u + } = chalk; + + return ` + ${ u( 'Description:' ) } + Saves hashes of commits in "mgit.json" file. It allows easily to restore the project to some point. + `; + }, + /** * @param {CommandData} data * @returns {Promise} diff --git a/lib/commands/status.js b/lib/commands/status.js index 990bb12..46d1a46 100644 --- a/lib/commands/status.js +++ b/lib/commands/status.js @@ -10,6 +10,25 @@ const Table = require( 'cli-table' ); const gitStatusParser = require( '../utils/gitstatusparser' ); module.exports = { + name: 'status', + + get helpMessage() { + const { + underline: u + } = chalk; + + return ` + ${ u( 'Description:' ) } + Prints a useful table that contains status of every repository. It displays: + + * current branch, + * whether current branch is equal to specified in "mgit.json" file, + * whether current branch is behind or ahead with the remote, + * current commit short hash, + * how many files is staged, modified and untracked. + `; + }, + beforeExecute() { console.log( chalk.blue( 'Collecting statuses...' ) ); }, diff --git a/lib/commands/update.js b/lib/commands/update.js index 2a723c7..94c7e1b 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -10,6 +10,34 @@ const path = require( 'upath' ); const chalk = require( 'chalk' ); module.exports = { + get helpMessage() { + const { + italic: i, + gray: g, + magenta: m, + underline: u + } = chalk; + + return ` + ${ u( 'Description:' ) } + Updates all packages. For packages that contain uncommitted changes, the update process is aborted. + If some package is missed, "${ i( 'bootstrap' ) }" is calling on the missing package. + + The update process executes following commands: + + * Checks whether repository can be updated. If the repository contains uncommitted changes, + the process is aborted. + * Fetches changes from the remote. + * Checks out on the branch or particular commit that is specified in "mgit.json" file. + * Pulls the changes if the repository is not detached at some commit. + + ${ u( 'Options:' ) } + ${ m( '--recursive' ) } Whether to install dependencies recursively. Only packages matching these + patterns will be cloned recursively. + ${ g( 'Default: false' ) } + `; + }, + /** * @param {CommandData} data * @returns {Promise} From 8e820fedf8cf442e3eedcc37309c0dc10996f4dc Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Aug 2018 09:51:43 +0200 Subject: [PATCH 14/38] Checkout command creates new branch (-b option) or read branch name from CLI (if specified). --- lib/commands/checkout.js | 88 +++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/lib/commands/checkout.js b/lib/commands/checkout.js index 4460eea..32ee6ea 100644 --- a/lib/commands/checkout.js +++ b/lib/commands/checkout.js @@ -6,18 +6,33 @@ 'use strict'; const chalk = require( 'chalk' ); +const buildOptions = require( 'minimist-options' ); +const minimist = require( 'minimist' ); +const execCommand = require( './exec' ); +const gitStatusParser = require( '../utils/gitstatusparser' ); module.exports = { name: 'checkout', get helpMessage() { const { + gray: g, + magenta: m, underline: u } = chalk; return ` ${ u( 'Description:' ) } Checks out the repository to specified branch or commit saved in "mgit.json" file. + + If specified a branch as an argument for "checkout" command, mgit will use the branch + instead of data saved in "mgit.json". E.g "${ g( 'mgit checkout master' ) }" will check out + all branches to "master". + + ${ u( 'Options:' ) } + ${ m( '--branch' ) } (-b) If specified, mgit will create given branch in all repositories + that contain changes that could be committed. + ${ g( '> mgit checkout -- --branch develop' ) } `; }, @@ -26,20 +41,73 @@ module.exports = { * @returns {Promise} */ execute( data ) { - const execCommand = require( './exec' ); - const checkoutCommand = `git checkout ${ data.repository.branch }`; + const options = this._parseArguments( data.arguments ); - return execCommand.execute( getExecData( checkoutCommand ) ) + // Used `--branch` option. + if ( options.branch ) { + return this._createAndCheckout( options.branch, data ); + } + + const branch = data.arguments[ 0 ] || data.repository.branch; + const checkoutCommand = `git checkout ${ branch }`; + + return execCommand.execute( this._getExecData( checkoutCommand, data ) ); + }, + + /** + * Executes "git checkout -b `branch`" command if a repository contains changes which could be committed. + * + * @private + * @param {String} branch + * @param {CommandData} data + * @returns {Promise} + */ + _createAndCheckout( branch, data ) { + const log = require( '../utils/log' )(); + + return execCommand.execute( this._getExecData( 'git status --branch --porcelain', data ) ) .then( execResponse => { - execResponse.logs.info = execResponse.logs.info[ 0 ].split( '\n' ).slice( -1 ); + const status = gitStatusParser( execResponse.logs.info[ 0 ] ); - return Promise.resolve( execResponse ); - } ); + if ( !status.anythingToCommit ) { + log.info( 'Repository does not contain changes to commit. New branch was not created.' ); + + return { + logs: log.all() + }; + } - function getExecData( command ) { - return Object.assign( {}, data, { - arguments: [ command ] + const checkoutCommand = `git checkout -b ${ branch }`; + + return execCommand.execute( this._getExecData( checkoutCommand, data ) ); } ); - } + }, + + /** + * Prepares new configuration object for "execute" command which is called inside this command. + * + * @private + * @param {String} command + * @param {CommandData} data + * @returns {CommandData} + */ + _getExecData( command, data ) { + return Object.assign( {}, data, { + arguments: [ command ] + } ); + }, + + /** + * @private + * @param {Array.} argv List of arguments provided by the user via CLI. + * @returns {Object} + */ + _parseArguments( argv ) { + return minimist( argv, buildOptions( { + branch: { + type: 'string', + alias: 'b', + } + } ) ); } }; From 9d7c7270a195420095f497137e4c135f67c77e29 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Aug 2018 09:52:06 +0200 Subject: [PATCH 15/38] Minor fixes in docs. --- lib/commands/commit.js | 2 +- lib/commands/update.js | 2 +- lib/default-resolver.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/commands/commit.js b/lib/commands/commit.js index 21d2a96..1a013b6 100644 --- a/lib/commands/commit.js +++ b/lib/commands/commit.js @@ -24,7 +24,7 @@ module.exports = { return ` ${ u( 'Description:' ) } Makes a commit in every repository that contains tracked files that have changed. - This command is a shorthand for: ${ i( 'mgit exec \'git commit -a\'' ) }. + This command is a shorthand for: "${ i( 'mgit exec \'git commit -a\'' ) }". ${ u( 'Options:' ) } ${ m( '--message' ) } (-m) Required. A message for the commit. It can be specified more then once, e.g.: diff --git a/lib/commands/update.js b/lib/commands/update.js index 94c7e1b..d999b32 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -21,7 +21,7 @@ module.exports = { return ` ${ u( 'Description:' ) } Updates all packages. For packages that contain uncommitted changes, the update process is aborted. - If some package is missed, "${ i( 'bootstrap' ) }" is calling on the missing package. + If some package is missed, "${ i( 'bootstrap' ) }" command is calling on the missing package. The update process executes following commands: diff --git a/lib/default-resolver.js b/lib/default-resolver.js index d901d85..e1bdd38 100644 --- a/lib/default-resolver.js +++ b/lib/default-resolver.js @@ -11,7 +11,7 @@ const parseRepositoryUrl = require( './utils/parserepositoryurl' ); * Resolves repository URL for a given package name. * * @param {String} packageName Package name. - * @param {Options} data.options The options object. + * @param {Options} options The options object. * @returns {Repository|null} */ module.exports = function resolver( packageName, options ) { From c20a482f7cb2ed1135a40b4562c364918f04b2cd Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Aug 2018 12:15:16 +0200 Subject: [PATCH 16/38] Add merge command. --- lib/commands/merge.js | 99 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 lib/commands/merge.js diff --git a/lib/commands/merge.js b/lib/commands/merge.js new file mode 100644 index 0000000..bbc94ad --- /dev/null +++ b/lib/commands/merge.js @@ -0,0 +1,99 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const chalk = require( 'chalk' ); +const buildOptions = require( 'minimist-options' ); +const minimist = require( 'minimist' ); + +module.exports = { + get helpMessage() { + const { + italic: i, + gray: g, + magenta: m, + underline: u + } = chalk; + + return ` + ${ u( 'Description:' ) } + Merges specified branch with the current which on the repository is checked out. + + Merge is executed only on repositories where specified branch exist. + + The merge commit will be made using following message: "${ i( 'Merge branch \'branch-name\'' ) }". + + ${ u( 'Options:' ) } + ${ m( '--message' ) } (-m) An additional description for merge commit. It will be + appended to the default message. E.g.: + ${ g( '> mgit merge develop -- -m "Some description about merged changes."' ) } + `; + }, + + beforeExecute( args ) { + if ( !args[ 0 ] ) { + throw new Error( 'Missing branch to merge. Use: mgit merge [branch].' ); + } + }, + + /** + * @param {CommandData} data + * @returns {Promise} + */ + execute( data ) { + const log = require( '../utils/log' )(); + const execCommand = require( './exec' ); + const branch = data.arguments[ 0 ]; + + return execCommand.execute( getExecData( `git branch --list ${ branch }` ) ) + .then( execResponse => { + const branchExists = Boolean( execResponse.logs.info[ 0 ] ); + + if ( !branchExists ) { + return { + logs: log.all() + }; + } + + const options = this._parseArguments( data.arguments ); + const commitTitle = `Merge branch '${ branch }'`; + + let mergeCommand = `git merge ${ branch } --no-ff -m "${ commitTitle }"`; + + if ( options.message.length ) { + mergeCommand += ' ' + options.message.map( message => `-m "${ message }"` ).join( ' ' ); + } + + return execCommand.execute( getExecData( mergeCommand ) ); + } ); + + function getExecData( command ) { + return Object.assign( {}, data, { + arguments: [ command ] + } ); + } + }, + + /** + * @private + * @param {Array.} argv List of arguments provided by the user via CLI. + * @returns {Object} + */ + _parseArguments( argv ) { + const options = minimist( argv, buildOptions( { + message: { + type: 'string', + alias: 'm', + } + } ) ); + + if ( !Array.isArray( options.message ) ) { + options.message = [ options.message ].filter( Boolean ); + } + + return options; + } +}; From 96249da40cc66708875014b9da2d20a0c2acc489 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 13 Aug 2018 12:15:32 +0200 Subject: [PATCH 17/38] Code style. --- index.js | 24 ++++++++++++++---------- lib/commands/commit.js | 8 +++----- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index 9c0d222..88fed4d 100755 --- a/index.js +++ b/index.js @@ -35,11 +35,14 @@ const mgitLogo = ` |___/ `; -const u = chalk.underline; -const c = chalk.cyan; -const g = chalk.gray; -const m = chalk.magenta; -const y = chalk.yellow; +const { + italic: i, + cyan: c, + gray: g, + magenta: m, + underline: u, + yellow: y, +} = chalk; const cli = meow( ` ${ mgitLogo } ${ u( 'Usage:' ) } @@ -47,13 +50,14 @@ const cli = meow( ` ${ mgitLogo } ${ u( 'Commands:' ) } ${ c( 'bootstrap' ) } Installs packages (i.e. clone dependent repositories). + ${ c( 'checkout' ) } Changes branches in repositories according to the configuration file. + ${ c( 'commit' ) } Commits all changes. A shorthand for "mgit exec 'git commit -a'". + ${ c( 'diff' ) } Prints changes from packages where something has changed. ${ c( 'exec' ) } Executes shell command in each package. - ${ c( 'update' ) } Updates packages to the latest versions (i.e. pull changes). + ${ c( 'merge' ) } Merges specified branch with the current one. ${ c( 'save-hashes' ) } Saves hashes of packages in mgit.json. It allows to easily fix project to a specific state. ${ c( 'status' ) } Prints a table which contains useful information about the status of repositories. - ${ c( 'diff' ) } Prints changes from packages where something has changed. - ${ c( 'checkout' ) } Changes branches in repositories according to the configuration file. - ${ c( 'commit' ) } Commits all changes. A shorthand for "mgit exec 'git commit -a'" + ${ c( 'update' ) } Updates packages to the latest versions (i.e. pull changes). ${ u( 'Options:' ) } ${ y( '--packages' ) } Directory to which all repositories will be cloned. @@ -102,7 +106,7 @@ if ( !commandName || cli.flags.help ) { const commandInstance = getCommandInstance( commandName ); if ( !commandInstance ) { - process.errorCode = -1; + process.errorCode = 1; return; } diff --git a/lib/commands/commit.js b/lib/commands/commit.js index 1a013b6..25a449c 100644 --- a/lib/commands/commit.js +++ b/lib/commands/commit.js @@ -66,9 +66,9 @@ module.exports = { } const options = this._parseArguments( data.arguments ); - const cliCommand = buildCliCommand( options ); + const commitCommand = buildCliCommand( options ); - return execCommand.execute( getExecData( cliCommand ) ); + return execCommand.execute( getExecData( commitCommand ) ); } ); function getExecData( command ) { @@ -80,9 +80,7 @@ module.exports = { function buildCliCommand( options ) { let command = 'git commit'; - if ( options.message ) { - command += ' ' + options.message.map( message => `-m "${ message }"` ).join( ' ' ); - } + command += ' ' + options.message.map( message => `-m "${ message }"` ).join( ' ' ); if ( options[ 'no-verify' ] ) { command += ' -n'; From f5cf41090468791d63a233ff529fc25364be896c Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Tue, 14 Aug 2018 09:17:24 +0200 Subject: [PATCH 18/38] Fix validation in merge command. --- lib/commands/merge.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/commands/merge.js b/lib/commands/merge.js index bbc94ad..7afdaa8 100644 --- a/lib/commands/merge.js +++ b/lib/commands/merge.js @@ -34,7 +34,7 @@ module.exports = { }, beforeExecute( args ) { - if ( !args[ 0 ] ) { + if ( !args[ 1 ] ) { throw new Error( 'Missing branch to merge. Use: mgit merge [branch].' ); } }, @@ -53,6 +53,8 @@ module.exports = { const branchExists = Boolean( execResponse.logs.info[ 0 ] ); if ( !branchExists ) { + log.info( 'Branch does not exist.' ); + return { logs: log.all() }; From cb5babad5f75a536e2d4880f9bdc2e0735fa179c Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Tue, 14 Aug 2018 09:17:45 +0200 Subject: [PATCH 19/38] Rename save-hashes to save and add options: --hash and --branch. --- lib/commands/save.js | 141 +++++++++++++++++++++++++++++++++++++ lib/commands/savehashes.js | 76 -------------------- 2 files changed, 141 insertions(+), 76 deletions(-) create mode 100644 lib/commands/save.js delete mode 100644 lib/commands/savehashes.js diff --git a/lib/commands/save.js b/lib/commands/save.js new file mode 100644 index 0000000..b3f4725 --- /dev/null +++ b/lib/commands/save.js @@ -0,0 +1,141 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const path = require( 'upath' ); +const chalk = require( 'chalk' ); +const buildOptions = require( 'minimist-options' ); +const minimist = require( 'minimist' ); +const updateJsonFile = require( '../utils/updatejsonfile' ); +const gitStatusParser = require( '../utils/gitstatusparser' ); + +module.exports = { + get helpMessage() { + const { + gray: g, + magenta: m, + underline: u + } = chalk; + + return ` + ${ u( 'Description:' ) } + Saves hashes of commits or branches which repositories are checked out in "mgit.json" file. + + ${ u( 'Options:' ) } + ${ m( '--hash' ) } Whether to save hashes (id of last commit) on current branch. + ${ g( 'Default: true' ) } + ${ m( '--branch' ) } Whether to save names of current branches instead of commit ids. + ${ g( 'Default: false' ) } + ${ g( '> mgit save -- --branch' ) } + `; + }, + + /** + * @param {Array.} args Arguments and options that a user provided calling the command. + */ + beforeExecute( args ) { + const options = this._parseArguments( args ); + + if ( !options.branch && !options.hash) { + throw new Error( 'Need to specify what kind of information you want to save. Call "mgit commit -h" in order to read more.' ); + } + }, + + /** + * @param {CommandData} data + * @returns {Promise} + */ + execute( data ) { + const log = require( '../utils/log' )(); + const execCommand = require( './exec' ); + const options = this._parseArguments( data.arguments ); + + let promise; + + if ( options.branch ) { + promise = execCommand.execute( getExecData( 'git status --branch --porcelain' ) ) + .then( execResponse => gitStatusParser( execResponse.logs.info[ 0 ] ).branch ); + } else if ( options.hash ) { + promise = execCommand.execute( getExecData( 'git rev-parse HEAD' ) ) + .then( execResponse => execResponse.logs.info[ 0 ].slice( 0, 7 ) ); + } + + return promise.then( dataToSave => { + const commandResponse = { + packageName: data.packageName, + data: dataToSave, + branch: options.branch, + hash: options.hash + }; + + if ( options.branch ) { + log.info( `Branch: "${ dataToSave }".` ); + } else if ( options.hash ) { + log.info( `Commit: "${ dataToSave }".` ); + } + + return Promise.resolve( { + response: commandResponse, + logs: log.all() + } ); + } ); + + function getExecData( command ) { + return Object.assign( {}, data, { + arguments: [ command ] + } ); + } + }, + + /** + * Saves collected hashes to configuration file. + * + * @param {Set} processedPackages Collection of processed packages. + * @param {Set} commandResponses Results of executed command for each package. + */ + afterExecute( processedPackages, commandResponses ) { + const cwd = require( '../utils/getcwd' )(); + const mgitJsonPath = path.join( cwd, 'mgit.json' ); + + updateJsonFile( mgitJsonPath, json => { + for ( const response of commandResponses.values() ) { + const repository = json.dependencies[ response.packageName ].split( '#' )[ 0 ]; + + // If returned branch is equal to 'master', save only the repository path. + if ( response.branch && response.data === 'master' ) { + json.dependencies[ response.packageName ] = repository; + } else { + json.dependencies[ response.packageName ] = `${ repository }#${ response.data }`; + } + } + + return json; + } ); + }, + + /** + * @private + * @param {Array.} argv List of arguments provided by the user via CLI. + * @returns {Object} + */ + _parseArguments( argv ) { + const options = minimist( argv, buildOptions( { + branch: { + type: 'boolean', + }, + hash: { + type: 'boolean', + default: true + } + } ) ); + + if ( options.branch ) { + options.hash = false; + } + + return options; + } +}; diff --git a/lib/commands/savehashes.js b/lib/commands/savehashes.js deleted file mode 100644 index e1bb0cc..0000000 --- a/lib/commands/savehashes.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const path = require( 'upath' ); -const chalk = require( 'chalk' ); -const updateJsonFile = require( '../utils/updatejsonfile' ); - -module.exports = { - get helpMessage() { - const { - underline: u - } = chalk; - - return ` - ${ u( 'Description:' ) } - Saves hashes of commits in "mgit.json" file. It allows easily to restore the project to some point. - `; - }, - - /** - * @param {CommandData} data - * @returns {Promise} - */ - execute( data ) { - const log = require( '../utils/log' )(); - const execCommand = require( './exec' ); - - return execCommand.execute( getExecData( 'git rev-parse HEAD' ) ) - .then( execResponse => { - const commitHash = execResponse.logs.info[ 0 ]; - - const commandResponse = { - packageName: data.packageName, - commit: commitHash.slice( 0, 7 ) // Short version of the commit hash. - }; - - log.info( `Commit: "${ commitHash }".` ); - - return Promise.resolve( { - response: commandResponse, - logs: log.all() - } ); - } ); - - function getExecData( command ) { - return Object.assign( {}, data, { - arguments: [ command ] - } ); - } - }, - - /** - * Saves collected hashes to configuration file. - * - * @param {Set} processedPackages Collection of processed packages. - * @param {Set} commandResponses Results of executed command for each package. - */ - afterExecute( processedPackages, commandResponses ) { - const cwd = require( '../utils/getcwd' )(); - const mgitJsonPath = path.join( cwd, 'mgit.json' ); - - updateJsonFile( mgitJsonPath, json => { - for ( const response of commandResponses.values() ) { - const repository = json.dependencies[ response.packageName ].split( '#' )[ 0 ]; - - json.dependencies[ response.packageName ] = `${ repository }#${ response.commit }`; - } - - return json; - } ); - } -}; From 4fd1836105906bf77d6c5c84ed5ca70175c79c60 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Tue, 14 Aug 2018 09:43:06 +0200 Subject: [PATCH 20/38] Rename util exec to shell. Fix failed tests of "getOptions" util. --- lib/commands/bootstrap.js | 4 ++-- lib/commands/exec.js | 4 ++-- lib/utils/getoptions.js | 2 +- lib/utils/{exec.js => shell.js} | 0 tests/commands/bootstrap.js | 14 +++++++------- tests/commands/exec.js | 5 +++-- .../project-with-options-in-mgitjson/mgit.json | 1 - tests/utils/getoptions.js | 4 ---- 8 files changed, 15 insertions(+), 19 deletions(-) rename lib/utils/{exec.js => shell.js} (100%) diff --git a/lib/commands/bootstrap.js b/lib/commands/bootstrap.js index 0bfe9d4..b666c2c 100644 --- a/lib/commands/bootstrap.js +++ b/lib/commands/bootstrap.js @@ -10,7 +10,7 @@ const path = require( 'upath' ); const buildOptions = require( 'minimist-options' ); const minimist = require( 'minimist' ); const chalk = require( 'chalk' ); -const exec = require( '../utils/exec' ); +const shell = require( '../utils/shell' ); module.exports = { get helpMessage() { @@ -58,7 +58,7 @@ module.exports = { `git checkout --quiet ${ data.repository.branch }` ].join( ' && ' ); - promise = exec( command ); + promise = shell( command ); } return promise diff --git a/lib/commands/exec.js b/lib/commands/exec.js index 1e154fc..de0dee9 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -7,8 +7,8 @@ const fs = require( 'fs' ); const path = require( 'upath' ); -const exec = require( '../utils/exec' ); const chalk = require( 'chalk' ); +const shell = require( '../utils/shell' ); module.exports = { get helpMessage() { @@ -54,7 +54,7 @@ module.exports = { process.chdir( newCwd ); - exec( data.arguments[ 0 ] ) + shell( data.arguments[ 0 ] ) .then( stdout => { process.chdir( data.mgitOptions.cwd ); diff --git a/lib/utils/getoptions.js b/lib/utils/getoptions.js index 375760e..0243b3f 100644 --- a/lib/utils/getoptions.js +++ b/lib/utils/getoptions.js @@ -20,7 +20,7 @@ module.exports = function cwdResolver( callOptions, cwd ) { let options = { cwd, packages: 'packages', - resolverPath: path.resolve( __dirname, '../default-resolver.js' ), + resolverPath: path.resolve( __dirname, '..', 'default-resolver.js' ), resolverUrlTemplate: 'git@github.com:${ path }.git', resolverTargetDirectory: 'git', resolverDefaultBranch: 'master', diff --git a/lib/utils/exec.js b/lib/utils/shell.js similarity index 100% rename from lib/utils/exec.js rename to lib/utils/shell.js diff --git a/tests/commands/bootstrap.js b/tests/commands/bootstrap.js index ed1575a..e71553b 100644 --- a/tests/commands/bootstrap.js +++ b/tests/commands/bootstrap.js @@ -24,7 +24,7 @@ describe( 'commands/bootstrap', () => { } ); stubs = { - exec: sinon.stub(), + shell: sinon.stub(), fs: { existsSync: sinon.stub( fs, 'existsSync' ) }, @@ -46,7 +46,7 @@ describe( 'commands/bootstrap', () => { } }; - mockery.registerMock( '../utils/exec', stubs.exec ); + mockery.registerMock( '../utils/shell', stubs.shell ); bootstrapCommand = require( '../../lib/commands/bootstrap' ); } ); @@ -61,7 +61,7 @@ describe( 'commands/bootstrap', () => { const error = new Error( 'Unexpected error.' ); stubs.fs.existsSync.returns( false ); - stubs.exec.returns( Promise.reject( error ) ); + stubs.shell.returns( Promise.reject( error ) ); return bootstrapCommand.execute( data ) .then( @@ -76,13 +76,13 @@ describe( 'commands/bootstrap', () => { it( 'clones a repository if is not available', () => { stubs.fs.existsSync.returns( false ); - stubs.exec.returns( Promise.resolve( 'Git clone log.' ) ); + stubs.shell.returns( Promise.resolve( 'Git clone log.' ) ); return bootstrapCommand.execute( data ) .then( response => { - expect( stubs.exec.calledOnce ).to.equal( true ); + expect( stubs.shell.calledOnce ).to.equal( true ); - const cloneCommand = stubs.exec.firstCall.args[ 0 ].split( ' && ' ); + const cloneCommand = stubs.shell.firstCall.args[ 0 ].split( ' && ' ); // Clone the repository. expect( cloneCommand[ 0 ] ) @@ -101,7 +101,7 @@ describe( 'commands/bootstrap', () => { return bootstrapCommand.execute( data ) .then( response => { - expect( stubs.exec.called ).to.equal( false ); + expect( stubs.shell.called ).to.equal( false ); expect( response.logs.info[ 0 ] ).to.equal( 'Package "test-package" is already cloned.' ); } ); diff --git a/tests/commands/exec.js b/tests/commands/exec.js index e62c141..63c0de5 100644 --- a/tests/commands/exec.js +++ b/tests/commands/exec.js @@ -37,7 +37,8 @@ describe( 'commands/exec', () => { }; data = { - // `execute` is called without the "exec" command (`mgit exec first-cmd other-cmd` => [ 'first-cmd', 'other-cmd' ]). + // Command `#execute` function is called without the "exec" command. + // `mgit exec pwd` => [ 'pwd' ] arguments: [ 'pwd' ], packageName: 'test-package', options: { @@ -49,7 +50,7 @@ describe( 'commands/exec', () => { } }; - mockery.registerMock( '../utils/exec', stubs.exec ); + mockery.registerMock( '../utils/shell', stubs.exec ); execCommand = require( '../../lib/commands/exec' ); } ); diff --git a/tests/fixtures/project-with-options-in-mgitjson/mgit.json b/tests/fixtures/project-with-options-in-mgitjson/mgit.json index a7fcdaa..34b3394 100644 --- a/tests/fixtures/project-with-options-in-mgitjson/mgit.json +++ b/tests/fixtures/project-with-options-in-mgitjson/mgit.json @@ -1,6 +1,5 @@ { "packages": "foo", - "recursive": true, "dependencies": { "simple-package": "a/b" } diff --git a/tests/utils/getoptions.js b/tests/utils/getoptions.js index d4dcefd..e5a389e 100644 --- a/tests/utils/getoptions.js +++ b/tests/utils/getoptions.js @@ -24,7 +24,6 @@ describe( 'utils', () => { expect( options ).to.deep.equal( { cwd, packages: path.resolve( cwd, 'packages' ), - recursive: false, resolverPath: path.resolve( __dirname, '../../lib/default-resolver.js' ), resolverUrlTemplate: 'git@github.com:${ path }.git', resolverTargetDirectory: 'git', @@ -48,7 +47,6 @@ describe( 'utils', () => { expect( options ).to.deep.equal( { cwd, packages: path.resolve( cwd, 'packages' ), - recursive: false, resolverPath: path.resolve( __dirname, '../../lib/default-resolver.js' ), resolverUrlTemplate: 'git@github.com:${ path }.git', resolverTargetDirectory: 'git', @@ -68,7 +66,6 @@ describe( 'utils', () => { }, cwd, packages: path.resolve( cwd, 'foo' ), - recursive: true, resolverPath: path.resolve( __dirname, '../../lib/default-resolver.js' ), resolverUrlTemplate: 'git@github.com:${ path }.git', resolverTargetDirectory: 'git', @@ -91,7 +88,6 @@ describe( 'utils', () => { }, cwd, packages: path.resolve( cwd, 'bar' ), - recursive: true, resolverPath: path.resolve( __dirname, '../../lib/default-resolver.js' ), resolverUrlTemplate: 'a/b/c', resolverTargetDirectory: 'git', From 29a4100d5b5f2a7d95481f33e09915878c80cf9c Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Tue, 14 Aug 2018 11:14:45 +0200 Subject: [PATCH 21/38] Rename save-hashes to save, add option --hash (default) and --branch. --- lib/commands/save.js | 4 +- tests/commands/save.js | 279 +++++++++++++++++++++++++++++++++++ tests/commands/savehashes.js | 142 ------------------ 3 files changed, 282 insertions(+), 143 deletions(-) create mode 100644 tests/commands/save.js delete mode 100644 tests/commands/savehashes.js diff --git a/lib/commands/save.js b/lib/commands/save.js index b3f4725..5a60907 100644 --- a/lib/commands/save.js +++ b/lib/commands/save.js @@ -40,7 +40,7 @@ module.exports = { const options = this._parseArguments( args ); if ( !options.branch && !options.hash) { - throw new Error( 'Need to specify what kind of information you want to save. Call "mgit commit -h" in order to read more.' ); + throw new Error( 'Need to specify what kind of information you want to save. Call "mgit save -h" in order to read more.' ); } }, @@ -55,6 +55,7 @@ module.exports = { let promise; + /* istanbul ignore else */ if ( options.branch ) { promise = execCommand.execute( getExecData( 'git status --branch --porcelain' ) ) .then( execResponse => gitStatusParser( execResponse.logs.info[ 0 ] ).branch ); @@ -71,6 +72,7 @@ module.exports = { hash: options.hash }; + /* istanbul ignore else */ if ( options.branch ) { log.info( `Branch: "${ dataToSave }".` ); } else if ( options.hash ) { diff --git a/tests/commands/save.js b/tests/commands/save.js new file mode 100644 index 0000000..36c3548 --- /dev/null +++ b/tests/commands/save.js @@ -0,0 +1,279 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* jshint mocha:true */ + +'use strict'; + +const path = require( 'upath' ); +const sinon = require( 'sinon' ); +const mockery = require( 'mockery' ); +const expect = require( 'chai' ).expect; + +describe( 'commands/save', () => { + let saveCommand, stubs, commandData, mgitJsonPath, updateFunction; + + beforeEach( () => { + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + execCommand: { + execute: sinon.stub() + }, + path: { + join: sinon.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ) + } + }; + + commandData = { + packageName: 'test-package', + arguments: [] + }; + + mockery.registerMock( './exec', stubs.execCommand ); + mockery.registerMock( '../utils/updatejsonfile', ( pathToFile, callback ) => { + mgitJsonPath = pathToFile; + updateFunction = callback; + } ); + mockery.registerMock( '../utils/getcwd', () => { + return __dirname; + } ); + + saveCommand = require( '../../lib/commands/save' ); + } ); + + afterEach( () => { + sinon.restore(); + mockery.disable(); + } ); + + describe( '#helpMessage', () => { + it( 'defines help screen', () => { + expect( saveCommand.helpMessage ).is.a( 'string' ); + } ); + } ); + + describe( 'beforeExecute()', () => { + it( 'throws an error if default option is canceled', () => { + const errorMessage = 'Need to specify what kind of information you want to save. Call "mgit save -h" in order to read more.'; + + expect( () => { + saveCommand.beforeExecute( [ 'hash', '--no-hash' ] ); + } ).to.throw( Error, errorMessage ); + } ); + + it( 'does nothing if options are valid (--branch)', () => { + expect( () => { + saveCommand.beforeExecute( [ 'hash', '--branch' ] ); + } ).to.not.throw( Error ); + } ); + + it( 'does nothing if called only command (without options)', () => { + expect( () => { + saveCommand.beforeExecute( [ 'hash' ] ); + } ).to.not.throw( Error ); + } ); + } ); + + describe( 'execute()', () => { + it( 'rejects promise if called command returned an error', () => { + const error = new Error( 'Unexpected error.' ); + + stubs.execCommand.execute.returns( Promise.reject( { + logs: { + error: [ error.stack ] + } + } ) ); + + return saveCommand.execute( commandData ) + .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: [ '584f341' ] + } + }; + + stubs.execCommand.execute.returns( Promise.resolve( execCommandResponse ) ); + + return saveCommand.execute( commandData ) + .then( commandResponse => { + expect( stubs.execCommand.execute.calledOnce ).to.equal( true ); + expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { + packageName: commandData.packageName, + arguments: [ 'git rev-parse HEAD' ] + } ); + + expect( commandResponse.response ).to.deep.equal( { + packageName: commandData.packageName, + data: '584f341', + branch: false, + hash: true + } ); + + expect( commandResponse.logs.info[ 0 ] ).to.equal( 'Commit: "584f341".' ); + } ); + } ); + + it( 'resolves promise with a name of current branch if called with --branch option', () => { + const execCommandResponse = { + logs: { + info: [ '## master...origin/master' ] + } + }; + + stubs.execCommand.execute.returns( Promise.resolve( execCommandResponse ) ); + commandData.arguments.push( '--branch' ); + + return saveCommand.execute( commandData ) + .then( commandResponse => { + expect( stubs.execCommand.execute.calledOnce ).to.equal( true ); + expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { + packageName: commandData.packageName, + arguments: [ 'git status --branch --porcelain' ] + } ); + + expect( commandResponse.response ).to.deep.equal( { + packageName: commandData.packageName, + data: 'master', + branch: true, + hash: false + } ); + + expect( commandResponse.logs.info[ 0 ] ).to.equal( 'Branch: "master".' ); + } ); + } ); + } ); + + describe( 'afterExecute()', () => { + it( 'updates collected hashes in "mgit.json" (--hash option)', () => { + const processedPackages = new Set(); + const commandResponses = new Set(); + + processedPackages.add( 'test-package' ); + processedPackages.add( 'package-test' ); + + commandResponses.add( { + packageName: 'test-package', + data: '584f341', + hash: true, + branch: false + } ); + commandResponses.add( { + packageName: 'package-test', + data: '52910fe', + hash: true, + branch: false + } ); + + saveCommand.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#584f341', + 'package-test': 'organization/package-test#52910fe', + 'other-package': 'organization/other-package' + } ); + } ); + + it( 'updates collected branches in "mgit.json" (--branch option)', () => { + const processedPackages = new Set(); + const commandResponses = new Set(); + + processedPackages.add( 'test-package' ); + processedPackages.add( 'package-test' ); + + commandResponses.add( { + packageName: 'test-package', + data: 'develop', + hash: false, + branch: true + } ); + commandResponses.add( { + packageName: 'package-test', + data: 'develop', + hash: false, + branch: true + } ); + + saveCommand.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#develop', + 'package-test': 'organization/package-test#develop', + 'other-package': 'organization/other-package' + } ); + } ); + + it( 'does not save "#master" branch because it is default branch', () => { + const processedPackages = new Set(); + const commandResponses = new Set(); + + processedPackages.add( 'test-package' ); + + commandResponses.add( { + packageName: 'test-package', + data: 'master', + hash: false, + branch: true + } ); + + saveCommand.afterExecute( processedPackages, commandResponses ); + + let json = { + dependencies: { + 'test-package': 'organization/test-package#some-branch', + } + }; + + 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', + } ); + } ); + } ); +} ); diff --git a/tests/commands/savehashes.js b/tests/commands/savehashes.js deleted file mode 100644 index 7e873e1..0000000 --- a/tests/commands/savehashes.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/* jshint mocha:true */ - -'use strict'; - -const path = require( 'upath' ); -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); -const expect = require( 'chai' ).expect; - -describe( 'commands/savehashes', () => { - let saveHashesCommand, stubs, data, mgitJsonPath, updateFunction; - - beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - execCommand: { - execute: sinon.stub() - }, - path: { - join: sinon.stub( path, 'join' ).callsFake( ( ...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( () => { - sinon.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( { - logs: { - error: [ error.stack ] - } - } ) ); - - 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: [ '584f341' ] - } - }; - - 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: '584f341' - } ); - - expect( commandResponse.logs.info[ 0 ] ).to.equal( 'Commit: "584f341".' ); - } ); - } ); - } ); - - 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: '584f341' - } ); - commandResponses.add( { - packageName: 'package-test', - commit: '52910fe' - } ); - - 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#584f341', - 'package-test': 'organization/package-test#52910fe', - 'other-package': 'organization/other-package' - } ); - } ); - } ); -} ); From e3d5b3242be10f72da0a5b28faf069335dfe4af4 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Tue, 14 Aug 2018 15:00:29 +0200 Subject: [PATCH 22/38] Add tests for new commands and adjust existing to changes in utils. --- lib/commands/commit.js | 3 +- lib/commands/merge.js | 1 + lib/commands/status.js | 2 + tests/commands/bootstrap.js | 49 +++++-- tests/commands/checkout.js | 144 +++++++++++++++++-- tests/commands/commit.js | 269 ++++++++++++++++++++++++++++++++++++ tests/commands/diff.js | 34 ++++- tests/commands/exec.js | 27 ++-- tests/commands/merge.js | 207 +++++++++++++++++++++++++++ tests/commands/save.js | 9 +- tests/commands/status.js | 48 ++++--- tests/commands/update.js | 41 ++++-- 12 files changed, 757 insertions(+), 77 deletions(-) create mode 100644 tests/commands/commit.js create mode 100644 tests/commands/merge.js diff --git a/lib/commands/commit.js b/lib/commands/commit.js index 25a449c..f8943ba 100644 --- a/lib/commands/commit.js +++ b/lib/commands/commit.js @@ -78,7 +78,7 @@ module.exports = { } function buildCliCommand( options ) { - let command = 'git commit'; + let command = 'git commit -a'; command += ' ' + options.message.map( message => `-m "${ message }"` ).join( ' ' ); @@ -108,6 +108,7 @@ module.exports = { } } ) ); + /* istanbul ignore else */ if ( !Array.isArray( options.message ) ) { options.message = [ options.message ].filter( Boolean ); } diff --git a/lib/commands/merge.js b/lib/commands/merge.js index 7afdaa8..8d72409 100644 --- a/lib/commands/merge.js +++ b/lib/commands/merge.js @@ -92,6 +92,7 @@ module.exports = { } } ) ); + /* istanbul ignore else */ if ( !Array.isArray( options.message ) ) { options.message = [ options.message ].filter( Boolean ); } diff --git a/lib/commands/status.js b/lib/commands/status.js index 46d1a46..32721e7 100644 --- a/lib/commands/status.js +++ b/lib/commands/status.js @@ -92,12 +92,14 @@ module.exports = { const packagesResponses = Array.from( commandResponses.values() ) .sort( ( a, b ) => { + /* istanbul ignore else */ if ( a.packageName < b.packageName ) { return -1; } else if ( a.packageName > b.packageName ) { return 1; } + /* istanbul ignore next */ return 0; } ); diff --git a/tests/commands/bootstrap.js b/tests/commands/bootstrap.js index e71553b..2bf1d3f 100644 --- a/tests/commands/bootstrap.js +++ b/tests/commands/bootstrap.js @@ -14,7 +14,7 @@ const mockery = require( 'mockery' ); const expect = require( 'chai' ).expect; describe( 'commands/bootstrap', () => { - let bootstrapCommand, stubs, data; + let bootstrapCommand, stubs, commandData; beforeEach( () => { mockery.enable( { @@ -33,9 +33,10 @@ describe( 'commands/bootstrap', () => { } }; - data = { + commandData = { + arguments: [], packageName: 'test-package', - options: { + mgitOptions: { cwd: __dirname, packages: 'packages' }, @@ -53,9 +54,29 @@ describe( 'commands/bootstrap', () => { afterEach( () => { sinon.restore(); + mockery.deregisterAll(); mockery.disable(); } ); + describe( '#helpMessage', () => { + it( 'defines help screen', () => { + expect( bootstrapCommand.helpMessage ).is.a( 'string' ); + } ); + } ); + + describe( 'beforeExecute()', () => { + it( 'informs about starting the process', () => { + const consoleLog = sinon.stub( console, 'log' ); + + bootstrapCommand.beforeExecute(); + + expect( consoleLog.calledOnce ).to.equal( true ); + expect( consoleLog.firstCall.args[ 0 ] ).to.match( /Cloning missing packages\.\.\./ ); + + consoleLog.restore(); + } ); + } ); + describe( 'execute()', () => { it( 'rejects promise if something went wrong', () => { const error = new Error( 'Unexpected error.' ); @@ -63,7 +84,7 @@ describe( 'commands/bootstrap', () => { stubs.fs.existsSync.returns( false ); stubs.shell.returns( Promise.reject( error ) ); - return bootstrapCommand.execute( data ) + return bootstrapCommand.execute( commandData ) .then( () => { throw new Error( 'Supposed to be rejected.' ); @@ -78,7 +99,7 @@ describe( 'commands/bootstrap', () => { stubs.fs.existsSync.returns( false ); stubs.shell.returns( Promise.resolve( 'Git clone log.' ) ); - return bootstrapCommand.execute( data ) + return bootstrapCommand.execute( commandData ) .then( response => { expect( stubs.shell.calledOnce ).to.equal( true ); @@ -99,7 +120,7 @@ describe( 'commands/bootstrap', () => { it( 'does not clone a repository if is available', () => { stubs.fs.existsSync.returns( true ); - return bootstrapCommand.execute( data ) + return bootstrapCommand.execute( commandData ) .then( response => { expect( stubs.shell.called ).to.equal( false ); @@ -108,13 +129,13 @@ describe( 'commands/bootstrap', () => { } ); it( 'installs dependencies of cloned package', () => { - data.options.recursive = true; - data.options.packages = __dirname + '/../fixtures'; - data.repository.directory = 'project-a'; + commandData.arguments.push( '--recursive' ); + commandData.mgitOptions.packages = __dirname + '/../fixtures'; + commandData.repository.directory = 'project-a'; stubs.fs.existsSync.returns( true ); - return bootstrapCommand.execute( data ) + return bootstrapCommand.execute( commandData ) .then( response => { expect( response.packages ).is.an( 'array' ); expect( response.packages ).to.deep.equal( [ 'test-foo' ] ); @@ -122,13 +143,13 @@ describe( 'commands/bootstrap', () => { } ); it( 'installs devDependencies of cloned package', () => { - data.options.recursive = true; - data.options.packages = __dirname + '/../fixtures'; - data.repository.directory = 'project-with-options-in-mgitjson'; + commandData.arguments.push( '--recursive' ); + commandData.mgitOptions.packages = __dirname + '/../fixtures'; + commandData.repository.directory = 'project-with-options-in-mgitjson'; stubs.fs.existsSync.returns( true ); - return bootstrapCommand.execute( data ) + return bootstrapCommand.execute( commandData ) .then( response => { expect( response.packages ).is.an( 'array' ); expect( response.packages ).to.deep.equal( [ 'test-bar' ] ); diff --git a/tests/commands/checkout.js b/tests/commands/checkout.js index 09ed10e..3e0b17f 100644 --- a/tests/commands/checkout.js +++ b/tests/commands/checkout.js @@ -12,7 +12,7 @@ const mockery = require( 'mockery' ); const expect = require( 'chai' ).expect; describe( 'commands/checkout', () => { - let checkoutCommand, stubs, data; + let checkoutCommand, stubs, commandData; beforeEach( () => { mockery.enable( { @@ -24,25 +24,41 @@ describe( 'commands/checkout', () => { stubs = { execCommand: { execute: sinon.stub() - } + }, + gitStatusParser: sinon.stub() }; - data = { + commandData = { + arguments: [], repository: { branch: 'master' } }; mockery.registerMock( './exec', stubs.execCommand ); + mockery.registerMock( '../utils/gitstatusparser', stubs.gitStatusParser ); checkoutCommand = require( '../../lib/commands/checkout' ); } ); afterEach( () => { sinon.restore(); + mockery.deregisterAll(); mockery.disable(); } ); + describe( '#helpMessage', () => { + it( 'defines help screen', () => { + expect( checkoutCommand.helpMessage ).is.a( 'string' ); + } ); + } ); + + describe( '#name', () => { + it( 'returns a full name of executed command', () => { + expect( checkoutCommand.name ).is.a( 'string' ); + } ); + } ); + describe( 'execute()', () => { it( 'rejects promise if called command returned an error', () => { const error = new Error( 'Unexpected error.' ); @@ -53,7 +69,7 @@ describe( 'commands/checkout', () => { } } ); - return checkoutCommand.execute( data ) + return checkoutCommand.execute( commandData ) .then( () => { throw new Error( 'Supposed to be rejected.' ); @@ -67,11 +83,14 @@ describe( 'commands/checkout', () => { it( 'checkouts to the correct branch', () => { stubs.execCommand.execute.resolves( { logs: { - info: [ 'Already on \'master\'\nYour branch is up-to-date with \'origin/master\'.' ] + info: [ + 'Already on \'master\'', + 'Already on \'master\'\nYour branch is up-to-date with \'origin/master\'.' + ] } } ); - return checkoutCommand.execute( data ) + return checkoutCommand.execute( commandData ) .then( commandResponse => { expect( stubs.execCommand.execute.calledOnce ).to.equal( true ); expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { @@ -81,9 +100,116 @@ describe( 'commands/checkout', () => { arguments: [ 'git checkout master' ] } ); - expect( commandResponse.logs.info[ 0 ] ).to.equal( - 'Your branch is up-to-date with \'origin/master\'.' - ); + expect( commandResponse.logs.info ).to.deep.equal( [ + 'Already on \'master\'', + 'Already on \'master\'\nYour branch is up-to-date with \'origin/master\'.' + ] ); + } ); + } ); + + it( 'checkouts to specified branch', () => { + commandData.arguments.push( 'develop' ); + + stubs.execCommand.execute.resolves( { + logs: { + info: [ + 'Switched to branch \'develop\'', + 'Your branch is up to date with \'origin/develop\'.' + ] + } + } ); + + return checkoutCommand.execute( commandData ) + .then( commandResponse => { + expect( stubs.execCommand.execute.calledOnce ).to.equal( true ); + expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git checkout develop' ] + } ); + + expect( commandResponse.logs.info ).to.deep.equal( [ + 'Switched to branch \'develop\'', + 'Your branch is up to date with \'origin/develop\'.' + ] ); + } ); + } ); + + it( 'creates a new branch if a repository has changes that could be committed and specified --branch option', () => { + commandData.arguments.push( '--branch' ); + commandData.arguments.push( 'develop' ); + + stubs.execCommand.execute.onFirstCall().resolves( { + logs: { + info: [ + 'Response returned by "git status" command.' + ] + } + } ); + + stubs.execCommand.execute.onSecondCall().resolves( { + logs: { + info: [ + 'Switched to a new branch \'develop\'' + ] + } + } ); + + stubs.gitStatusParser.returns( { anythingToCommit: true } ); + + return checkoutCommand.execute( commandData ) + .then( commandResponse => { + expect( stubs.execCommand.execute.calledTwice ).to.equal( true ); + + expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git status --branch --porcelain' ] + } ); + + expect( stubs.execCommand.execute.secondCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git checkout -b develop' ] + } ); + + expect( commandResponse.logs.info ).to.deep.equal( [ + 'Switched to a new branch \'develop\'' + ] ); + } ); + } ); + + it( 'does not create a branch if a repository has no-changes that could be committed when specified --branch option', () => { + commandData.arguments.push( '--branch' ); + commandData.arguments.push( 'develop' ); + + stubs.execCommand.execute.onFirstCall().resolves( { + logs: { + info: [ + 'Response returned by "git status" command.' + ] + } + } ); + + stubs.gitStatusParser.returns( { anythingToCommit: false } ); + + return checkoutCommand.execute( commandData ) + .then( commandResponse => { + expect( stubs.execCommand.execute.calledOnce ).to.equal( true ); + + expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git status --branch --porcelain' ] + } ); + + expect( commandResponse.logs.info ).to.deep.equal( [ + 'Repository does not contain changes to commit. New branch was not created.' + ] ); } ); } ); } ); diff --git a/tests/commands/commit.js b/tests/commands/commit.js new file mode 100644 index 0000000..f5151d8 --- /dev/null +++ b/tests/commands/commit.js @@ -0,0 +1,269 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* jshint mocha:true */ + +'use strict'; + +const sinon = require( 'sinon' ); +const mockery = require( 'mockery' ); +const expect = require( 'chai' ).expect; + +describe( 'commands/commit', () => { + let commitCommand, stubs, commandData; + + beforeEach( () => { + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + execCommand: { + execute: sinon.stub() + }, + gitStatusParser: sinon.stub() + }; + + commandData = { + arguments: [], + repository: { + branch: 'master' + } + }; + + mockery.registerMock( './exec', stubs.execCommand ); + mockery.registerMock( '../utils/gitstatusparser', stubs.gitStatusParser ); + + commitCommand = require( '../../lib/commands/commit' ); + } ); + + afterEach( () => { + sinon.restore(); + mockery.deregisterAll(); + mockery.disable(); + } ); + + describe( '#helpMessage', () => { + it( 'defines help screen', () => { + expect( commitCommand.helpMessage ).is.a( 'string' ); + } ); + } ); + + describe( '#name', () => { + it( 'returns a full name of executed command', () => { + expect( commitCommand.name ).is.a( 'string' ); + } ); + } ); + + describe( 'beforeExecute()', () => { + it( 'throws an error if command to execute is not specified', () => { + expect( () => { + commitCommand.beforeExecute( [ 'commit' ] ); + } ).to.throw( Error, 'Missing --message (-m) option. Call "mgit commit -h" in order to read more.' ); + } ); + + it( 'does nothing if specified message for commit', () => { + expect( () => { + commitCommand.beforeExecute( [ 'commit', '--message', 'Test' ] ); + } ).to.not.throw( Error ); + } ); + } ); + + describe( 'execute()', () => { + it( 'rejects promise if called command returned an error', () => { + const error = new Error( 'Unexpected error.' ); + + stubs.execCommand.execute.rejects( { + logs: { + error: [ error.stack ] + } + } ); + + return commitCommand.execute( commandData ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + response => { + expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( `Error: ${ error.message }` ); + } + ); + } ); + + it( 'commits all changes', () => { + commandData.arguments.push( '--message' ); + commandData.arguments.push( 'Test.' ); + + stubs.execCommand.execute.onFirstCall().resolves( { + logs: { + info: [ + 'Response returned by "git status" command.' + ] + } + } ); + + stubs.execCommand.execute.onSecondCall().resolves( { + logs: { + info: [ + '[master a89f9ee] Test.' + ] + } + } ); + + stubs.gitStatusParser.returns( { anythingToCommit: true } ); + + return commitCommand.execute( commandData ) + .then( commandResponse => { + expect( stubs.execCommand.execute.calledTwice ).to.equal( true ); + + expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git status --branch --porcelain' ] + } ); + + expect( stubs.execCommand.execute.secondCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git commit -a -m "Test."' ] + } ); + + expect( commandResponse.logs.info ).to.deep.equal( [ + '[master a89f9ee] Test.' + ] ); + } ); + } ); + + it( 'accepts `--no-verify` option', () => { + commandData.arguments.push( '-n' ); + commandData.arguments.push( '--message' ); + commandData.arguments.push( 'Test' ); + + stubs.execCommand.execute.onFirstCall().resolves( { + logs: { + info: [ + 'Response returned by "git status" command.' + ] + } + } ); + + stubs.execCommand.execute.onSecondCall().resolves( { + logs: { + info: [ + '[master a89f9ee] Test' + ] + } + } ); + + stubs.gitStatusParser.returns( { anythingToCommit: true } ); + + return commitCommand.execute( commandData ) + .then( commandResponse => { + expect( stubs.execCommand.execute.calledTwice ).to.equal( true ); + + expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git status --branch --porcelain' ] + } ); + + expect( stubs.execCommand.execute.secondCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git commit -a -m "Test" -n' ] + } ); + + expect( commandResponse.logs.info ).to.deep.equal( [ + '[master a89f9ee] Test' + ] ); + } ); + } ); + + it( 'accepts duplicated `--message` option', () => { + commandData.arguments.push( '--message' ); + commandData.arguments.push( 'Test.' ); + commandData.arguments.push( '-m' ); + commandData.arguments.push( 'Foo.' ); + + stubs.execCommand.execute.onFirstCall().resolves( { + logs: { + info: [ + 'Response returned by "git status" command.' + ] + } + } ); + + stubs.execCommand.execute.onSecondCall().resolves( { + logs: { + info: [ + '[master a89f9ee] Test.' + ] + } + } ); + + stubs.gitStatusParser.returns( { anythingToCommit: true } ); + + return commitCommand.execute( commandData ) + .then( commandResponse => { + expect( stubs.execCommand.execute.calledTwice ).to.equal( true ); + + expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git status --branch --porcelain' ] + } ); + + expect( stubs.execCommand.execute.secondCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git commit -a -m "Test." -m "Foo."' ] + } ); + + expect( commandResponse.logs.info ).to.deep.equal( [ + '[master a89f9ee] Test.' + ] ); + } ); + } ); + + it( 'does not commit if there is no changes', () => { + commandData.arguments.push( '--message' ); + commandData.arguments.push( 'Test.' ); + + stubs.execCommand.execute.onFirstCall().resolves( { + logs: { + info: [ + 'Response returned by "git status" command.' + ] + } + } ); + + stubs.gitStatusParser.returns( { anythingToCommit: false } ); + + return commitCommand.execute( commandData ) + .then( commandResponse => { + expect( stubs.execCommand.execute.calledOnce ).to.equal( true ); + + expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git status --branch --porcelain' ] + } ); + + expect( commandResponse.logs.info ).to.deep.equal( [ + 'Nothing to commit.' + ] ); + } ); + } ); + } ); +} ); diff --git a/tests/commands/diff.js b/tests/commands/diff.js index 3003fec..d20c6f5 100644 --- a/tests/commands/diff.js +++ b/tests/commands/diff.js @@ -12,7 +12,7 @@ const mockery = require( 'mockery' ); const expect = require( 'chai' ).expect; describe( 'commands/diff', () => { - let diffCommand, stubs, data; + let diffCommand, stubs, commandData; beforeEach( () => { mockery.enable( { @@ -27,7 +27,7 @@ describe( 'commands/diff', () => { } }; - data = { + commandData = { arguments: [] }; @@ -38,9 +38,29 @@ describe( 'commands/diff', () => { afterEach( () => { sinon.restore(); + mockery.deregisterAll(); mockery.disable(); } ); + describe( '#helpMessage', () => { + it( 'defines help screen', () => { + expect( diffCommand.helpMessage ).is.a( 'string' ); + } ); + } ); + + describe( 'beforeExecute()', () => { + it( 'informs about starting the process', () => { + const consoleLog = sinon.stub( console, 'log' ); + + diffCommand.beforeExecute(); + + expect( consoleLog.calledOnce ).to.equal( true ); + expect( consoleLog.firstCall.args[ 0 ] ).to.match( /Collecting changes\.\.\./ ); + + consoleLog.restore(); + } ); + } ); + describe( 'execute()', () => { it( 'rejects promise if called command returned an error', () => { const error = new Error( 'Unexpected error.' ); @@ -51,7 +71,7 @@ describe( 'commands/diff', () => { } } ); - return diffCommand.execute( data ) + return diffCommand.execute( commandData ) .then( () => { throw new Error( 'Supposed to be rejected.' ); @@ -79,7 +99,7 @@ describe( 'commands/diff', () => { } } ); - return diffCommand.execute( data ) + return diffCommand.execute( commandData ) .then( diffResponse => { expect( stubs.execCommand.execute.calledOnce ).to.equal( true ); expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { @@ -93,7 +113,7 @@ describe( 'commands/diff', () => { it( 'does not return the logs when repository has not changed', () => { stubs.execCommand.execute.resolves( { logs: { info: [] } } ); - return diffCommand.execute( data ) + return diffCommand.execute( commandData ) .then( diffResponse => { expect( stubs.execCommand.execute.calledOnce ).to.equal( true ); @@ -104,12 +124,12 @@ describe( 'commands/diff', () => { it( 'allows modifying the "git diff" command', () => { stubs.execCommand.execute.resolves( { logs: { info: [] } } ); - data.arguments = [ + commandData.arguments = [ '--stat', '--staged' ]; - return diffCommand.execute( data ) + return diffCommand.execute( commandData ) .then( diffResponse => { expect( stubs.execCommand.execute.calledOnce ).to.equal( true ); expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { diff --git a/tests/commands/exec.js b/tests/commands/exec.js index 63c0de5..5ccf451 100644 --- a/tests/commands/exec.js +++ b/tests/commands/exec.js @@ -14,7 +14,7 @@ const mockery = require( 'mockery' ); const expect = require( 'chai' ).expect; describe( 'commands/exec', () => { - let execCommand, stubs, data; + let execCommand, stubs, commandData; beforeEach( () => { mockery.enable( { @@ -24,7 +24,7 @@ describe( 'commands/exec', () => { } ); stubs = { - exec: sinon.stub(), + shell: sinon.stub(), fs: { existsSync: sinon.stub( fs, 'existsSync' ) }, @@ -36,12 +36,12 @@ describe( 'commands/exec', () => { } }; - data = { + commandData = { // Command `#execute` function is called without the "exec" command. // `mgit exec pwd` => [ 'pwd' ] arguments: [ 'pwd' ], packageName: 'test-package', - options: { + mgitOptions: { cwd: __dirname, packages: 'packages' }, @@ -50,16 +50,23 @@ describe( 'commands/exec', () => { } }; - mockery.registerMock( '../utils/shell', stubs.exec ); + mockery.registerMock( '../utils/shell', stubs.shell ); execCommand = require( '../../lib/commands/exec' ); } ); afterEach( () => { sinon.restore(); + mockery.deregisterAll(); mockery.disable(); } ); + describe( '#helpMessage', () => { + it( 'defines help screen', () => { + expect( execCommand.helpMessage ).is.a( 'string' ); + } ); + } ); + describe( 'beforeExecute()', () => { it( 'throws an error if command to execute is not specified', () => { expect( () => { @@ -79,7 +86,7 @@ describe( 'commands/exec', () => { it( 'does not execute the command if package is not available', () => { stubs.fs.existsSync.returns( false ); - return execCommand.execute( data ) + return execCommand.execute( commandData ) .then( () => { throw new Error( 'Supposed to be rejected.' ); @@ -95,9 +102,9 @@ describe( 'commands/exec', () => { const error = new Error( 'Unexpected error.' ); stubs.fs.existsSync.returns( true ); - stubs.exec.returns( Promise.reject( error ) ); + stubs.shell.returns( Promise.reject( error ) ); - return execCommand.execute( data ) + return execCommand.execute( commandData ) .then( () => { throw new Error( 'Supposed to be rejected.' ); @@ -114,9 +121,9 @@ describe( 'commands/exec', () => { it( 'resolves promise if command has been executed', () => { const pwd = '/packages/test-package'; stubs.fs.existsSync.returns( true ); - stubs.exec.returns( Promise.resolve( pwd ) ); + stubs.shell.returns( Promise.resolve( pwd ) ); - return execCommand.execute( data ) + return execCommand.execute( commandData ) .then( response => { expect( stubs.process.chdir.calledTwice ).to.equal( true ); expect( stubs.process.chdir.firstCall.args[ 0 ] ).to.equal( 'packages/test-package' ); diff --git a/tests/commands/merge.js b/tests/commands/merge.js new file mode 100644 index 0000000..0456534 --- /dev/null +++ b/tests/commands/merge.js @@ -0,0 +1,207 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* jshint mocha:true */ + +'use strict'; + +const sinon = require( 'sinon' ); +const mockery = require( 'mockery' ); +const expect = require( 'chai' ).expect; + +describe( 'commands/merge', () => { + let mergeCommand, stubs, commandData; + + beforeEach( () => { + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + execCommand: { + execute: sinon.stub() + } + }; + + commandData = { + arguments: [], + repository: { + branch: 'master' + } + }; + + mockery.registerMock( './exec', stubs.execCommand ); + + mergeCommand = require( '../../lib/commands/merge' ); + } ); + + afterEach( () => { + sinon.restore(); + mockery.deregisterAll(); + mockery.disable(); + } ); + + describe( '#helpMessage', () => { + it( 'defines help screen', () => { + expect( mergeCommand.helpMessage ).is.a( 'string' ); + } ); + } ); + + describe( 'beforeExecute()', () => { + it( 'throws an error if command to execute is not specified', () => { + expect( () => { + mergeCommand.beforeExecute( [ 'merge' ] ); + } ).to.throw( Error, 'Missing branch to merge. Use: mgit merge [branch].' ); + } ); + + it( 'does nothing if branch to merge is specified', () => { + expect( () => { + mergeCommand.beforeExecute( [ 'merge', 'develop' ] ); + } ).to.not.throw( Error ); + } ); + } ); + + describe( 'execute()', () => { + it( 'rejects promise if called command returned an error', () => { + const error = new Error( 'Unexpected error.' ); + + stubs.execCommand.execute.rejects( { + logs: { + error: [ error.stack ] + } + } ); + + return mergeCommand.execute( commandData ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + response => { + expect( response.logs.error[ 0 ].split( '\n' )[ 0 ] ).to.equal( `Error: ${ error.message }` ); + } + ); + } ); + + it( 'merges specified branch', () => { + commandData.arguments.push( 'develop' ); + + stubs.execCommand.execute.onFirstCall().resolves( { + logs: { + info: [ + '* develop' + ] + } + } ); + + stubs.execCommand.execute.onSecondCall().resolves( { + logs: { + info: [ + 'Merge made by the \'recursive\' strategy.' + ] + } + } ); + + return mergeCommand.execute( commandData ) + .then( commandResponse => { + expect( stubs.execCommand.execute.calledTwice ).to.equal( true ); + + expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git branch --list develop' ] + } ); + + expect( stubs.execCommand.execute.secondCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git merge develop --no-ff -m "Merge branch \'develop\'"' ] + } ); + + expect( commandResponse.logs.info ).to.deep.equal( [ + 'Merge made by the \'recursive\' strategy.' + ] ); + } ); + } ); + + it( 'merges specified branch using specified message', () => { + commandData.arguments.push( 'develop' ); + commandData.arguments.push( '--message' ); + commandData.arguments.push( 'Test.' ); + + stubs.execCommand.execute.onFirstCall().resolves( { + logs: { + info: [ + '* develop' + ] + } + } ); + + stubs.execCommand.execute.onSecondCall().resolves( { + logs: { + info: [ + 'Merge made by the \'recursive\' strategy.' + ] + } + } ); + + return mergeCommand.execute( commandData ) + .then( commandResponse => { + expect( stubs.execCommand.execute.calledTwice ).to.equal( true ); + + expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git branch --list develop' ] + } ); + + expect( stubs.execCommand.execute.secondCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git merge develop --no-ff -m "Merge branch \'develop\'" -m "Test."' ] + } ); + + expect( commandResponse.logs.info ).to.deep.equal( [ + 'Merge made by the \'recursive\' strategy.' + ] ); + } ); + } ); + + it( 'does not merge branch if it does not exist', () => { + commandData.arguments.push( 'develop' ); + commandData.arguments.push( '--message' ); + commandData.arguments.push( 'Test.' ); + + stubs.execCommand.execute.onFirstCall().resolves( { + logs: { + info: [ + '' + ] + } + } ); + + return mergeCommand.execute( commandData ) + .then( commandResponse => { + expect( stubs.execCommand.execute.calledOnce ).to.equal( true ); + + expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git branch --list develop' ] + } ); + + expect( commandResponse.logs.info ).to.deep.equal( [ + 'Branch does not exist.' + ] ); + } ); + } ); + } ); +} ); diff --git a/tests/commands/save.js b/tests/commands/save.js index 36c3548..10b356b 100644 --- a/tests/commands/save.js +++ b/tests/commands/save.js @@ -28,7 +28,8 @@ describe( 'commands/save', () => { }, path: { join: sinon.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ) - } + }, + gitStatusParser: sinon.stub() }; commandData = { @@ -44,12 +45,14 @@ describe( 'commands/save', () => { mockery.registerMock( '../utils/getcwd', () => { return __dirname; } ); + mockery.registerMock( '../utils/gitstatusparser', stubs.gitStatusParser ); saveCommand = require( '../../lib/commands/save' ); } ); afterEach( () => { sinon.restore(); + mockery.deregisterAll(); mockery.disable(); } ); @@ -137,9 +140,11 @@ describe( 'commands/save', () => { } }; - stubs.execCommand.execute.returns( Promise.resolve( execCommandResponse ) ); commandData.arguments.push( '--branch' ); + stubs.gitStatusParser.returns( { branch: 'master' } ); + stubs.execCommand.execute.returns( Promise.resolve( execCommandResponse ) ); + return saveCommand.execute( commandData ) .then( commandResponse => { expect( stubs.execCommand.execute.calledOnce ).to.equal( true ); diff --git a/tests/commands/status.js b/tests/commands/status.js index fd22b70..82cca4e 100644 --- a/tests/commands/status.js +++ b/tests/commands/status.js @@ -12,7 +12,7 @@ const mockery = require( 'mockery' ); const expect = require( 'chai' ).expect; describe( 'commands/status', () => { - let statusCommand, stubs, data; + let statusCommand, stubs, commandData; beforeEach( () => { mockery.enable( { @@ -42,8 +42,8 @@ describe( 'commands/status', () => { } }; - data = { - options: { + commandData = { + mgitOptions: { packagesPrefix: '@ckeditor/ckeditor5-' }, repository: { @@ -55,13 +55,14 @@ describe( 'commands/status', () => { // Do not modify the color. mockery.registerMock( 'chalk', { - cyan: stubs.chalk.cyan.callsFake( chalkCallsFake() ), - bold: stubs.chalk.bold.callsFake( chalkCallsFake() ), - yellow: stubs.chalk.yellow.callsFake( chalkCallsFake() ), - green: stubs.chalk.green.callsFake( chalkCallsFake() ), - red: stubs.chalk.red.callsFake( chalkCallsFake() ), - blue: stubs.chalk.blue.callsFake( chalkCallsFake() ), - magenta: stubs.chalk.magenta.callsFake( chalkCallsFake() ) + cyan: stubs.chalk.cyan.callsFake( msg => msg ), + bold: stubs.chalk.bold.callsFake( msg => msg ), + yellow: stubs.chalk.yellow.callsFake( msg => msg ), + green: stubs.chalk.green.callsFake( msg => msg ), + red: stubs.chalk.red.callsFake( msg => msg ), + blue: stubs.chalk.blue.callsFake( msg => msg ), + magenta: stubs.chalk.magenta.callsFake( msg => msg ), + underline: stubs.chalk.magenta.callsFake( msg => msg ) } ); mockery.registerMock( 'cli-table', class Table { constructor( ...args ) { @@ -80,17 +81,26 @@ describe( 'commands/status', () => { mockery.registerMock( '../utils/gitstatusparser', stubs.gitStatusParser ); statusCommand = require( '../../lib/commands/status' ); - - function chalkCallsFake() { - return ( ...args ) => args.join( ',' ); - } } ); afterEach( () => { sinon.restore(); + mockery.deregisterAll(); mockery.disable(); } ); + describe( '#helpMessage', () => { + it( 'defines help screen', () => { + expect( statusCommand.helpMessage ).is.a( 'string' ); + } ); + } ); + + describe( '#name', () => { + it( 'returns a full name of executed command', () => { + expect( statusCommand.name ).is.a( 'string' ); + } ); + } ); + describe( 'beforeExecute()', () => { it( 'should describe why logs are not display in "real-time"', () => { const logStub = sinon.stub( console, 'log' ); @@ -113,7 +123,7 @@ describe( 'commands/status', () => { } } ); - return statusCommand.execute( data ) + return statusCommand.execute( commandData ) .then( () => { throw new Error( 'Supposed to be rejected.' ); @@ -138,7 +148,7 @@ describe( 'commands/status', () => { stubs.gitStatusParser.returns( { response: 'Parsed response.' } ); - return statusCommand.execute( data ) + return statusCommand.execute( commandData ) .then( statusResponse => { expect( stubs.execCommand.execute.calledTwice ).to.equal( true ); expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( @@ -160,14 +170,14 @@ describe( 'commands/status', () => { } ); function getCommandArguments( command ) { - return Object.assign( {}, data, { + return Object.assign( {}, commandData, { arguments: [ command ] } ); } } ); it( 'does not modify the package name if "packagesPrefix" option is not specified', () => { - delete data.options.packagesPrefix; + delete commandData.mgitOptions.packagesPrefix; stubs.execCommand.execute.onFirstCall().resolves( { logs: { @@ -182,7 +192,7 @@ describe( 'commands/status', () => { stubs.gitStatusParser.returns( { response: 'Parsed response.' } ); - return statusCommand.execute( data ) + return statusCommand.execute( commandData ) .then( statusResponse => { expect( statusResponse.response ).to.deep.equal( { packageName: '@ckeditor/ckeditor5-test-package', diff --git a/tests/commands/update.js b/tests/commands/update.js index 0f8224f..45fb0b7 100644 --- a/tests/commands/update.js +++ b/tests/commands/update.js @@ -14,7 +14,7 @@ const mockery = require( 'mockery' ); const expect = require( 'chai' ).expect; describe( 'commands/update', () => { - let updateCommand, stubs, data; + let updateCommand, stubs, commandData; beforeEach( () => { mockery.enable( { @@ -39,9 +39,10 @@ describe( 'commands/update', () => { } }; - data = { + commandData = { + arguments: [], packageName: 'test-package', - options: { + mgitOptions: { cwd: __dirname, packages: 'packages' }, @@ -60,17 +61,26 @@ describe( 'commands/update', () => { afterEach( () => { sinon.restore(); + mockery.deregisterAll(); mockery.disable(); } ); + describe( '#helpMessage', () => { + it( 'defines help screen', () => { + expect( updateCommand.helpMessage ).is.a( 'string' ); + } ); + } ); + describe( 'execute()', () => { it( 'clones a package if is not available', () => { + commandData.arguments.push( '--recursive' ); + stubs.fs.existsSync.returns( false ); stubs.bootstrapCommand.execute.returns( Promise.resolve( { logs: getCommandLogs( 'Cloned.' ) } ) ); - return updateCommand.execute( data ) + return updateCommand.execute( commandData ) .then( response => { expect( response.logs.info ).to.deep.equal( [ 'Package "test-package" was not found. Cloning...', @@ -78,6 +88,7 @@ describe( 'commands/update', () => { ] ); expect( stubs.bootstrapCommand.execute.calledOnce ).to.equal( true ); + expect( stubs.bootstrapCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( commandData ) } ); } ); @@ -106,7 +117,7 @@ describe( 'commands/update', () => { logs: getCommandLogs( 'Already up-to-date.' ) } ) ); - return updateCommand.execute( data ) + return updateCommand.execute( commandData ) .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' ); @@ -132,7 +143,7 @@ describe( 'commands/update', () => { logs: getCommandLogs( ' M first-file.js\n ?? second-file.js' ) } ) ); - return updateCommand.execute( data ) + return updateCommand.execute( commandData ) .then( () => { throw new Error( 'Supposed to be rejected.' ); @@ -149,7 +160,7 @@ describe( 'commands/update', () => { it( 'does not pull the changes if detached on a commit or a tag', () => { stubs.fs.existsSync.returns( true ); - data.repository.branch = '1a0ff0a2ee60549656177cd2a18b057764ec2146'; + commandData.repository.branch = '1a0ff0a'; const exec = stubs.execCommand.execute; @@ -162,21 +173,21 @@ describe( 'commands/update', () => { } ) ); exec.onCall( 2 ).returns( Promise.resolve( { - logs: getCommandLogs( 'Note: checking out \'1a0ff0a2ee60549656177cd2a18b057764ec2146\'.' ) + logs: getCommandLogs( 'Note: checking out \'1a0ff0a\'.' ) } ) ); exec.onCall( 3 ).returns( Promise.resolve( { logs: getCommandLogs( [ - '* (HEAD detached at 1a0ff0a2ee60549656177cd2a18b057764ec2146)', + '* (HEAD detached at 1a0ff0a)', ' master', ' remotes/origin/master' ].join( '\n' ) ) } ) ); - return updateCommand.execute( data ) + return updateCommand.execute( commandData ) .then( response => { expect( response.logs.info ).to.deep.equal( [ - 'Note: checking out \'1a0ff0a2ee60549656177cd2a18b057764ec2146\'.', + 'Note: checking out \'1a0ff0a\'.', 'Package "test-package" is on a detached commit.' ] ); @@ -187,7 +198,7 @@ describe( 'commands/update', () => { it( 'aborts if user wants to pull changes from non-existing branch', () => { stubs.fs.existsSync.returns( true ); - data.repository.branch = 'develop'; + commandData.repository.branch = 'develop'; const exec = stubs.execCommand.execute; @@ -211,7 +222,7 @@ describe( 'commands/update', () => { logs: getCommandLogs( 'fatal: Couldn\'t find remote ref develop', true ) } ) ); - return updateCommand.execute( data ) + return updateCommand.execute( commandData ) .then( () => { throw new Error( 'Supposed to be rejected.' ); @@ -232,7 +243,7 @@ describe( 'commands/update', () => { it( 'aborts if user wants to check out to non-existing branch', () => { stubs.fs.existsSync.returns( true ); - data.repository.branch = 'non-existing-branch'; + commandData.repository.branch = 'non-existing-branch'; const exec = stubs.execCommand.execute; @@ -248,7 +259,7 @@ describe( 'commands/update', () => { logs: getCommandLogs( 'error: pathspec \'ggdfgd\' did not match any file(s) known to git.', true ), } ) ); - return updateCommand.execute( data ) + return updateCommand.execute( commandData ) .then( () => { throw new Error( 'Supposed to be rejected.' ); From c38b4f87884d398e3d86e5e081449ed8e3fd9a11 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 20 Aug 2018 08:08:18 +0200 Subject: [PATCH 23/38] Code style. --- index.js | 40 ++++++++++++++++++--------------- lib/commands/exec.js | 2 +- lib/commands/save.js | 4 ++-- lib/utils/displaylog.js | 2 +- lib/utils/getcommandinstance.js | 2 +- tests/commands/update.js | 2 +- 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/index.js b/index.js index 88fed4d..31a0bbe 100755 --- a/index.js +++ b/index.js @@ -36,7 +36,6 @@ const mgitLogo = ` `; const { - italic: i, cyan: c, gray: g, magenta: m, @@ -94,27 +93,32 @@ const cli = meow( ` ${ mgitLogo } ${ g( 'Default: null' ) } `, meowOptions ); -const commandName = cli.input[ 0 ]; +handleCli(); -// If a user wants to see "help" screen. -if ( !commandName || cli.flags.help ) { - // Checks whether specified a command. If not, displays default help screen. - // Buf if the command is available, displays the command's help. +function handleCli() { + const commandName = cli.input[ 0 ]; + + // If user specified a command and `--help` flag wasn't active. + if ( commandName && !cli.flags.help ) { + return mgit( cli.input, cli.flags ); + } + + // A user wants to see "help" screen. + // Missing command. Displays help screen for the entire Mgit. if ( !commandName ) { - cli.showHelp( 0 ); - } else { - const commandInstance = getCommandInstance( commandName ); + return cli.showHelp( 0 ); + } - if ( !commandInstance ) { - process.errorCode = 1; + const commandInstance = getCommandInstance( commandName ); - return; - } + if ( !commandInstance ) { + process.errorCode = 1; - console.log( mgitLogo ); - console.log( ` ${ u( 'Command:' ) } ${ c( commandInstance.name || commandName ) } `); - console.log( commandInstance.helpMessage ); + return; } -} else { - mgit( cli.input, cli.flags ); + + // Specified command is is available, displays the command's help. + console.log( mgitLogo ); + console.log( ` ${ u( 'Command:' ) } ${ c( commandInstance.name || commandName ) } ` ); + console.log( commandInstance.helpMessage ); } diff --git a/lib/commands/exec.js b/lib/commands/exec.js index de0dee9..a3caf62 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -22,7 +22,7 @@ module.exports = { ${ u( 'Description:' ) } Requires a command that will be executed on all repositories. E.g. "${ g( 'mgit exec pwd' ) }" will execute "${ i( 'pwd' ) }" command in every repository. Commands that contain spaces must be wrapped in quotation marks, - e.g.: "${ g( 'mgit exec \"git remote\"' ) }". + e.g.: "${ g( 'mgit exec "git remote"' ) }". `; }, diff --git a/lib/commands/save.js b/lib/commands/save.js index 5a60907..5955347 100644 --- a/lib/commands/save.js +++ b/lib/commands/save.js @@ -39,7 +39,7 @@ module.exports = { beforeExecute( args ) { const options = this._parseArguments( args ); - if ( !options.branch && !options.hash) { + if ( !options.branch && !options.hash ) { throw new Error( 'Need to specify what kind of information you want to save. Call "mgit save -h" in order to read more.' ); } }, @@ -57,7 +57,7 @@ module.exports = { /* istanbul ignore else */ if ( options.branch ) { - promise = execCommand.execute( getExecData( 'git status --branch --porcelain' ) ) + promise = execCommand.execute( getExecData( 'git status --branch --porcelain' ) ) .then( execResponse => gitStatusParser( execResponse.logs.info[ 0 ] ).branch ); } else if ( options.hash ) { promise = execCommand.execute( getExecData( 'git rev-parse HEAD' ) ) diff --git a/lib/utils/displaylog.js b/lib/utils/displaylog.js index 53c063b..56104b7 100644 --- a/lib/utils/displaylog.js +++ b/lib/utils/displaylog.js @@ -18,7 +18,7 @@ const chalk = require( 'chalk' ); * @param {Boolean} [options.skipCounter=false] A flag that allows hiding the progress bar. */ module.exports = function displayLog( packageName, logs, options ) { - let infoLogs = logs.info.filter( l => l.length ).join( '\n' ).trim(); + const infoLogs = logs.info.filter( l => l.length ).join( '\n' ).trim(); const errorLogs = logs.error.filter( l => l.length ).join( '\n' ).trim(); const progressPercentage = Math.round( ( options.current / options.all ) * 100 ); diff --git a/lib/utils/getcommandinstance.js b/lib/utils/getcommandinstance.js index 3cacb06..19ef413 100644 --- a/lib/utils/getcommandinstance.js +++ b/lib/utils/getcommandinstance.js @@ -20,7 +20,7 @@ const COMMAND_ALIASES = { module.exports = function getCommandInstance( commandName ) { try { // Find full command name if used an alias or just use specified name. - const resolvedCommandName = (COMMAND_ALIASES[ commandName ] || commandName).replace( /-/g, '' ); + const resolvedCommandName = ( COMMAND_ALIASES[ commandName ] || commandName ).replace( /-/g, '' ); const commandPath = require.resolve( '../commands/' + resolvedCommandName ); const commandInstance = require( commandPath ); diff --git a/tests/commands/update.js b/tests/commands/update.js index 45fb0b7..46c4d5d 100644 --- a/tests/commands/update.js +++ b/tests/commands/update.js @@ -88,7 +88,7 @@ describe( 'commands/update', () => { ] ); expect( stubs.bootstrapCommand.execute.calledOnce ).to.equal( true ); - expect( stubs.bootstrapCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( commandData ) + expect( stubs.bootstrapCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( commandData ); } ); } ); From a3812ed7cc4adcfbdd2a3bf8c4578057dcccf9ff Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 20 Aug 2018 15:04:02 +0200 Subject: [PATCH 24/38] New commands: fetch, pull, push. --- lib/commands/fetch.js | 90 +++++++++++++++++++++++ lib/commands/pull.js | 80 +++++++++++++++++++++ lib/commands/push.js | 89 +++++++++++++++++++++++ lib/utils/log.js | 1 + tests/commands/fetch.js | 155 ++++++++++++++++++++++++++++++++++++++++ tests/commands/pull.js | 147 +++++++++++++++++++++++++++++++++++++ tests/commands/push.js | 155 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 717 insertions(+) create mode 100644 lib/commands/fetch.js create mode 100644 lib/commands/pull.js create mode 100644 lib/commands/push.js create mode 100644 tests/commands/fetch.js create mode 100644 tests/commands/pull.js create mode 100644 tests/commands/push.js diff --git a/lib/commands/fetch.js b/lib/commands/fetch.js new file mode 100644 index 0000000..9ce3c78 --- /dev/null +++ b/lib/commands/fetch.js @@ -0,0 +1,90 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const fs = require( 'fs' ); +const path = require( 'upath' ); +const chalk = require( 'chalk' ); +const buildOptions = require( 'minimist-options' ); +const minimist = require( 'minimist' ); + +module.exports = { + get helpMessage() { + const { + italic: i, + gray: g, + magenta: m, + underline: u + } = chalk; + + return ` + ${ u( 'Description:' ) } + Download objects and refs from the remote repository. If some package is missed, the command will not be executed. + For cloned repositories this command is a shorthand for: "${ i( 'mgit exec \'git fetch\'' ) }". + + ${ u( 'Options:' ) } + ${ m( '--prune' ) } (-p) Before fetching, remove any remote-tracking references that + no longer exist on the remote. + ${ g( 'Default: false' ) } + `; + }, + + /** + * @param {CommandData} data + * @returns {Promise} + */ + execute( data ) { + const log = require( '../utils/log' )(); + const execCommand = require( './exec' ); + + const destinationPath = path.join( data.mgitOptions.packages, data.repository.directory ); + + // Package is not cloned. + if ( !fs.existsSync( destinationPath ) ) { + log.info( `Package "${ data.packageName }" was not found. Skipping...` ); + + return Promise.resolve( { + logs: log.all() + } ); + } + + const options = this._parseArguments( data.arguments ); + let command = 'git fetch'; + + if ( options.prune ) { + command += ' -p'; + } + + return execCommand.execute( getExecData( command ) ); + + function getExecData( command ) { + return Object.assign( {}, data, { + arguments: [ command ] + } ); + } + }, + + /** + * @param {Set} parsedPackages Collection of processed packages. + */ + afterExecute( parsedPackages ) { + console.log( chalk.cyan( `${ parsedPackages.size } packages have been processed.` ) ); + }, + + /** + * @private + * @param {Array.} argv List of arguments provided by the user via CLI. + * @returns {Object} + */ + _parseArguments( argv ) { + return minimist( argv, buildOptions( { + prune: { + type: 'boolean', + alias: 'p', + } + } ) ); + } +}; diff --git a/lib/commands/pull.js b/lib/commands/pull.js new file mode 100644 index 0000000..a08ca03 --- /dev/null +++ b/lib/commands/pull.js @@ -0,0 +1,80 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const fs = require( 'fs' ); +const path = require( 'upath' ); +const chalk = require( 'chalk' ); + +module.exports = { + get helpMessage() { + const { + italic: i, + gray: g, + magenta: m, + underline: u + } = chalk; + + return ` + ${ u( 'Description:' ) } + Pull changes in all packages. If some package is missed, "${ i( 'bootstrap' ) }" command is calling on the missing package. + For cloned repositories this command is a shorthand for: "${ i( 'mgit exec \'git pull\'' ) }". + + ${ u( 'Options:' ) } + ${ m( '--recursive' ) } Whether to install dependencies recursively. Only packages matching these + patterns will be cloned recursively. + ${ g( 'Default: false' ) } + `; + }, + + /** + * @param {CommandData} data + * @returns {Promise} + */ + execute( data ) { + const log = require( '../utils/log' )(); + const bootstrapCommand = require( './bootstrap' ); + const execCommand = require( './exec' ); + + const destinationPath = path.join( data.mgitOptions.packages, data.repository.directory ); + + // Package is not cloned. + if ( !fs.existsSync( destinationPath ) ) { + log.info( `Package "${ data.packageName }" was not found. Cloning...` ); + + const bootstrapOptions = { + arguments: data.arguments, + mgitOptions: data.mgitOptions, + packageName: data.packageName, + repository: data.repository + }; + + return bootstrapCommand.execute( bootstrapOptions ) + .then( response => { + log.concat( response.logs ); + + response.logs = log.all(); + + return Promise.resolve( response ); + } ); + } + + return execCommand.execute( getExecData( 'git pull' ) ); + + function getExecData( command ) { + return Object.assign( {}, data, { + arguments: [ command ] + } ); + } + }, + + /** + * @param {Set} parsedPackages Collection of processed packages. + */ + afterExecute( parsedPackages ) { + console.log( chalk.cyan( `${ parsedPackages.size } packages have been processed.` ) ); + } +}; diff --git a/lib/commands/push.js b/lib/commands/push.js new file mode 100644 index 0000000..94adfeb --- /dev/null +++ b/lib/commands/push.js @@ -0,0 +1,89 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const fs = require( 'fs' ); +const path = require( 'upath' ); +const chalk = require( 'chalk' ); +const buildOptions = require( 'minimist-options' ); +const minimist = require( 'minimist' ); + +module.exports = { + get helpMessage() { + const { + italic: i, + gray: g, + magenta: m, + underline: u + } = chalk; + + return ` + ${ u( 'Description:' ) } + Push changes in all packages. If some package is missed, the command will not be executed. + For cloned repositories this command is a shorthand for: "${ i( 'mgit exec \'git push\'' ) }". + + ${ u( 'Options:' ) } + ${ m( '--set-upstream' ) } (-u) Whether to set upstream for git pull/status. + ${ g( 'Default: false' ) } + `; + }, + + /** + * @param {CommandData} data + * @returns {Promise} + */ + execute( data ) { + const log = require( '../utils/log' )(); + const execCommand = require( './exec' ); + + const destinationPath = path.join( data.mgitOptions.packages, data.repository.directory ); + + // Package is not cloned. + if ( !fs.existsSync( destinationPath ) ) { + log.info( `Package "${ data.packageName }" was not found. Skipping...` ); + + return Promise.resolve( { + logs: log.all() + } ); + } + + const options = this._parseArguments( data.arguments ); + let command = 'git push'; + + if ( options[ 'set-upstream' ] ) { + command += ' -u'; + } + + return execCommand.execute( getExecData( command ) ); + + function getExecData( command ) { + return Object.assign( {}, data, { + arguments: [ command ] + } ); + } + }, + + /** + * @param {Set} parsedPackages Collection of processed packages. + */ + afterExecute( parsedPackages ) { + console.log( chalk.cyan( `${ parsedPackages.size } packages have been processed.` ) ); + }, + + /** + * @private + * @param {Array.} argv List of arguments provided by the user via CLI. + * @returns {Object} + */ + _parseArguments( argv ) { + return minimist( argv, buildOptions( { + 'set-upstream': { + type: 'boolean', + alias: 'u', + } + } ) ); + } +}; diff --git a/lib/utils/log.js b/lib/utils/log.js index d77e9d5..f04d5d0 100644 --- a/lib/utils/log.js +++ b/lib/utils/log.js @@ -31,6 +31,7 @@ module.exports = function log() { msg = msg.trim(); + /* istanbul ignore if */ if ( !msg ) { return; } diff --git a/tests/commands/fetch.js b/tests/commands/fetch.js new file mode 100644 index 0000000..4f806f9 --- /dev/null +++ b/tests/commands/fetch.js @@ -0,0 +1,155 @@ +/** + * @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( 'upath' ); +const sinon = require( 'sinon' ); +const mockery = require( 'mockery' ); +const expect = require( 'chai' ).expect; + +describe( 'commands/fetch', () => { + let fetchCommand, stubs, commandData; + + beforeEach( () => { + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + exec: sinon.stub(), + fs: { + existsSync: sinon.stub( fs, 'existsSync' ) + }, + path: { + join: sinon.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ) + }, + execCommand: { + execute: sinon.stub() + } + }; + + commandData = { + arguments: [], + packageName: 'test-package', + mgitOptions: { + cwd: __dirname, + packages: 'packages' + }, + repository: { + directory: 'test-package', + url: 'git@github.com/organization/test-package.git', + branch: 'master' + } + }; + + mockery.registerMock( './exec', stubs.execCommand ); + + fetchCommand = require( '../../lib/commands/fetch' ); + } ); + + afterEach( () => { + sinon.restore(); + mockery.deregisterAll(); + mockery.disable(); + } ); + + describe( '#helpMessage', () => { + it( 'defines help screen', () => { + expect( fetchCommand.helpMessage ).is.a( 'string' ); + } ); + } ); + + describe( 'execute()', () => { + it( 'skips a package if is not available', () => { + stubs.fs.existsSync.returns( false ); + + return fetchCommand.execute( commandData ) + .then( response => { + expect( response.logs.info ).to.deep.equal( [ + 'Package "test-package" was not found. Skipping...', + ] ); + } ); + } ); + + it( 'resolves promise after pushing the changes', () => { + stubs.fs.existsSync.returns( true ); + + const exec = stubs.execCommand.execute; + + exec.returns( Promise.resolve( { + logs: getCommandLogs( 'remote: Counting objects: 254, done.' ) + } ) ); + + return fetchCommand.execute( commandData ) + .then( response => { + expect( exec.callCount ).to.equal( 1 ); + expect( exec.firstCall.args[ 0 ].arguments[ 0 ] ).to.equal( 'git fetch' ); + + expect( response.logs.info ).to.deep.equal( [ + 'remote: Counting objects: 254, done.' + ] ); + } ); + } ); + + it( 'allows removing remote-tracking references that no longer exist', () => { + commandData.arguments.push( '--prune' ); + stubs.fs.existsSync.returns( true ); + + const exec = stubs.execCommand.execute; + + exec.returns( Promise.resolve( { + logs: getCommandLogs( 'remote: Counting objects: 254, done.' ) + } ) ); + + return fetchCommand.execute( commandData ) + .then( response => { + expect( exec.callCount ).to.equal( 1 ); + expect( exec.firstCall.args[ 0 ].arguments[ 0 ] ).to.equal( 'git fetch -p' ); + + expect( response.logs.info ).to.deep.equal( [ + 'remote: Counting objects: 254, done.' + ] ); + } ); + } ); + } ); + + describe( 'afterExecute()', () => { + it( 'informs about number of processed packages', () => { + const consoleLog = sinon.stub( console, 'log' ); + + const processedPackages = new Set(); + processedPackages.add( 'package-1' ); + processedPackages.add( 'package-2' ); + + fetchCommand.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/commands/pull.js b/tests/commands/pull.js new file mode 100644 index 0000000..8f2f127 --- /dev/null +++ b/tests/commands/pull.js @@ -0,0 +1,147 @@ +/** + * @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( 'upath' ); +const sinon = require( 'sinon' ); +const mockery = require( 'mockery' ); +const expect = require( 'chai' ).expect; + +describe( 'commands/pull', () => { + let pullCommand, stubs, commandData; + + beforeEach( () => { + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + exec: sinon.stub(), + fs: { + existsSync: sinon.stub( fs, 'existsSync' ) + }, + path: { + join: sinon.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ) + }, + bootstrapCommand: { + execute: sinon.stub() + }, + execCommand: { + execute: sinon.stub() + } + }; + + commandData = { + arguments: [], + packageName: 'test-package', + mgitOptions: { + 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 ); + + pullCommand = require( '../../lib/commands/pull' ); + } ); + + afterEach( () => { + sinon.restore(); + mockery.deregisterAll(); + mockery.disable(); + } ); + + describe( '#helpMessage', () => { + it( 'defines help screen', () => { + expect( pullCommand.helpMessage ).is.a( 'string' ); + } ); + } ); + + describe( 'execute()', () => { + it( 'clones a package if is not available', () => { + commandData.arguments.push( '--recursive' ); + + stubs.fs.existsSync.returns( false ); + stubs.bootstrapCommand.execute.returns( Promise.resolve( { + logs: getCommandLogs( 'Cloned.' ) + } ) ); + + return pullCommand.execute( commandData ) + .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 ); + expect( stubs.bootstrapCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( commandData ); + } ); + } ); + + it( 'resolves promise after pulling the changes', () => { + stubs.fs.existsSync.returns( true ); + + const exec = stubs.execCommand.execute; + + exec.returns( Promise.resolve( { + logs: getCommandLogs( 'Already up-to-date.' ) + } ) ); + + return pullCommand.execute( commandData ) + .then( response => { + expect( exec.callCount ).to.equal( 1 ); + expect( exec.firstCall.args[ 0 ].arguments[ 0 ] ).to.equal( 'git pull' ); + + expect( response.logs.info ).to.deep.equal( [ + 'Already up-to-date.' + ] ); + } ); + } ); + } ); + + describe( 'afterExecute()', () => { + it( 'informs about number of processed packages', () => { + const consoleLog = sinon.stub( console, 'log' ); + + const processedPackages = new Set(); + processedPackages.add( 'package-1' ); + processedPackages.add( 'package-2' ); + + pullCommand.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/commands/push.js b/tests/commands/push.js new file mode 100644 index 0000000..d8536ae --- /dev/null +++ b/tests/commands/push.js @@ -0,0 +1,155 @@ +/** + * @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( 'upath' ); +const sinon = require( 'sinon' ); +const mockery = require( 'mockery' ); +const expect = require( 'chai' ).expect; + +describe( 'commands/push', () => { + let pushCommand, stubs, commandData; + + beforeEach( () => { + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + exec: sinon.stub(), + fs: { + existsSync: sinon.stub( fs, 'existsSync' ) + }, + path: { + join: sinon.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ) + }, + execCommand: { + execute: sinon.stub() + } + }; + + commandData = { + arguments: [], + packageName: 'test-package', + mgitOptions: { + cwd: __dirname, + packages: 'packages' + }, + repository: { + directory: 'test-package', + url: 'git@github.com/organization/test-package.git', + branch: 'master' + } + }; + + mockery.registerMock( './exec', stubs.execCommand ); + + pushCommand = require( '../../lib/commands/push' ); + } ); + + afterEach( () => { + sinon.restore(); + mockery.deregisterAll(); + mockery.disable(); + } ); + + describe( '#helpMessage', () => { + it( 'defines help screen', () => { + expect( pushCommand.helpMessage ).is.a( 'string' ); + } ); + } ); + + describe( 'execute()', () => { + it( 'skips a package if is not available', () => { + stubs.fs.existsSync.returns( false ); + + return pushCommand.execute( commandData ) + .then( response => { + expect( response.logs.info ).to.deep.equal( [ + 'Package "test-package" was not found. Skipping...', + ] ); + } ); + } ); + + it( 'resolves promise after pushing the changes', () => { + stubs.fs.existsSync.returns( true ); + + const exec = stubs.execCommand.execute; + + exec.returns( Promise.resolve( { + logs: getCommandLogs( 'Everything up-to-date' ) + } ) ); + + return pushCommand.execute( commandData ) + .then( response => { + expect( exec.callCount ).to.equal( 1 ); + expect( exec.firstCall.args[ 0 ].arguments[ 0 ] ).to.equal( 'git push' ); + + expect( response.logs.info ).to.deep.equal( [ + 'Everything up-to-date' + ] ); + } ); + } ); + + it( 'allows set upstream when pushing', () => { + commandData.arguments.push( '--set-upstream' ); + stubs.fs.existsSync.returns( true ); + + const exec = stubs.execCommand.execute; + + exec.returns( Promise.resolve( { + logs: getCommandLogs( 'Everything up-to-date' ) + } ) ); + + return pushCommand.execute( commandData ) + .then( response => { + expect( exec.callCount ).to.equal( 1 ); + expect( exec.firstCall.args[ 0 ].arguments[ 0 ] ).to.equal( 'git push -u' ); + + expect( response.logs.info ).to.deep.equal( [ + 'Everything up-to-date' + ] ); + } ); + } ); + } ); + + describe( 'afterExecute()', () => { + it( 'informs about number of processed packages', () => { + const consoleLog = sinon.stub( console, 'log' ); + + const processedPackages = new Set(); + processedPackages.add( 'package-1' ); + processedPackages.add( 'package-2' ); + + pushCommand.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 096f2916d09f0db8f7920f9fffb76c244e2befee Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 20 Aug 2018 15:04:32 +0200 Subject: [PATCH 25/38] CLI core is closed in function now. --- index.js | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/index.js b/index.js index 31a0bbe..d5d5b16 100755 --- a/index.js +++ b/index.js @@ -12,19 +12,22 @@ const meow = require( 'meow' ); const mgit = require( './lib/index' ); const getCommandInstance = require( './lib/utils/getcommandinstance' ); -const meowOptions = { - autoHelp: false, - flags: { - version: { - alias: 'v' - }, - help: { - alias: 'h' +handleCli(); + +function handleCli() { + const meowOptions = { + autoHelp: false, + flags: { + version: { + alias: 'v' + }, + help: { + alias: 'h' + } } - } -}; + }; -const mgitLogo = ` + const mgitLogo = ` _ _ (_) | _ __ ___ __ _ _| |_ @@ -35,15 +38,15 @@ const mgitLogo = ` |___/ `; -const { - cyan: c, - gray: g, - magenta: m, - underline: u, - yellow: y, -} = chalk; + const { + cyan: c, + gray: g, + magenta: m, + underline: u, + yellow: y, + } = chalk; -const cli = meow( ` ${ mgitLogo } + const cli = meow( `${ mgitLogo } ${ u( 'Usage:' ) } $ mgit ${ c( 'command' ) } ${ y( '[--options]' ) } -- ${ m( '[--command-options]' ) } @@ -53,10 +56,13 @@ const cli = meow( ` ${ mgitLogo } ${ c( 'commit' ) } Commits all changes. A shorthand for "mgit exec 'git commit -a'". ${ c( 'diff' ) } Prints changes from packages where something has changed. ${ c( 'exec' ) } Executes shell command in each package. + ${ c( 'fetch' ) } Fetches existing repositories. ${ c( 'merge' ) } Merges specified branch with the current one. + ${ c( 'pull' ) } Pulls changes in existing repositories and clones missing ones. + ${ c( 'push' ) } Pushes changes in existing repositories to remotes. ${ c( 'save-hashes' ) } Saves hashes of packages in mgit.json. It allows to easily fix project to a specific state. ${ c( 'status' ) } Prints a table which contains useful information about the status of repositories. - ${ c( 'update' ) } Updates packages to the latest versions (i.e. pull changes). + ${ c( 'update' ) } Updates packages to the latest versions (pull changes and check out to proper branch). ${ u( 'Options:' ) } ${ y( '--packages' ) } Directory to which all repositories will be cloned. @@ -93,9 +99,6 @@ const cli = meow( ` ${ mgitLogo } ${ g( 'Default: null' ) } `, meowOptions ); -handleCli(); - -function handleCli() { const commandName = cli.input[ 0 ]; // If user specified a command and `--help` flag wasn't active. From f17a9cee468aa7ff95c33121f345d478b29060c8 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 20 Aug 2018 15:22:43 +0200 Subject: [PATCH 26/38] Improve README. --- README.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++------- index.js | 2 +- 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 534b08c..2337d07 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,7 @@ packages/ CLI options: ``` ---recursive Whether to install dependencies recursively. Only packages matching - these patterns will be cloned recursively. - Default: false. - ---packages Directory to which all repositories will be cloned. +--packages Directory to which all repositories will be cloned or are already installed. Default: '/packages/' --resolver-path Path to a custom repository resolver function. @@ -134,6 +130,8 @@ Examples: ### Recursive cloning +**Note**: `--recursive` option is a commands option, so remember about [`--`](https://unix.stackexchange.com/questions/147143/when-and-how-was-the-double-dash-introduced-as-an-end-of-options-delimiter) in order to separate options for mgit and specified command. + When the `--recursive` option is used mgit will clone repositories recursively. First, it will clone the `dependencies` specified in `mgit.json` and, then, their `dependencies` and `devDependencies` specified in `package.json` files located in cloned repositories. However, mgit needs to know repository URLs of those dependencies, as well as which dependencies to clone (usually, only the ones maintained by you). In order to configure that you need to use a custom repository resolver (`--resolver-path`). @@ -151,7 +149,7 @@ const parseRepositoryUrl = require( 'mgit2/lib/utils/parserepositoryurl' ); * Resolves repository URL for a given package name. * * @param {String} packageName Package name. - * @param {Options} data.options The options object. + * @param {Options} options The options object. * @returns {Repository|null} */ module.exports = function resolver( packageName, options ) { @@ -187,7 +185,7 @@ You can also use full HTTPS URLs to configure `dependencies` in your `mgit.json` $ mgit [command] ``` -For displaying help screen for commands, type: +For displaying help screen for specified command, type: ```bash $ mgit [command] --help @@ -202,7 +200,7 @@ This command will not change existing repositories, so you can always safely use Example: ```bash -mgit bootstrap --recursive --resolver=path ./dev/custom-repository-resolver.js +mgit bootstrap --resolver=path ./dev/custom-repository-resolver.js -- --recursive ``` ### update @@ -216,7 +214,43 @@ This command does not touch repositories in which there are uncommitted changes. Examples: ```bash -mgit update --recursive +mgit update -- --recursive +``` + +### pull + +Pulls changes in existing repositories. + +If any dependency is missing, the command will install it too. + +Examples: + +```bash +mgit pull -- --recursive +``` + +### push + +Pushes changes in existing repositories. + +If any dependency is missing, the command will not be executed. + +Examples: + +```bash +mgit push +``` + +### fetch + +Fetches changes in existing repositories. + +If any dependency is missing, the command will not be executed. + +Examples: + +```bash +mgit fetch ``` ### exec @@ -240,14 +274,50 @@ mgit exec 'echo `pwd`' # /home/mgit/packages/organization/repository-2 ``` -### save-hashes +### commit (alias: `ci`) + +For every repository that contains changes which can be committed, makes a commit with these files. +You need to specify the message for the commit. + +Example: + +```bash +mgit commit -- --message 'Introduce PULL_REQUEST_TEMPLATE.md.' + +# Executes `git commit --message 'Introduce PULL_REQUEST_TEMPLATE.md.'` command on each repository. +# Commit will be made in repositories that "git status" returns a list if changed files (these files must be tracked by Git). +``` + +### merge + +Requires a second argument which is a branch name that will be merged to current one. You can also specify the message +which will be added to the default git-merge message. + +Repositories which do not have specified branch will be ignored. + +Example: + +```bash +# Assumptions: we are on "master" branch and "develop" branch exists. +mgit merge develop -- --message 'These changes are required for the future release.' + +# Branch `develop` will be merged into `master`. +``` + +### save Saves hashes of packages in `mgit.json`. It allows to easily fix project to a specific state. Example: ```bash -mgit save-hashes +mgit save +``` + +If you would like to save name of branches instead of current commit, you can use an option `--branch`: + +```bash +mgit save -- --branch ``` ### status (alias: `st`) diff --git a/index.js b/index.js index d5d5b16..ce1ab58 100755 --- a/index.js +++ b/index.js @@ -65,7 +65,7 @@ function handleCli() { ${ c( 'update' ) } Updates packages to the latest versions (pull changes and check out to proper branch). ${ u( 'Options:' ) } - ${ y( '--packages' ) } Directory to which all repositories will be cloned. + ${ y( '--packages' ) } Directory to which all repositories will be cloned or are already installed. ${ g( 'Default: \'/packages/\'' ) } ${ y( '--resolver-path' ) } Path to a custom repository resolver function. From 28f776499feaa272f12ceea4e86d1a08fe53329c Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 20 Aug 2018 15:42:45 +0200 Subject: [PATCH 27/38] Fix istanbul command. --- lib/commands/merge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/merge.js b/lib/commands/merge.js index 8d72409..f772236 100644 --- a/lib/commands/merge.js +++ b/lib/commands/merge.js @@ -92,7 +92,7 @@ module.exports = { } } ) ); - /* istanbul ignore else */ + /* istanbul ignore if */ if ( !Array.isArray( options.message ) ) { options.message = [ options.message ].filter( Boolean ); } From c0eabcb1fd7fa6e8bb720e593c3200f66456ce2a Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 20 Aug 2018 15:48:56 +0200 Subject: [PATCH 28/38] Revert "Fix istanbul command." This reverts commit 28f776499feaa272f12ceea4e86d1a08fe53329c. --- lib/commands/merge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/merge.js b/lib/commands/merge.js index f772236..8d72409 100644 --- a/lib/commands/merge.js +++ b/lib/commands/merge.js @@ -92,7 +92,7 @@ module.exports = { } } ) ); - /* istanbul ignore if */ + /* istanbul ignore else */ if ( !Array.isArray( options.message ) ) { options.message = [ options.message ].filter( Boolean ); } From d2758355740ff1993611473943d71058213f6743 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Tue, 21 Aug 2018 12:18:56 +0200 Subject: [PATCH 29/38] Fix command name (s/save-hashes/save/). --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index ce1ab58..894be59 100755 --- a/index.js +++ b/index.js @@ -60,7 +60,7 @@ function handleCli() { ${ c( 'merge' ) } Merges specified branch with the current one. ${ c( 'pull' ) } Pulls changes in existing repositories and clones missing ones. ${ c( 'push' ) } Pushes changes in existing repositories to remotes. - ${ c( 'save-hashes' ) } Saves hashes of packages in mgit.json. It allows to easily fix project to a specific state. + ${ c( 'save' ) } Saves hashes of packages in mgit.json. It allows to easily fix project to a specific state. ${ c( 'status' ) } Prints a table which contains useful information about the status of repositories. ${ c( 'update' ) } Updates packages to the latest versions (pull changes and check out to proper branch). From 0b20605c06277148a510c27111a22efb2d4704b2 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Fri, 24 Aug 2018 14:10:24 +0200 Subject: [PATCH 30/38] "push" command passes all arguments to git command (the same as "diff"). --- lib/commands/push.js | 32 ++++++-------------------------- tests/commands/push.js | 7 ++++--- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/lib/commands/push.js b/lib/commands/push.js index 94adfeb..a02ee16 100644 --- a/lib/commands/push.js +++ b/lib/commands/push.js @@ -8,15 +8,12 @@ const fs = require( 'fs' ); const path = require( 'upath' ); const chalk = require( 'chalk' ); -const buildOptions = require( 'minimist-options' ); -const minimist = require( 'minimist' ); module.exports = { get helpMessage() { const { italic: i, gray: g, - magenta: m, underline: u } = chalk; @@ -26,8 +23,10 @@ module.exports = { For cloned repositories this command is a shorthand for: "${ i( 'mgit exec \'git push\'' ) }". ${ u( 'Options:' ) } - ${ m( '--set-upstream' ) } (-u) Whether to set upstream for git pull/status. - ${ g( 'Default: false' ) } + All options accepted by "${ i( 'git push' ) }" are supported by mgit. Everything specified after "--" is passed directly to the + "${ i( 'git push' ) }" command. + + E.g.: "${ g( 'mgit push -- --verbose --all' ) }" will execute "${ i( 'git push --verbose --all' ) }" `; }, @@ -50,14 +49,9 @@ module.exports = { } ); } - const options = this._parseArguments( data.arguments ); - let command = 'git push'; + const pushCommand = ( 'git push ' + data.arguments.join( ' ' ) ).trim(); - if ( options[ 'set-upstream' ] ) { - command += ' -u'; - } - - return execCommand.execute( getExecData( command ) ); + return execCommand.execute( getExecData( pushCommand ) ); function getExecData( command ) { return Object.assign( {}, data, { @@ -71,19 +65,5 @@ module.exports = { */ afterExecute( parsedPackages ) { console.log( chalk.cyan( `${ parsedPackages.size } packages have been processed.` ) ); - }, - - /** - * @private - * @param {Array.} argv List of arguments provided by the user via CLI. - * @returns {Object} - */ - _parseArguments( argv ) { - return minimist( argv, buildOptions( { - 'set-upstream': { - type: 'boolean', - alias: 'u', - } - } ) ); } }; diff --git a/tests/commands/push.js b/tests/commands/push.js index d8536ae..037ba3b 100644 --- a/tests/commands/push.js +++ b/tests/commands/push.js @@ -99,8 +99,9 @@ describe( 'commands/push', () => { } ); } ); - it( 'allows set upstream when pushing', () => { - commandData.arguments.push( '--set-upstream' ); + it( 'allows modifying the "git push" command', () => { + commandData.arguments.push( '--verbose' ); + commandData.arguments.push( '--all' ); stubs.fs.existsSync.returns( true ); const exec = stubs.execCommand.execute; @@ -112,7 +113,7 @@ describe( 'commands/push', () => { return pushCommand.execute( commandData ) .then( response => { expect( exec.callCount ).to.equal( 1 ); - expect( exec.firstCall.args[ 0 ].arguments[ 0 ] ).to.equal( 'git push -u' ); + expect( exec.firstCall.args[ 0 ].arguments[ 0 ] ).to.equal( 'git push --verbose --all' ); expect( response.logs.info ).to.deep.equal( [ 'Everything up-to-date' From d7c9092b53568f0c500b4adb4c9409299f88e55a Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 10 Sep 2018 14:32:25 +0200 Subject: [PATCH 31/38] Removed bootstrap command. Pull does not clone missing repos anymore. Update was renamed to sync. --- README.md | 18 +- lib/commands/bootstrap.js | 116 ---------- lib/commands/exec.js | 2 +- lib/commands/pull.js | 23 +- lib/commands/{update.js => sync.js} | 90 +++++-- tests/commands/bootstrap.js | 176 -------------- tests/commands/exec.js | 2 +- tests/commands/pull.js | 13 +- tests/commands/sync.js | 348 ++++++++++++++++++++++++++++ tests/commands/update.js | 308 ------------------------ 10 files changed, 429 insertions(+), 667 deletions(-) delete mode 100644 lib/commands/bootstrap.js rename lib/commands/{update.js => sync.js} (64%) delete mode 100644 tests/commands/bootstrap.js create mode 100644 tests/commands/sync.js delete mode 100644 tests/commands/update.js diff --git a/README.md b/README.md index 2337d07..d6bb0ee 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ First, create a configuration file `mgit.json`: (Keys of the `dependencies` object are package names and values are repository URLs (GitHub identifiers in this case). Read more about the [`dependencies` option](#the-dependencies-option).) -And run `mgit bootstrap` to clone all the repositories. By default, they will be cloned to `/packages/` directory: +And run `mgit update` to clone all the repositories. By default, they will be cloned to `/packages/` directory: ```bash packages/ @@ -191,23 +191,11 @@ For displaying help screen for specified command, type: $ mgit [command] --help ``` -### bootstrap - -Installs missing packages (i.e. clone them) and check them out to correct branches. - -This command will not change existing repositories, so you can always safely use it. It's useful to bootstrap the project initially, but later you'll rather want to use `mgit update`. - -Example: - -```bash -mgit bootstrap --resolver=path ./dev/custom-repository-resolver.js -- --recursive -``` - ### update Updates dependencies. Switches repositories to correct branches (specified in `mgit.json`) and pulls changes. -If any dependency is missing, the command will install it too. +If any dependency is missing, the command will install this dependensy as well. This command does not touch repositories in which there are uncommitted changes. @@ -221,7 +209,7 @@ mgit update -- --recursive Pulls changes in existing repositories. -If any dependency is missing, the command will install it too. +If any dependency is missing, the command will not be executed. Examples: diff --git a/lib/commands/bootstrap.js b/lib/commands/bootstrap.js deleted file mode 100644 index b666c2c..0000000 --- a/lib/commands/bootstrap.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const fs = require( 'fs' ); -const path = require( 'upath' ); -const buildOptions = require( 'minimist-options' ); -const minimist = require( 'minimist' ); -const chalk = require( 'chalk' ); -const shell = require( '../utils/shell' ); - -module.exports = { - get helpMessage() { - const { - gray: g, - magenta: m, - underline: u - } = chalk; - - return ` - ${ u( 'Description:' ) } - Installs packages defined in a configuration file. Packages that are already cloned will be skipped. - - ${ u( 'Options:' ) } - ${ m( '--recursive' ) } (-r) Whether to install dependencies recursively. Only packages matching these - patterns will be cloned recursively. - ${ g( 'Default: false' ) } - `; - }, - - beforeExecute() { - console.log( chalk.blue( 'Cloning missing packages...' ) ); - }, - - /** - * @param {CommandData} data - * @returns {Promise} - */ - execute( data ) { - const log = require( '../utils/log' )(); - const options = this._parseArguments( data.arguments ); - const destinationPath = path.join( data.mgitOptions.packages, data.repository.directory ); - - let promise; - - // 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 }"`, - `cd "${ destinationPath }"`, - `git checkout --quiet ${ data.repository.branch }` - ].join( ' && ' ); - - promise = shell( command ); - } - - return promise - .then( output => { - log.info( output ); - - const commandOutput = { - logs: log.all() - }; - - if ( options.recursive ) { - const packageJson = require( path.join( destinationPath, 'package.json' ) ); - const packages = []; - - if ( packageJson.dependencies ) { - packages.push( ...Object.keys( packageJson.dependencies ) ); - } - - if ( packageJson.devDependencies ) { - packages.push( ...Object.keys( packageJson.devDependencies ) ); - } - - commandOutput.packages = packages; - } - - return Promise.resolve( commandOutput ); - } ) - .catch( error => { - log.error( error ); - - return Promise.reject( { logs: log.all() } ); - } ); - }, - - /** - * @param {Set} processedPackages Collection of processed packages. - */ - afterExecute( processedPackages ) { - console.log( chalk.cyan( `${ processedPackages.size } packages have been processed.` ) ); - }, - - /** - * @private - * @param {Array.} argv List of arguments provided by the user via CLI. - * @returns {Object} - */ - _parseArguments( argv ) { - return minimist( argv, buildOptions( { - recursive: { - type: 'boolean', - alias: 'r', - } - } ) ); - } -}; diff --git a/lib/commands/exec.js b/lib/commands/exec.js index a3caf62..3d2fc1b 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -47,7 +47,7 @@ module.exports = { // Package does not exist. if ( !fs.existsSync( newCwd ) ) { - log.error( `Package "${ data.packageName }" is not available. Run "mgit bootstrap" in order to download the package.` ); + log.error( `Package "${ data.packageName }" is not available. Run "mgit sync" in order to download the package.` ); return reject( { logs: log.all() } ); } diff --git a/lib/commands/pull.js b/lib/commands/pull.js index a08ca03..e62785f 100644 --- a/lib/commands/pull.js +++ b/lib/commands/pull.js @@ -20,7 +20,7 @@ module.exports = { return ` ${ u( 'Description:' ) } - Pull changes in all packages. If some package is missed, "${ i( 'bootstrap' ) }" command is calling on the missing package. + Pull changes in all packages. If some package is missed, the command will not be executed. For cloned repositories this command is a shorthand for: "${ i( 'mgit exec \'git pull\'' ) }". ${ u( 'Options:' ) } @@ -36,30 +36,17 @@ module.exports = { */ execute( data ) { const log = require( '../utils/log' )(); - const bootstrapCommand = require( './bootstrap' ); const execCommand = require( './exec' ); const destinationPath = path.join( data.mgitOptions.packages, data.repository.directory ); // Package is not cloned. if ( !fs.existsSync( destinationPath ) ) { - log.info( `Package "${ data.packageName }" was not found. Cloning...` ); + log.info( `Package "${ data.packageName }" was not found. Skipping...` ); - const bootstrapOptions = { - arguments: data.arguments, - mgitOptions: data.mgitOptions, - packageName: data.packageName, - repository: data.repository - }; - - return bootstrapCommand.execute( bootstrapOptions ) - .then( response => { - log.concat( response.logs ); - - response.logs = log.all(); - - return Promise.resolve( response ); - } ); + return Promise.resolve( { + logs: log.all() + } ); } return execCommand.execute( getExecData( 'git pull' ) ); diff --git a/lib/commands/update.js b/lib/commands/sync.js similarity index 64% rename from lib/commands/update.js rename to lib/commands/sync.js index d999b32..e60c770 100644 --- a/lib/commands/update.js +++ b/lib/commands/sync.js @@ -8,11 +8,13 @@ const fs = require( 'fs' ); const path = require( 'upath' ); const chalk = require( 'chalk' ); +const shell = require( '../utils/shell' ); +const buildOptions = require( 'minimist-options' ); +const minimist = require( 'minimist' ); module.exports = { get helpMessage() { const { - italic: i, gray: g, magenta: m, underline: u @@ -21,7 +23,7 @@ module.exports = { return ` ${ u( 'Description:' ) } Updates all packages. For packages that contain uncommitted changes, the update process is aborted. - If some package is missed, "${ i( 'bootstrap' ) }" command is calling on the missing package. + If some package is missed, it will be installed automatically. The update process executes following commands: @@ -32,7 +34,7 @@ module.exports = { * Pulls the changes if the repository is not detached at some commit. ${ u( 'Options:' ) } - ${ m( '--recursive' ) } Whether to install dependencies recursively. Only packages matching these + ${ m( '--recursive' ) } (-r) Whether to install dependencies recursively. Only packages matching these patterns will be cloned recursively. ${ g( 'Default: false' ) } `; @@ -44,30 +46,18 @@ module.exports = { */ execute( data ) { const log = require( '../utils/log' )(); - const bootstrapCommand = require( './bootstrap' ); const execCommand = require( './exec' ); const destinationPath = path.join( data.mgitOptions.packages, data.repository.directory ); // Package is not cloned. if ( !fs.existsSync( destinationPath ) ) { - log.info( `Package "${ data.packageName }" was not found. Cloning...` ); - - const bootstrapOptions = { - arguments: data.arguments, - mgitOptions: data.mgitOptions, - packageName: data.packageName, - repository: data.repository - }; - - return bootstrapCommand.execute( bootstrapOptions ) - .then( response => { - log.concat( response.logs ); - - response.logs = log.all(); - - return Promise.resolve( response ); - } ); + return this._clonePackage( { + path: destinationPath, + name: data.packageName, + url: data.repository.url, + branch: data.repository.branch + }, this._parseArguments( data.arguments ) ); } return execCommand.execute( getExecData( 'git status -s' ) ) @@ -132,5 +122,63 @@ module.exports = { */ afterExecute( parsedPackages ) { console.log( chalk.cyan( `${ parsedPackages.size } packages have been processed.` ) ); + }, + + /** + * @private + * @param {Object} packageDetails + * @param {Object} options Command options. + * @returns {Promise} + */ + _clonePackage( packageDetails, options ) { + const log = require( '../utils/log' )(); + + log.info( `Package "${ packageDetails.name }" was not found. Cloning...` ); + + const command = [ + `git clone --progress "${ packageDetails.url }" "${ packageDetails.path }"`, + `cd "${ packageDetails.path }"`, + `git checkout --quiet ${ packageDetails.branch }` + ].join( ' && ' ); + + return shell( command ) + .then( output => { + log.info( output ); + + const commandOutput = { + logs: log.all() + }; + + if ( options.recursive ) { + const packageJson = require( path.join( packageDetails.path, 'package.json' ) ); + const packages = []; + + if ( packageJson.dependencies ) { + packages.push( ...Object.keys( packageJson.dependencies ) ); + } + + if ( packageJson.devDependencies ) { + packages.push( ...Object.keys( packageJson.devDependencies ) ); + } + + commandOutput.packages = packages; + } + + return commandOutput; + } ); + }, + + /** + * @private + * @param {Array.} argv List of arguments provided by the user via CLI. + * @returns {Object} + */ + _parseArguments( argv ) { + return minimist( argv, buildOptions( { + recursive: { + type: 'boolean', + alias: 'r', + } + } ) ); } }; diff --git a/tests/commands/bootstrap.js b/tests/commands/bootstrap.js deleted file mode 100644 index 2bf1d3f..0000000 --- a/tests/commands/bootstrap.js +++ /dev/null @@ -1,176 +0,0 @@ -/** - * @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( 'upath' ); -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); -const expect = require( 'chai' ).expect; - -describe( 'commands/bootstrap', () => { - let bootstrapCommand, stubs, commandData; - - beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - shell: sinon.stub(), - fs: { - existsSync: sinon.stub( fs, 'existsSync' ) - }, - path: { - join: sinon.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ) - } - }; - - commandData = { - arguments: [], - packageName: 'test-package', - mgitOptions: { - cwd: __dirname, - packages: 'packages' - }, - repository: { - directory: 'test-package', - url: 'git@github.com/organization/test-package.git', - branch: 'master' - } - }; - - mockery.registerMock( '../utils/shell', stubs.shell ); - - bootstrapCommand = require( '../../lib/commands/bootstrap' ); - } ); - - afterEach( () => { - sinon.restore(); - mockery.deregisterAll(); - mockery.disable(); - } ); - - describe( '#helpMessage', () => { - it( 'defines help screen', () => { - expect( bootstrapCommand.helpMessage ).is.a( 'string' ); - } ); - } ); - - describe( 'beforeExecute()', () => { - it( 'informs about starting the process', () => { - const consoleLog = sinon.stub( console, 'log' ); - - bootstrapCommand.beforeExecute(); - - expect( consoleLog.calledOnce ).to.equal( true ); - expect( consoleLog.firstCall.args[ 0 ] ).to.match( /Cloning missing packages\.\.\./ ); - - consoleLog.restore(); - } ); - } ); - - describe( 'execute()', () => { - it( 'rejects promise if something went wrong', () => { - const error = new Error( 'Unexpected error.' ); - - stubs.fs.existsSync.returns( false ); - stubs.shell.returns( Promise.reject( error ) ); - - return bootstrapCommand.execute( commandData ) - .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.shell.returns( Promise.resolve( 'Git clone log.' ) ); - - return bootstrapCommand.execute( commandData ) - .then( response => { - expect( stubs.shell.calledOnce ).to.equal( true ); - - const cloneCommand = stubs.shell.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( commandData ) - .then( response => { - expect( stubs.shell.called ).to.equal( false ); - - expect( response.logs.info[ 0 ] ).to.equal( 'Package "test-package" is already cloned.' ); - } ); - } ); - - it( 'installs dependencies of cloned package', () => { - commandData.arguments.push( '--recursive' ); - commandData.mgitOptions.packages = __dirname + '/../fixtures'; - commandData.repository.directory = 'project-a'; - - stubs.fs.existsSync.returns( true ); - - return bootstrapCommand.execute( commandData ) - .then( response => { - expect( response.packages ).is.an( 'array' ); - expect( response.packages ).to.deep.equal( [ 'test-foo' ] ); - } ); - } ); - - it( 'installs devDependencies of cloned package', () => { - commandData.arguments.push( '--recursive' ); - commandData.mgitOptions.packages = __dirname + '/../fixtures'; - commandData.repository.directory = 'project-with-options-in-mgitjson'; - - stubs.fs.existsSync.returns( true ); - - return bootstrapCommand.execute( commandData ) - .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 = sinon.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 index 5ccf451..18552bf 100644 --- a/tests/commands/exec.js +++ b/tests/commands/exec.js @@ -92,7 +92,7 @@ describe( 'commands/exec', () => { throw new Error( 'Supposed to be rejected.' ); }, response => { - const err = 'Package "test-package" is not available. Run "mgit bootstrap" in order to download the package.'; + const err = 'Package "test-package" is not available. Run "mgit sync" in order to download the package.'; expect( response.logs.error[ 0 ] ).to.equal( err ); } ); diff --git a/tests/commands/pull.js b/tests/commands/pull.js index 8f2f127..7644c34 100644 --- a/tests/commands/pull.js +++ b/tests/commands/pull.js @@ -72,23 +72,14 @@ describe( 'commands/pull', () => { } ); describe( 'execute()', () => { - it( 'clones a package if is not available', () => { - commandData.arguments.push( '--recursive' ); - + it( 'skips a package if is not available', () => { stubs.fs.existsSync.returns( false ); - stubs.bootstrapCommand.execute.returns( Promise.resolve( { - logs: getCommandLogs( 'Cloned.' ) - } ) ); return pullCommand.execute( commandData ) .then( response => { expect( response.logs.info ).to.deep.equal( [ - 'Package "test-package" was not found. Cloning...', - 'Cloned.' + 'Package "test-package" was not found. Skipping...', ] ); - - expect( stubs.bootstrapCommand.execute.calledOnce ).to.equal( true ); - expect( stubs.bootstrapCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( commandData ); } ); } ); diff --git a/tests/commands/sync.js b/tests/commands/sync.js new file mode 100644 index 0000000..7b51919 --- /dev/null +++ b/tests/commands/sync.js @@ -0,0 +1,348 @@ +/** + * @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( 'upath' ); +const sinon = require( 'sinon' ); +const mockery = require( 'mockery' ); +const expect = require( 'chai' ).expect; + +describe( 'commands/sync', () => { + let syncCommand, stubs, commandData; + + beforeEach( () => { + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + shell: sinon.stub(), + exec: sinon.stub(), + fs: { + existsSync: sinon.stub( fs, 'existsSync' ) + }, + path: { + join: sinon.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ) + }, + bootstrapCommand: { + execute: sinon.stub() + }, + execCommand: { + execute: sinon.stub() + } + }; + + commandData = { + arguments: [], + packageName: 'test-package', + mgitOptions: { + 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( '../utils/shell', stubs.shell ); + + syncCommand = require( '../../lib/commands/sync' ); + } ); + + afterEach( () => { + sinon.restore(); + mockery.deregisterAll(); + mockery.disable(); + } ); + + describe( '#helpMessage', () => { + it( 'defines help screen', () => { + expect( syncCommand.helpMessage ).is.a( 'string' ); + } ); + } ); + + describe( 'execute()', () => { + describe( 'first call on a package', () => { + it( 'clones the package if is not available', () => { + stubs.fs.existsSync.returns( false ); + stubs.shell.returns( Promise.resolve( 'Git clone log.' ) ); + + return syncCommand.execute( commandData ) + .then( response => { + expect( stubs.shell.calledOnce ).to.equal( true ); + + const cloneCommand = stubs.shell.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 ).to.deep.equal( [ + 'Package "test-package" was not found. Cloning...', + 'Git clone log.' + ] ); + } ); + } ); + + it( 'clones dependencies of installed package', () => { + commandData.arguments.push( '--recursive' ); + commandData.mgitOptions.packages = __dirname + '/../fixtures'; + commandData.repository.directory = 'project-a'; + + stubs.fs.existsSync.returns( false ); + stubs.shell.returns( Promise.resolve( 'Git clone log.' ) ); + + return syncCommand.execute( commandData ) + .then( response => { + expect( response.packages ).is.an( 'array' ); + expect( response.packages ).to.deep.equal( [ 'test-foo' ] ); + } ); + } ); + + it( 'clones dev-dependencies of installed package', () => { + commandData.arguments.push( '--recursive' ); + commandData.mgitOptions.packages = __dirname + '/../fixtures'; + commandData.repository.directory = 'project-with-options-in-mgitjson'; + + stubs.fs.existsSync.returns( false ); + stubs.shell.returns( Promise.resolve( 'Git clone log.' ) ); + + return syncCommand.execute( commandData ) + .then( response => { + expect( response.packages ).is.an( 'array' ); + expect( response.packages ).to.deep.equal( [ 'test-bar' ] ); + } ); + } ); + } ); + + describe( 'the package is already installed', () => { + 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 syncCommand.execute( commandData ) + .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 syncCommand.execute( commandData ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + response => { + const errMsg = '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 ); + + commandData.repository.branch = '1a0ff0a'; + + 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 \'1a0ff0a\'.' ) + } ) ); + + exec.onCall( 3 ).returns( Promise.resolve( { + logs: getCommandLogs( [ + '* (HEAD detached at 1a0ff0a)', + ' master', + ' remotes/origin/master' + ].join( '\n' ) ) + } ) ); + + return syncCommand.execute( commandData ) + .then( response => { + expect( response.logs.info ).to.deep.equal( [ + 'Note: checking out \'1a0ff0a\'.', + '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 ); + + commandData.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 syncCommand.execute( commandData ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + response => { + expect( response.logs.info ).to.deep.equal( [ + 'Already on \'develop\'.' + ] ); + + const errMsg = '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 ); + + commandData.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 syncCommand.execute( commandData ) + .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 = sinon.stub( console, 'log' ); + + const processedPackages = new Set(); + processedPackages.add( 'package-1' ); + processedPackages.add( 'package-2' ); + + syncCommand.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/commands/update.js b/tests/commands/update.js deleted file mode 100644 index 46c4d5d..0000000 --- a/tests/commands/update.js +++ /dev/null @@ -1,308 +0,0 @@ -/** - * @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( 'upath' ); -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); -const expect = require( 'chai' ).expect; - -describe( 'commands/update', () => { - let updateCommand, stubs, commandData; - - beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - exec: sinon.stub(), - fs: { - existsSync: sinon.stub( fs, 'existsSync' ) - }, - path: { - join: sinon.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ) - }, - bootstrapCommand: { - execute: sinon.stub() - }, - execCommand: { - execute: sinon.stub() - } - }; - - commandData = { - arguments: [], - packageName: 'test-package', - mgitOptions: { - 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( () => { - sinon.restore(); - mockery.deregisterAll(); - mockery.disable(); - } ); - - describe( '#helpMessage', () => { - it( 'defines help screen', () => { - expect( updateCommand.helpMessage ).is.a( 'string' ); - } ); - } ); - - describe( 'execute()', () => { - it( 'clones a package if is not available', () => { - commandData.arguments.push( '--recursive' ); - - stubs.fs.existsSync.returns( false ); - stubs.bootstrapCommand.execute.returns( Promise.resolve( { - logs: getCommandLogs( 'Cloned.' ) - } ) ); - - return updateCommand.execute( commandData ) - .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 ); - expect( stubs.bootstrapCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( commandData ); - } ); - } ); - - 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( commandData ) - .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( commandData ) - .then( - () => { - throw new Error( 'Supposed to be rejected.' ); - }, - response => { - const errMsg = '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 ); - - commandData.repository.branch = '1a0ff0a'; - - 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 \'1a0ff0a\'.' ) - } ) ); - - exec.onCall( 3 ).returns( Promise.resolve( { - logs: getCommandLogs( [ - '* (HEAD detached at 1a0ff0a)', - ' master', - ' remotes/origin/master' - ].join( '\n' ) ) - } ) ); - - return updateCommand.execute( commandData ) - .then( response => { - expect( response.logs.info ).to.deep.equal( [ - 'Note: checking out \'1a0ff0a\'.', - '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 ); - - commandData.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( commandData ) - .then( - () => { - throw new Error( 'Supposed to be rejected.' ); - }, - response => { - expect( response.logs.info ).to.deep.equal( [ - 'Already on \'develop\'.' - ] ); - - const errMsg = '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 ); - - commandData.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( commandData ) - .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 = sinon.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 3ce3a131213b89ebbfa873aa8ef575e4cab13366 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 10 Sep 2018 14:46:18 +0200 Subject: [PATCH 32/38] Renamed merge to close. Now this command will remove merged branch from the remote. --- lib/commands/{merge.js => close.js} | 13 +++- tests/commands/{merge.js => close.js} | 89 +++++++++++++++++++++------ 2 files changed, 81 insertions(+), 21 deletions(-) rename lib/commands/{merge.js => close.js} (86%) rename tests/commands/{merge.js => close.js} (68%) diff --git a/lib/commands/merge.js b/lib/commands/close.js similarity index 86% rename from lib/commands/merge.js rename to lib/commands/close.js index 8d72409..2092d74 100644 --- a/lib/commands/merge.js +++ b/lib/commands/close.js @@ -69,7 +69,18 @@ module.exports = { mergeCommand += ' ' + options.message.map( message => `-m "${ message }"` ).join( ' ' ); } - return execCommand.execute( getExecData( mergeCommand ) ); + return execCommand.execute( getExecData( mergeCommand ) ) + .then( execResponse => { + log.concat( execResponse.logs ); + log.info( `Removing "${ branch }" branch from the remote.` ); + + return execCommand.execute( getExecData( `git push origin :${ branch }` ) ); + } ) + .then( execResponse => { + log.concat( execResponse.logs ); + + return { logs: log.all() }; + } ); } ); function getExecData( command ) { diff --git a/tests/commands/merge.js b/tests/commands/close.js similarity index 68% rename from tests/commands/merge.js rename to tests/commands/close.js index 0456534..2668ca3 100644 --- a/tests/commands/merge.js +++ b/tests/commands/close.js @@ -11,8 +11,8 @@ const sinon = require( 'sinon' ); const mockery = require( 'mockery' ); const expect = require( 'chai' ).expect; -describe( 'commands/merge', () => { - let mergeCommand, stubs, commandData; +describe( 'commands/close', () => { + let closeCommand, stubs, commandData; beforeEach( () => { mockery.enable( { @@ -36,7 +36,7 @@ describe( 'commands/merge', () => { mockery.registerMock( './exec', stubs.execCommand ); - mergeCommand = require( '../../lib/commands/merge' ); + closeCommand = require( '../../lib/commands/close' ); } ); afterEach( () => { @@ -47,20 +47,20 @@ describe( 'commands/merge', () => { describe( '#helpMessage', () => { it( 'defines help screen', () => { - expect( mergeCommand.helpMessage ).is.a( 'string' ); + expect( closeCommand.helpMessage ).is.a( 'string' ); } ); } ); describe( 'beforeExecute()', () => { it( 'throws an error if command to execute is not specified', () => { expect( () => { - mergeCommand.beforeExecute( [ 'merge' ] ); + closeCommand.beforeExecute( [ 'merge' ] ); } ).to.throw( Error, 'Missing branch to merge. Use: mgit merge [branch].' ); } ); it( 'does nothing if branch to merge is specified', () => { expect( () => { - mergeCommand.beforeExecute( [ 'merge', 'develop' ] ); + closeCommand.beforeExecute( [ 'merge', 'develop' ] ); } ).to.not.throw( Error ); } ); } ); @@ -75,7 +75,7 @@ describe( 'commands/merge', () => { } } ); - return mergeCommand.execute( commandData ) + return closeCommand.execute( commandData ) .then( () => { throw new Error( 'Supposed to be rejected.' ); @@ -86,14 +86,15 @@ describe( 'commands/merge', () => { ); } ); - it( 'merges specified branch', () => { + it( 'merges specified branch and remove it from remote', () => { commandData.arguments.push( 'develop' ); stubs.execCommand.execute.onFirstCall().resolves( { logs: { info: [ '* develop' - ] + ], + error: [] } } ); @@ -101,13 +102,24 @@ describe( 'commands/merge', () => { logs: { info: [ 'Merge made by the \'recursive\' strategy.' - ] + ], + error: [] } } ); - return mergeCommand.execute( commandData ) + stubs.execCommand.execute.onThirdCall().resolves( { + logs: { + info: [ + 'To github.com:foo/bar.git\n' + + ' - [deleted] develop' + ], + error: [] + } + } ); + + return closeCommand.execute( commandData ) .then( commandResponse => { - expect( stubs.execCommand.execute.calledTwice ).to.equal( true ); + expect( stubs.execCommand.execute.calledThrice ).to.equal( true ); expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { repository: { @@ -123,8 +135,20 @@ describe( 'commands/merge', () => { arguments: [ 'git merge develop --no-ff -m "Merge branch \'develop\'"' ] } ); + expect( stubs.execCommand.execute.thirdCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git push origin :develop' ] + } ); + expect( commandResponse.logs.info ).to.deep.equal( [ - 'Merge made by the \'recursive\' strategy.' + 'Merge made by the \'recursive\' strategy.', + + 'Removing "develop" branch from the remote.', + + 'To github.com:foo/bar.git\n' + + ' - [deleted] develop' ] ); } ); } ); @@ -138,7 +162,8 @@ describe( 'commands/merge', () => { logs: { info: [ '* develop' - ] + ], + error: [] } } ); @@ -146,13 +171,24 @@ describe( 'commands/merge', () => { logs: { info: [ 'Merge made by the \'recursive\' strategy.' - ] + ], + error: [] } } ); - return mergeCommand.execute( commandData ) + stubs.execCommand.execute.onThirdCall().resolves( { + logs: { + info: [ + 'To github.com:foo/bar.git\n' + + ' - [deleted] develop' + ], + error: [] + } + } ); + + return closeCommand.execute( commandData ) .then( commandResponse => { - expect( stubs.execCommand.execute.calledTwice ).to.equal( true ); + expect( stubs.execCommand.execute.calledThrice ).to.equal( true ); expect( stubs.execCommand.execute.firstCall.args[ 0 ] ).to.deep.equal( { repository: { @@ -168,8 +204,20 @@ describe( 'commands/merge', () => { arguments: [ 'git merge develop --no-ff -m "Merge branch \'develop\'" -m "Test."' ] } ); + expect( stubs.execCommand.execute.thirdCall.args[ 0 ] ).to.deep.equal( { + repository: { + branch: 'master' + }, + arguments: [ 'git push origin :develop' ] + } ); + expect( commandResponse.logs.info ).to.deep.equal( [ - 'Merge made by the \'recursive\' strategy.' + 'Merge made by the \'recursive\' strategy.', + + 'Removing "develop" branch from the remote.', + + 'To github.com:foo/bar.git\n' + + ' - [deleted] develop' ] ); } ); } ); @@ -183,11 +231,12 @@ describe( 'commands/merge', () => { logs: { info: [ '' - ] + ], + error: [] } } ); - return mergeCommand.execute( commandData ) + return closeCommand.execute( commandData ) .then( commandResponse => { expect( stubs.execCommand.execute.calledOnce ).to.equal( true ); From b376f55344f2aadeb5a2c3b0ac841e1e6e7d5817 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 10 Sep 2018 14:58:49 +0200 Subject: [PATCH 33/38] Aligned README to changes. --- README.md | 22 +++++++++------------- index.js | 8 ++++---- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index d6bb0ee..f6b94f7 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ First, create a configuration file `mgit.json`: (Keys of the `dependencies` object are package names and values are repository URLs (GitHub identifiers in this case). Read more about the [`dependencies` option](#the-dependencies-option).) -And run `mgit update` to clone all the repositories. By default, they will be cloned to `/packages/` directory: +And run `mgit sync` to clone all the repositories. By default, they will be cloned to `/packages/` directory: ```bash packages/ @@ -191,26 +191,24 @@ For displaying help screen for specified command, type: $ mgit [command] --help ``` -### update +### sync Updates dependencies. Switches repositories to correct branches (specified in `mgit.json`) and pulls changes. -If any dependency is missing, the command will install this dependensy as well. +If any dependency is missing, the command will install this dependency as well. This command does not touch repositories in which there are uncommitted changes. Examples: ```bash -mgit update -- --recursive +mgit sync -- --recursive ``` ### pull Pulls changes in existing repositories. -If any dependency is missing, the command will not be executed. - Examples: ```bash @@ -221,8 +219,6 @@ mgit pull -- --recursive Pushes changes in existing repositories. -If any dependency is missing, the command will not be executed. - Examples: ```bash @@ -233,8 +229,6 @@ mgit push Fetches changes in existing repositories. -If any dependency is missing, the command will not be executed. - Examples: ```bash @@ -243,7 +237,7 @@ mgit fetch ### exec -For every cloned repository executes the specified shell command. +Executes specified shell command in existing repositories. Example: @@ -276,13 +270,15 @@ mgit commit -- --message 'Introduce PULL_REQUEST_TEMPLATE.md.' # Commit will be made in repositories that "git status" returns a list if changed files (these files must be tracked by Git). ``` -### merge +### close Requires a second argument which is a branch name that will be merged to current one. You can also specify the message which will be added to the default git-merge message. Repositories which do not have specified branch will be ignored. +After merging the branch, it will be removed from the remote. + Example: ```bash @@ -363,7 +359,7 @@ mgit diff -- master...HEAD ### checkout (alias: `co`) -Changes branches in repositories according to the configuration file. It does not pull the changes and hance is much faster than `mgit update`. The command is useful for bisecting if your main repository contain a revision log like CKEditor 5's [`master-revision`](https://github.com/ckeditor/ckeditor5/commits/master-revisions) branch. +Changes branches in repositories according to the configuration file. It does not pull the changes and hance is much faster than `mgit sync`. The command is useful for bisecting if your main repository contain a revision log like CKEditor 5's [`master-revision`](https://github.com/ckeditor/ckeditor5/commits/master-revisions) branch. ```bash mgit checkout diff --git a/index.js b/index.js index 894be59..d3c4516 100755 --- a/index.js +++ b/index.js @@ -51,18 +51,18 @@ function handleCli() { $ mgit ${ c( 'command' ) } ${ y( '[--options]' ) } -- ${ m( '[--command-options]' ) } ${ u( 'Commands:' ) } - ${ c( 'bootstrap' ) } Installs packages (i.e. clone dependent repositories). ${ c( 'checkout' ) } Changes branches in repositories according to the configuration file. + ${ c( 'close' ) } Merges specified branch with the current one and remove merged branch from the remote. ${ c( 'commit' ) } Commits all changes. A shorthand for "mgit exec 'git commit -a'". ${ c( 'diff' ) } Prints changes from packages where something has changed. ${ c( 'exec' ) } Executes shell command in each package. ${ c( 'fetch' ) } Fetches existing repositories. - ${ c( 'merge' ) } Merges specified branch with the current one. - ${ c( 'pull' ) } Pulls changes in existing repositories and clones missing ones. + ${ c( 'pull' ) } Pulls changes in existing repositories. ${ c( 'push' ) } Pushes changes in existing repositories to remotes. ${ c( 'save' ) } Saves hashes of packages in mgit.json. It allows to easily fix project to a specific state. ${ c( 'status' ) } Prints a table which contains useful information about the status of repositories. - ${ c( 'update' ) } Updates packages to the latest versions (pull changes and check out to proper branch). + ${ c( 'sync' ) } Updates packages to the latest versions or install missing ones. + ${ u( 'Options:' ) } ${ y( '--packages' ) } Directory to which all repositories will be cloned or are already installed. From 28a07815f5db41d0d02e2ab9da9ad78bfb05a5a9 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 10 Sep 2018 14:59:54 +0200 Subject: [PATCH 34/38] Fetch command will print log if a repository is up-to-date. --- lib/commands/fetch.js | 13 ++++++++++++- tests/commands/fetch.js | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/commands/fetch.js b/lib/commands/fetch.js index 9ce3c78..c01f9a6 100644 --- a/lib/commands/fetch.js +++ b/lib/commands/fetch.js @@ -58,7 +58,18 @@ module.exports = { command += ' -p'; } - return execCommand.execute( getExecData( command ) ); + return execCommand.execute( getExecData( command ) ) + .then( execResponse => { + if ( execResponse.logs.info.length ) { + return execResponse; + } + + log.info( 'Repository is up-to-date.' ); + + return Promise.resolve( { + logs: log.all() + } ); + } ); function getExecData( command ) { return Object.assign( {}, data, { diff --git a/tests/commands/fetch.js b/tests/commands/fetch.js index 4f806f9..c652636 100644 --- a/tests/commands/fetch.js +++ b/tests/commands/fetch.js @@ -119,6 +119,26 @@ describe( 'commands/fetch', () => { ] ); } ); } ); + + it( 'prints a log if repository is up-to-date', () => { + stubs.fs.existsSync.returns( true ); + + const exec = stubs.execCommand.execute; + + exec.returns( Promise.resolve( { + logs: { info: [] } + } ) ); + + return fetchCommand.execute( commandData ) + .then( response => { + expect( exec.callCount ).to.equal( 1 ); + expect( exec.firstCall.args[ 0 ].arguments[ 0 ] ).to.equal( 'git fetch' ); + + expect( response.logs.info ).to.deep.equal( [ + 'Repository is up-to-date.' + ] ); + } ); + } ); } ); describe( 'afterExecute()', () => { From f4368eaed523c005201a3d4611e2a2c62f5c36f5 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 10 Sep 2018 15:00:19 +0200 Subject: [PATCH 35/38] Minor changes in commmand help screen. --- lib/commands/checkout.js | 2 ++ lib/commands/close.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/commands/checkout.js b/lib/commands/checkout.js index 32ee6ea..15d9325 100644 --- a/lib/commands/checkout.js +++ b/lib/commands/checkout.js @@ -28,6 +28,8 @@ module.exports = { If specified a branch as an argument for "checkout" command, mgit will use the branch instead of data saved in "mgit.json". E.g "${ g( 'mgit checkout master' ) }" will check out all branches to "master". + + You can also call "${ g( 'mgit checkout .' ) }" in order to restore files before changes. ${ u( 'Options:' ) } ${ m( '--branch' ) } (-b) If specified, mgit will create given branch in all repositories diff --git a/lib/commands/close.js b/lib/commands/close.js index 2092d74..a124c08 100644 --- a/lib/commands/close.js +++ b/lib/commands/close.js @@ -25,6 +25,8 @@ module.exports = { Merge is executed only on repositories where specified branch exist. The merge commit will be made using following message: "${ i( 'Merge branch \'branch-name\'' ) }". + + After merging, specified branch will be removed from the remote. ${ u( 'Options:' ) } ${ m( '--message' ) } (-m) An additional description for merge commit. It will be From b829f314a3e4c7246aa582757555483633569f4a Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 10 Sep 2018 15:07:37 +0200 Subject: [PATCH 36/38] Minor improvements in the code. --- lib/commands/diff.js | 6 ++---- lib/commands/fetch.js | 4 +--- lib/commands/save.js | 37 +++++++++++++++++++------------------ lib/commands/status.js | 4 +--- lib/commands/sync.js | 8 ++++++-- 5 files changed, 29 insertions(+), 30 deletions(-) diff --git a/lib/commands/diff.js b/lib/commands/diff.js index eae313e..18be34f 100644 --- a/lib/commands/diff.js +++ b/lib/commands/diff.js @@ -46,12 +46,10 @@ module.exports = { return execCommand.execute( getExecData( diffCommand ) ) .then( execResponse => { if ( !execResponse.logs.info.length ) { - return Promise.resolve( {} ); + return {}; } - return Promise.resolve( { - logs: execResponse.logs - } ); + return execResponse; } ); function getExecData( command ) { diff --git a/lib/commands/fetch.js b/lib/commands/fetch.js index c01f9a6..f6a465e 100644 --- a/lib/commands/fetch.js +++ b/lib/commands/fetch.js @@ -66,9 +66,7 @@ module.exports = { log.info( 'Repository is up-to-date.' ); - return Promise.resolve( { - logs: log.all() - } ); + return { logs: log.all() }; } ); function getExecData( command ) { diff --git a/lib/commands/save.js b/lib/commands/save.js index 5955347..12b0ac4 100644 --- a/lib/commands/save.js +++ b/lib/commands/save.js @@ -64,26 +64,27 @@ module.exports = { .then( execResponse => execResponse.logs.info[ 0 ].slice( 0, 7 ) ); } - return promise.then( dataToSave => { - const commandResponse = { - packageName: data.packageName, - data: dataToSave, - branch: options.branch, - hash: options.hash - }; - - /* istanbul ignore else */ - if ( options.branch ) { - log.info( `Branch: "${ dataToSave }".` ); - } else if ( options.hash ) { - log.info( `Commit: "${ dataToSave }".` ); - } + return promise + .then( dataToSave => { + const commandResponse = { + packageName: data.packageName, + data: dataToSave, + branch: options.branch, + hash: options.hash + }; + + /* istanbul ignore else */ + if ( options.branch ) { + log.info( `Branch: "${ dataToSave }".` ); + } else if ( options.hash ) { + log.info( `Commit: "${ dataToSave }".` ); + } - return Promise.resolve( { - response: commandResponse, - logs: log.all() + return { + response: commandResponse, + logs: log.all() + }; } ); - } ); function getExecData( command ) { return Object.assign( {}, data, { diff --git a/lib/commands/status.js b/lib/commands/status.js index 32721e7..6cf80f8 100644 --- a/lib/commands/status.js +++ b/lib/commands/status.js @@ -60,9 +60,7 @@ module.exports = { mgitBranch: data.repository.branch }; - return Promise.resolve( { - response: commandResponse - } ); + return { response: commandResponse }; } ); function getExecData( command ) { diff --git a/lib/commands/sync.js b/lib/commands/sync.js index e60c770..f0fc4c0 100644 --- a/lib/commands/sync.js +++ b/lib/commands/sync.js @@ -90,14 +90,14 @@ module.exports = { if ( isOnBranchRegexp.test( stdout ) ) { log.info( `Package "${ data.packageName }" is on a detached commit.` ); - return Promise.resolve( { logs: log.all() } ); + return { logs: log.all() }; } return execCommand.execute( getExecData( `git pull origin ${ data.repository.branch }` ) ) .then( response => { log.concat( response.logs ); - return Promise.resolve( { logs: log.all() } ); + return { logs: log.all() }; } ); } ) .catch( commandResponseOrError => { @@ -127,6 +127,10 @@ module.exports = { /** * @private * @param {Object} packageDetails + * @param {String} packageDetails.name A name of the package. + * @param {String} packageDetails.url A url that will be cloned. + * @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 {Object} options Command options. * @returns {Promise} */ From a11080f08895477fdc31214f23671e458b0aadaa Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Mon, 10 Sep 2018 15:11:32 +0200 Subject: [PATCH 37/38] Improvements for README. --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f6b94f7..2636f19 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,8 @@ mgit sync -- --recursive ### pull -Pulls changes in existing repositories. +Pulls changes in existing repositories. It does not change branches in the repositories and pull the changes even if +the repository contains uncommitted changes. Examples: @@ -277,7 +278,7 @@ which will be added to the default git-merge message. Repositories which do not have specified branch will be ignored. -After merging the branch, it will be removed from the remote. +After merging, the merged branch will be removed from the remote. Example: @@ -285,7 +286,8 @@ Example: # Assumptions: we are on "master" branch and "develop" branch exists. mgit merge develop -- --message 'These changes are required for the future release.' -# Branch `develop` will be merged into `master`. +# Branch "develop" will be merged into "master". +# Branch "develop" will be removed from the origin. ``` ### save From eab13b9692b7cf5d1499fe103fef5b30386f4583 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Tue, 11 Sep 2018 14:00:55 +0200 Subject: [PATCH 38/38] Sync command prints out directories which are not listed in mgit.json but are located in packages directory. --- lib/commands/sync.js | 31 +++++++++++++++- lib/index.js | 2 +- lib/utils/getcommandinstance.js | 3 +- tests/commands/sync.js | 65 ++++++++++++++++++++++++++------- 4 files changed, 85 insertions(+), 16 deletions(-) diff --git a/lib/commands/sync.js b/lib/commands/sync.js index f0fc4c0..d1b4d4c 100644 --- a/lib/commands/sync.js +++ b/lib/commands/sync.js @@ -120,8 +120,37 @@ module.exports = { /** * @param {Set} parsedPackages Collection of processed packages. */ - afterExecute( parsedPackages ) { + afterExecute( parsedPackages, commandResponses, mgitOptions ) { console.log( chalk.cyan( `${ parsedPackages.size } packages have been processed.` ) ); + + const repositoryResolver = require( mgitOptions.resolverPath ); + + const repositoryDirectories = Object.keys( mgitOptions.dependencies ) + .map( packageName => { + const repository = repositoryResolver( packageName, mgitOptions ); + + return path.join( mgitOptions.packages, repository.directory ); + } ); + + const skippedPackages = fs.readdirSync( mgitOptions.packages ) + .map( pathOrDirectory => { + return path.join( mgitOptions.packages, pathOrDirectory ); + } ) + .filter( pathOrDirectory => { + if ( !fs.lstatSync( pathOrDirectory ).isDirectory() ) { + return false; + } + + return !repositoryDirectories.includes( pathOrDirectory ); + } ); + + console.log( + chalk.yellow( 'Paths to directories listed below are skipped by mgit because they are not defined in "mgit.json":' ) + ); + + skippedPackages.forEach( absolutePath => { + console.log( chalk.yellow( ` - ${ absolutePath }` ) ); + } ); }, /** diff --git a/lib/index.js b/lib/index.js index 4db5b53..7643d80 100644 --- a/lib/index.js +++ b/lib/index.js @@ -117,7 +117,7 @@ module.exports = function( args, options ) { return forkPool.killAll() .then( () => { if ( command.afterExecute ) { - command.afterExecute( processedPackages, commandResponses ); + command.afterExecute( processedPackages, commandResponses, mgitOptions ); } const endTime = process.hrtime( startTime ); diff --git a/lib/utils/getcommandinstance.js b/lib/utils/getcommandinstance.js index 19ef413..60dd305 100644 --- a/lib/utils/getcommandinstance.js +++ b/lib/utils/getcommandinstance.js @@ -63,9 +63,10 @@ module.exports = function getCommandInstance( commandName ) { * - `packages` - an array of packages that mgit should process as well. * * @property {Function} [afterExecute] A function that is called by mgit automatically after executing the main command's method. - * This function is called once. It receives two parameters: + * This function is called once. It receives three parameters: * - a collection (`Set`) that contains all processed packages by mgit. * - a collection (`Set`) that contains responses returned by `#execute` function. + * - an options object (`Options`) which contains options resolved by mgit. */ /** diff --git a/tests/commands/sync.js b/tests/commands/sync.js index 7b51919..3b94585 100644 --- a/tests/commands/sync.js +++ b/tests/commands/sync.js @@ -14,7 +14,7 @@ const mockery = require( 'mockery' ); const expect = require( 'chai' ).expect; describe( 'commands/sync', () => { - let syncCommand, stubs, commandData; + let syncCommand, stubs, mgitOptions, commandData; beforeEach( () => { mockery.enable( { @@ -27,7 +27,9 @@ describe( 'commands/sync', () => { shell: sinon.stub(), exec: sinon.stub(), fs: { - existsSync: sinon.stub( fs, 'existsSync' ) + existsSync: sinon.stub( fs, 'existsSync' ), + lstatSync: sinon.stub( fs, 'lstatSync' ), + readdirSync: sinon.stub( fs, 'readdirSync' ) }, path: { join: sinon.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ) @@ -37,16 +39,20 @@ describe( 'commands/sync', () => { }, execCommand: { execute: sinon.stub() - } + }, + repositoryResolver: sinon.stub() + }; + + mgitOptions = { + cwd: __dirname, + packages: __dirname + '/packages', + resolverPath: 'PATH_TO_RESOLVER' }; commandData = { arguments: [], packageName: 'test-package', - mgitOptions: { - cwd: __dirname, - packages: 'packages' - }, + mgitOptions, repository: { directory: 'test-package', url: 'git@github.com/organization/test-package.git', @@ -56,6 +62,7 @@ describe( 'commands/sync', () => { mockery.registerMock( './exec', stubs.execCommand ); mockery.registerMock( '../utils/shell', stubs.shell ); + mockery.registerMock( 'PATH_TO_RESOLVER', stubs.repositoryResolver ); syncCommand = require( '../../lib/commands/sync' ); } ); @@ -85,10 +92,11 @@ describe( 'commands/sync', () => { const cloneCommand = stubs.shell.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"' ); + expect( cloneCommand[ 0 ] ).to.equal( + `git clone --progress "git@github.com/organization/test-package.git" "${ __dirname }/packages/test-package"` + ); // Change the directory to cloned package. - expect( cloneCommand[ 1 ] ).to.equal( 'cd "packages/test-package"' ); + expect( cloneCommand[ 1 ] ).to.equal( `cd "${ __dirname }/packages/test-package"` ); // And check out to proper branch. expect( cloneCommand[ 2 ] ).to.equal( 'git checkout --quiet master' ); @@ -315,17 +323,48 @@ describe( 'commands/sync', () => { } ); describe( 'afterExecute()', () => { - it( 'informs about number of processed packages', () => { + it( 'informs about number of processed packages and differences between packages in directory and defined in mgit.json', () => { const consoleLog = sinon.stub( console, 'log' ); const processedPackages = new Set(); processedPackages.add( 'package-1' ); processedPackages.add( 'package-2' ); - syncCommand.afterExecute( processedPackages ); + mgitOptions.dependencies = { + 'package-1': 'foo/package-1', + 'package-2': 'foo/package-2', + }; + + stubs.repositoryResolver.onFirstCall().returns( { directory: 'package-1' } ); + stubs.repositoryResolver.onSecondCall().returns( { directory: 'package-2' } ); + + stubs.fs.readdirSync.returns( [ + 'package-1', + 'package-2', + 'package-3', + '.DS_Store' + ] ); + + stubs.fs.lstatSync.returns( { + isDirectory() { + return true; + } + } ); + + stubs.fs.lstatSync.withArgs( __dirname + '/packages/.DS_Store' ).returns( { + isDirectory() { + return false; + } + } ); + + syncCommand.afterExecute( processedPackages, null, mgitOptions ); - expect( consoleLog.calledOnce ).to.equal( true ); + expect( consoleLog.callCount ).to.equal( 3 ); 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(); } );