UNPKG

mrgit

Version:

A tool for managing projects build using multiple repositories.

292 lines (237 loc) 9.1 kB
/** * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ 'use strict'; const fs = require( 'fs' ); const path = require( 'upath' ); const chalk = require( 'chalk' ); const shell = require( '../utils/shell' ); module.exports = { name: 'sync', get helpMessage() { const { 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, it will be installed automatically. 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 "mrgit.json" file. * Pulls the changes if the repository is not detached at some commit. ${ u( 'Options:' ) } ${ m( '--recursive' ) } (-r) 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 execCommand = require( './exec' ); if ( !data.isRootRepository ) { const destinationPath = path.join( data.toolOptions.packages, data.repository.directory ); // Package is not cloned. if ( !fs.existsSync( destinationPath ) ) { log.info( `Package "${ data.packageName }" was not found. Cloning...` ); return this._clonePackage( { path: destinationPath, name: data.packageName, url: data.repository.url, branch: data.repository.branch, tag: data.repository.tag }, data.toolOptions, { log } ); } } return execCommand.execute( getExecData( 'git status -s' ) ) .then( async response => { const stdout = response.logs.info.join( '\n' ).trim(); if ( stdout ) { throw new Error( `Package "${ data.packageName }" has uncommitted changes. Aborted.` ); } return execCommand.execute( getExecData( 'git fetch' ) ); } ) .then( response => { log.concat( response.logs ); } ) .then( async () => { let checkoutValue; if ( !data.repository.tag ) { checkoutValue = data.repository.branch; } else if ( data.repository.tag === 'latest' ) { const commandOutput = await execCommand.execute( getExecData( 'git log --tags --simplify-by-decoration --pretty="%S"' ) ); if ( !commandOutput.logs.info.length ) { throw new Error( `Can't check out the latest tag as package "${ data.packageName }" has no tags. Aborted.` ); } const latestTag = commandOutput.logs.info[ 0 ].trim().split( '\n' ).shift(); checkoutValue = 'tags/' + latestTag.trim(); } else { checkoutValue = 'tags/' + data.repository.tag; } return execCommand.execute( getExecData( `git checkout "${ checkoutValue }"` ) ); } ) .then( response => { log.concat( response.logs ); } ) .then( () => { return execCommand.execute( getExecData( 'git branch' ) ); } ) .then( response => { const stdout = response.logs.info.join( '\n' ).trim(); const isOnBranchRegexp = /HEAD detached at+/; // If on a detached commit, mrgit must not pull the changes. if ( isOnBranchRegexp.test( stdout ) ) { log.info( `Package "${ data.packageName }" is on a detached commit.` ); return { logs: log.all() }; } return execCommand.execute( getExecData( `git pull origin "${ data.repository.branch }"` ) ) .then( response => { log.concat( response.logs ); return { logs: log.all() }; } ); } ) .catch( commandResponseOrError => { if ( commandResponseOrError instanceof Error ) { log.error( commandResponseOrError.message ); } else { log.concat( commandResponseOrError.logs ); } return Promise.reject( { logs: log.all() } ); } ); function getExecData( command ) { return Object.assign( {}, data, { arguments: [ command ] } ); } }, /** * @param {Set} parsedPackages Collection of processed packages. */ afterExecute( parsedPackages, commandResponses, toolOptions ) { console.log( chalk.cyan( `${ parsedPackages.size } packages have been processed.` ) ); const repositoryResolver = require( toolOptions.resolverPath ); const repositoryDirectories = Object.keys( toolOptions.dependencies ) .map( packageName => { const repository = repositoryResolver( packageName, toolOptions ); return path.join( toolOptions.packages, repository.directory ); } ); const skippedPackages = fs.readdirSync( toolOptions.packages ) .map( directoryName => { const absolutePath = path.join( toolOptions.packages, directoryName ); if ( !directoryName.startsWith( '@' ) ) { return absolutePath; } return fs.readdirSync( absolutePath ).map( directoryName => path.join( absolutePath, directoryName ) ); } ) // TODO: Array.prototype.flat would be awesome here... But it isn't supported in Node yet. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat .reduce( ( pathsCollection, pathOrArrayPaths ) => { if ( Array.isArray( pathOrArrayPaths ) ) { pathsCollection.push( ...pathOrArrayPaths ); } else { pathsCollection.push( pathOrArrayPaths ); } return pathsCollection; }, [] ) .filter( pathOrDirectory => { if ( !fs.lstatSync( pathOrDirectory ).isDirectory() ) { return false; } return !repositoryDirectories.includes( pathOrDirectory ); } ); if ( skippedPackages.length ) { console.log( chalk.yellow( 'Paths to directories listed below are skipped by mrgit because they are not defined in "mrgit.json":' ) ); skippedPackages.forEach( absolutePath => { console.log( chalk.yellow( ` - ${ absolutePath }` ) ); } ); } }, /** * @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 {Options} toolOptions Options resolved by mrgit. * @param {Object} options Additional options which aren't related to mrgit. * @param {Logger} options.log Logger * @param {Boolean} [options.doNotTryAgain=false] If set to `true`, bootstrap command won't be executed again. * @returns {Promise} */ _clonePackage( packageDetails, toolOptions, options ) { const log = options.log; return shell( `git clone --progress "${ packageDetails.url }" "${ packageDetails.path }"` ) .then( async output => { log.info( output ); let checkoutValue; if ( !packageDetails.tag ) { checkoutValue = packageDetails.branch; } else if ( packageDetails.tag === 'latest' ) { const commandOutput = await shell( `cd "${ packageDetails.path }" && git log --tags --simplify-by-decoration --pretty="%S"` ); const latestTag = commandOutput.trim().split( '\n' ).shift(); checkoutValue = 'tags/' + latestTag.trim(); } else { checkoutValue = 'tags/' + packageDetails.tag; } return shell( `cd "${ packageDetails.path }" && git checkout --quiet "${ checkoutValue }"` ); } ) .then( () => { const commandOutput = { logs: log.all() }; if ( toolOptions.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; } ) .catch( error => { /* istanbul ignore else */ if ( isRemoteHungUpError( error ) && !options.doNotTryAgain ) { return delay( 5000 ).then( () => { return this._clonePackage( packageDetails, toolOptions, { log, doNotTryAgain: true } ); } ); } log.error( error ); return Promise.reject( { logs: log.all() } ); } ); } }; // See: https://github.com/cksource/mrgit/issues/87 function isRemoteHungUpError( error ) { if ( typeof error != 'string' ) { error = error.toString(); } const fatalErrors = error.split( '\n' ) .filter( message => message.startsWith( 'fatal:' ) ) .map( message => message.trim() ); return fatalErrors[ 0 ] && fatalErrors[ 0 ].match( /fatal: the remote end hung up unexpectedly/i ); } function delay( ms ) { return new Promise( resolve => { setTimeout( resolve, ms ); } ); }