diff --git a/README.md b/README.md index 781c569..fd8d242 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,29 @@ Multi-repo manager for git. A tool for managing projects build using multiple re mgit2 is designed to work with [yarn workspaces](https://yarnpkg.com/lang/en/docs/workspaces/) and [Lerna](https://github.com/lerna/lerna) out of the box, hence, it mixes the "package" and "repository" concepts. In other words, every repository is meant to be a single [npm](https://npmjs.com) package. It doesn't mean that you must use it with Lerna and npm, but don't be surprised that mgit2 talks about "packages" and works best with npm packages. +# Table of content + +1. [Installation](#installation) +1. [Usage](#usage) +1. [Configuration](#configuration) + 1. [The `dependencies` option](#the-dependencies-option) + 1. [Recursive cloning](#recursive-cloning) + 1. [Cloning repositories on CI servers](#cloning-repositories-on-ci-servers) + 1. [Standard/core branches](#standardcore-branches) +1. [Commands](#commands) + 1. [`sync`](#sync) + 1. [`pull`](#pull) + 1. [`push`](#push) + 1. [`fetch`](#fetch) + 1. [`exec`](#exec) + 1. [`commit` or `ci`](#commit-alias-ci) + 1. [`close`](#close) + 1. [`save`](#save) + 1. [`status` or `st`](#status-alias-st) + 1. [`diff`](#diff) + 1. [`checkout` or `co`](#checkout-alias-co) +1. [Projects using mgit2](#projects-using-mgit2) + ## Installation ```bash @@ -187,6 +210,24 @@ mgit --resolver-url-template="https://github.com/\${ path }.git" You can also use full HTTPS URLs to configure `dependencies` in your `mgit.json`. +### Standard/core branches + +When you call `mgit sync` or `mgit co` it uses the `master` branch in every repository, unless the repository's branch is defined in `mgit.json`. + +If you support only the one main/core branch in your projects, it isn't a problem. But if you have more than one, on every single branch you need modify branches for dependencies in `mgit.json`. + +In order to simplify the flow, we introduced the standard/core branches option. It uses the current branch of the main repository in all cases where the branch is not defined in `mgit.json`. + +```json +{ + ... + "standardBranches": [ "master", "stable" ], + ... +} +``` + +[Read more about the feature.](https://github.com/cksource/mgit2/issues/103) + ## Commands ```bash diff --git a/lib/default-resolver.js b/lib/default-resolver.js index 7e52c20..3ea0de9 100644 --- a/lib/default-resolver.js +++ b/lib/default-resolver.js @@ -23,7 +23,9 @@ module.exports = function resolver( packageName, options ) { const repository = parseRepositoryUrl( repositoryUrl, { urlTemplate: options.resolverUrlTemplate, - defaultBranch: options.resolverDefaultBranch + defaultBranch: options.resolverDefaultBranch, + standardBranches: options.standardBranches, + cwdPackageBranch: options.cwdPackageBranch, } ); if ( options.overrideDirectoryNames[ packageName ] ) { diff --git a/lib/utils/getoptions.js b/lib/utils/getoptions.js index 3c01921..2ccf518 100644 --- a/lib/utils/getoptions.js +++ b/lib/utils/getoptions.js @@ -7,6 +7,7 @@ const fs = require( 'fs' ); const path = require( 'upath' ); +const shell = require( 'shelljs' ); /** * @param {Object} callOptions Call options. @@ -27,7 +28,8 @@ module.exports = function cwdResolver( callOptions, cwd ) { ignore: null, scope: null, packagesPrefix: [], - overrideDirectoryNames: {} + overrideDirectoryNames: {}, + standardBranches: [] }; if ( fs.existsSync( mgitJsonPath ) ) { @@ -42,6 +44,14 @@ module.exports = function cwdResolver( callOptions, cwd ) { options.packagesPrefix = [ options.packagesPrefix ]; } + // Check if under specified `cwd` path, the git repository exists. + // If so, find a branch name that the repository is checked out. See #103. + if ( fs.existsSync( path.join( cwd, '.git' ) ) ) { + const response = shell.exec( 'git rev-parse --abbrev-ref HEAD', { silent: true } ); + + options.cwdPackageBranch = response.stdout.trim(); + } + return options; }; @@ -84,4 +94,10 @@ module.exports = function cwdResolver( callOptions, cwd ) { * * @property {String|Array.} [packagesPrefix=[]] Prefix or prefixes which will be removed from packages' names during * printing the summary of the "status" command. + * + * @property {Array.} [standardBranches=[]] Name of branches that are allowed to check out (based on a branch in main repository) + * if specified package does not have defined a branch. + * + * @property {String} [cwdPackageBranch] If the main repository is a git repository, this variable keeps + * a name of a current branch of the repository. The value is required for `standardBranches` option. */ diff --git a/lib/utils/parserepositoryurl.js b/lib/utils/parserepositoryurl.js index c03ec04..cd1cbec 100644 --- a/lib/utils/parserepositoryurl.js +++ b/lib/utils/parserepositoryurl.js @@ -17,11 +17,19 @@ const url = require( 'url' ); * Used if `repositoryUrl` defines only `'/'`. * @param {String} [options.defaultBranch='master'] The default branch name to be used if the * repository URL doesn't specify it. + * @param {Array.>} [options.standardBranches=[]] Name of branches that are allowed to check out + * based on the value specified as `options.cwdPackageBranch`. + * @param {String} [options.cwdPackageBranch] A name of a branch that the main repository is checked out. * @returns {Repository} */ module.exports = function parseRepositoryUrl( repositoryUrl, options = {} ) { const parsedUrl = url.parse( repositoryUrl ); - const branch = parsedUrl.hash ? parsedUrl.hash.slice( 1 ) : options.defaultBranch || 'master'; + const branch = getBranch( parsedUrl, { + defaultBranch: options.defaultBranch, + standardBranches: options.standardBranches || [], + cwdPackageBranch: options.cwdPackageBranch, + } ); + let repoUrl; if ( repositoryUrl.match( /^(file|https?):\/\// ) || repositoryUrl.match( /^git@/ ) ) { @@ -41,6 +49,23 @@ module.exports = function parseRepositoryUrl( repositoryUrl, options = {} ) { }; }; +function getBranch( parsedUrl, options ) { + const defaultBranch = options.defaultBranch || 'master'; + + // Check if branch is defined in mgit.json. Use it. + if ( parsedUrl.hash ) { + return parsedUrl.hash.slice( 1 ); + } + + // Check if the main repo is on one of standard branches. If yes, use that branch. + if ( options.cwdPackageBranch && options.standardBranches.includes( options.cwdPackageBranch ) ) { + return options.cwdPackageBranch; + } + + // Nothing matches. Use default branch. + return defaultBranch; +} + /** * Repository info. * diff --git a/tests/utils/getoptions.js b/tests/utils/getoptions.js index cd7e37d..b70ce35 100644 --- a/tests/utils/getoptions.js +++ b/tests/utils/getoptions.js @@ -9,7 +9,11 @@ const getOptions = require( '../../lib/utils/getoptions' ); const path = require( 'upath' ); +const fs = require( 'fs' ); +const shell = require( 'shelljs' ); const expect = require( 'chai' ).expect; +const sinon = require( 'sinon' ); + const cwd = path.resolve( __dirname, '..', 'fixtures', 'project-a' ); describe( 'utils', () => { @@ -33,7 +37,8 @@ describe( 'utils', () => { packagesPrefix: [], overrideDirectoryNames: { 'override-directory': 'custom-directory' - } + }, + standardBranches: [] } ); } ); @@ -58,7 +63,8 @@ describe( 'utils', () => { scope: null, ignore: null, packagesPrefix: [], - overrideDirectoryNames: {} + overrideDirectoryNames: {}, + standardBranches: [] } ); } ); @@ -79,7 +85,8 @@ describe( 'utils', () => { scope: null, ignore: null, packagesPrefix: [], - overrideDirectoryNames: {} + overrideDirectoryNames: {}, + standardBranches: [] } ); } ); @@ -103,8 +110,73 @@ describe( 'utils', () => { scope: null, ignore: null, packagesPrefix: [], - overrideDirectoryNames: {} + overrideDirectoryNames: {}, + standardBranches: [] + } ); + } ); + + it( 'returns "packagesPrefix" as array', () => { + const options = getOptions( { + packagesPrefix: 'ckeditor5-' + }, cwd ); + + expect( options ).to.have.property( 'dependencies' ); + + delete options.dependencies; + + expect( options ).to.deep.equal( { + cwd, + packages: path.resolve( cwd, 'packages' ), + resolverPath: path.resolve( __dirname, '../../lib/default-resolver.js' ), + resolverUrlTemplate: 'git@github.com:${ path }.git', + resolverTargetDirectory: 'git', + resolverDefaultBranch: 'master', + scope: null, + ignore: null, + packagesPrefix: [ + 'ckeditor5-' + ], + overrideDirectoryNames: { + 'override-directory': 'custom-directory' + }, + standardBranches: [] + } ); + } ); + + it( 'attaches to options branch name from the cwd directory (if in git repository)', () => { + const fsExistsStub = sinon.stub( fs, 'existsSync' ); + const shelljsStub = sinon.stub( shell, 'exec' ); + + fsExistsStub.returns( true ); + shelljsStub.returns( { + stdout: 'master\n' + } ); + + const options = getOptions( {}, cwd ); + + expect( options ).to.have.property( 'dependencies' ); + + delete options.dependencies; + + expect( options ).to.deep.equal( { + cwd, + packages: path.resolve( cwd, 'packages' ), + resolverPath: path.resolve( __dirname, '../../lib/default-resolver.js' ), + resolverUrlTemplate: 'git@github.com:${ path }.git', + resolverTargetDirectory: 'git', + resolverDefaultBranch: 'master', + scope: null, + ignore: null, + packagesPrefix: [], + overrideDirectoryNames: { + 'override-directory': 'custom-directory' + }, + standardBranches: [], + cwdPackageBranch: 'master' } ); + + fsExistsStub.restore(); + shelljsStub.restore(); } ); } ); } ); diff --git a/tests/utils/parserepositoryurl.js b/tests/utils/parserepositoryurl.js index b119ee1..7d2a3f7 100644 --- a/tests/utils/parserepositoryurl.js +++ b/tests/utils/parserepositoryurl.js @@ -108,5 +108,122 @@ describe( 'utils', () => { directory: 'bar' } ); } ); + + describe( 'standardBranches support (ticket: #103)', () => { + it( 'returns default branch name if standard branches is not specified', () => { + const repository = parseRepositoryUrl( 'foo/bar', { + urlTemplate: 'https://github.com/${ path }.git', + defaultBranch: 'develop', + cwdPackageBranch: 'master' + } ); + + expect( repository ).to.deep.equal( { + url: 'https://github.com/foo/bar.git', + branch: 'develop', + directory: 'bar' + } ); + } ); + + it( 'returns default branch name if main package is not a git repository', () => { + const repository = parseRepositoryUrl( 'foo/bar', { + urlTemplate: 'https://github.com/${ path }.git', + defaultBranch: 'develop' + } ); + + expect( repository ).to.deep.equal( { + url: 'https://github.com/foo/bar.git', + branch: 'develop', + directory: 'bar' + } ); + } ); + + it( 'returns "master" as default branch if standard branches and default branch are not specified', () => { + const repository = parseRepositoryUrl( 'foo/bar', { + urlTemplate: 'https://github.com/${ path }.git', + cwdPackageBranch: 'master' + } ); + + expect( repository ).to.deep.equal( { + url: 'https://github.com/foo/bar.git', + branch: 'master', + directory: 'bar' + } ); + } ); + + it( 'returns default branch name if standard branches is an empty array', () => { + const repository = parseRepositoryUrl( 'foo/bar', { + urlTemplate: 'https://github.com/${ path }.git', + defaultBranch: 'develop', + standardBranches: [], + cwdPackageBranch: 'master' + } ); + + expect( repository ).to.deep.equal( { + url: 'https://github.com/foo/bar.git', + branch: 'develop', + directory: 'bar' + } ); + } ); + + it( 'returns default branch name if the main repo is not whitelisted in "standardBranches" array', () => { + const repository = parseRepositoryUrl( 'foo/bar', { + urlTemplate: 'https://github.com/${ path }.git', + defaultBranch: 'develop', + standardBranches: [ 'stable' ], + cwdPackageBranch: 'master' + } ); + + expect( repository ).to.deep.equal( { + url: 'https://github.com/foo/bar.git', + branch: 'develop', + directory: 'bar' + } ); + } ); + + it( 'returns the "cwdPackageBranch" value if a branch is not specified and the value is whitelisted', () => { + const repository = parseRepositoryUrl( 'foo/bar', { + urlTemplate: 'https://github.com/${ path }.git', + defaultBranch: 'develop', + standardBranches: [ 'stable', 'master' ], + cwdPackageBranch: 'stable' + } ); + + expect( repository ).to.deep.equal( { + url: 'https://github.com/foo/bar.git', + branch: 'stable', + directory: 'bar' + } ); + } ); + + it( 'ignores options if a branch is specified in the repository URL', () => { + const repository = parseRepositoryUrl( 'foo/bar#mgit', { + urlTemplate: 'https://github.com/${ path }.git', + defaultBranch: 'develop', + standardBranches: [ 'stable' ], + cwdPackageBranch: 'master' + } ); + + expect( repository ).to.deep.equal( { + url: 'https://github.com/foo/bar.git', + branch: 'mgit', + directory: 'bar' + } ); + } ); + + it( 'ignores options if a branch is specified in the repository URL ("standardBranches" contains "cwdPackageBranch")', () => { + const repository = parseRepositoryUrl( 'foo/bar#mgit', { + urlTemplate: 'https://github.com/${ path }.git', + defaultBranch: 'develop', + standardBranches: [ 'master' ], + cwdPackageBranch: 'master' + } ); + + expect( repository ).to.deep.equal( { + url: 'https://github.com/foo/bar.git', + branch: 'mgit', + directory: 'bar' + } ); + } ); + } ); } ); } );