Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. [Base branches](#base-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
Expand Down Expand Up @@ -187,6 +210,26 @@ mgit --resolver-url-template="https://github.com/\${ path }.git"

You can also use full HTTPS URLs to configure `dependencies` in your `mgit.json`.

### Base branches

When you call `mgit sync` or `mgit co`, mgit will use the following algorithm to determine the branch to which each repository should be checked out:

1. If a branch is defined in `mgit.json`, use it. A branch can be defined after `#` in a repository URL. For example: `"@cksource/foo": "cksource/foo#dev"`.
2. If the root repository (assuming, it is a repository) is on one of the "base branches", use that branch name.
3. Otherwise, use `master`.

You can define the base branches as follows:

```json
{
...
"baseBranches": [ "master", "stable" ],
...
}
```

With this configuration, if the root repository is on `stable`, calling `mgit co` will check out all repositories to `stable`. If you change the branch of the root repository to `master` and call `mgit co`, all sub repositories will be checked out to `master`.

## Commands

```bash
Expand Down
4 changes: 3 additions & 1 deletion lib/default-resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ module.exports = function resolver( packageName, options ) {

const repository = parseRepositoryUrl( repositoryUrl, {
urlTemplate: options.resolverUrlTemplate,
defaultBranch: options.resolverDefaultBranch
defaultBranch: options.resolverDefaultBranch,
baseBranches: options.baseBranches,
cwdPackageBranch: options.cwdPackageBranch,
} );

if ( options.overrideDirectoryNames[ packageName ] ) {
Expand Down
18 changes: 17 additions & 1 deletion lib/utils/getoptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

const fs = require( 'fs' );
const path = require( 'upath' );
const shell = require( 'shelljs' );

/**
* @param {Object} callOptions Call options.
Expand All @@ -27,7 +28,8 @@ module.exports = function cwdResolver( callOptions, cwd ) {
ignore: null,
scope: null,
packagesPrefix: [],
overrideDirectoryNames: {}
overrideDirectoryNames: {},
baseBranches: []
};

if ( fs.existsSync( mgitJsonPath ) ) {
Expand All @@ -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;
};

Expand Down Expand Up @@ -84,4 +94,10 @@ module.exports = function cwdResolver( callOptions, cwd ) {
*
* @property {String|Array.<String>} [packagesPrefix=[]] Prefix or prefixes which will be removed from packages' names during
* printing the summary of the "status" command.
*
* @property {Array.<String>} [baseBranches=[]] 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 `baseBranches` option.
*/
27 changes: 26 additions & 1 deletion lib/utils/parserepositoryurl.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,19 @@ const url = require( 'url' );
* Used if `repositoryUrl` defines only `'<organization>/<repositoryName>'`.
* @param {String} [options.defaultBranch='master'] The default branch name to be used if the
* repository URL doesn't specify it.
* @param {Array.<String>>} [options.baseBranches=[]] 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,
baseBranches: options.baseBranches || [],
cwdPackageBranch: options.cwdPackageBranch,
} );

let repoUrl;

if ( repositoryUrl.match( /^(file|https?):\/\// ) || repositoryUrl.match( /^git@/ ) ) {
Expand All @@ -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 base branches. If yes, use that branch.
if ( options.cwdPackageBranch && options.baseBranches.includes( options.cwdPackageBranch ) ) {
return options.cwdPackageBranch;
}

// Nothing matches. Use default branch.
return defaultBranch;
}

/**
* Repository info.
*
Expand Down
80 changes: 76 additions & 4 deletions tests/utils/getoptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -33,7 +37,8 @@ describe( 'utils', () => {
packagesPrefix: [],
overrideDirectoryNames: {
'override-directory': 'custom-directory'
}
},
baseBranches: []
} );
} );

Expand All @@ -58,7 +63,8 @@ describe( 'utils', () => {
scope: null,
ignore: null,
packagesPrefix: [],
overrideDirectoryNames: {}
overrideDirectoryNames: {},
baseBranches: []
} );
} );

Expand All @@ -79,7 +85,8 @@ describe( 'utils', () => {
scope: null,
ignore: null,
packagesPrefix: [],
overrideDirectoryNames: {}
overrideDirectoryNames: {},
baseBranches: []
} );
} );

Expand All @@ -103,8 +110,73 @@ describe( 'utils', () => {
scope: null,
ignore: null,
packagesPrefix: [],
overrideDirectoryNames: {}
overrideDirectoryNames: {},
baseBranches: []
} );
} );

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'
},
baseBranches: []
} );
} );

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'
},
baseBranches: [],
cwdPackageBranch: 'master'
} );

fsExistsStub.restore();
shelljsStub.restore();
} );
} );
} );
117 changes: 117 additions & 0 deletions tests/utils/parserepositoryurl.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,122 @@ describe( 'utils', () => {
directory: 'bar'
} );
} );

describe( 'baseBranches support (ticket: #103)', () => {
it( 'returns default branch name if base 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 base 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 base branches is an empty array', () => {
const repository = parseRepositoryUrl( 'foo/bar', {
urlTemplate: 'https://github.com/${ path }.git',
defaultBranch: 'develop',
baseBranches: [],
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 "baseBranches" array', () => {
const repository = parseRepositoryUrl( 'foo/bar', {
urlTemplate: 'https://github.com/${ path }.git',
defaultBranch: 'develop',
baseBranches: [ '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',
baseBranches: [ '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',
baseBranches: [ '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 ("baseBranches" contains "cwdPackageBranch")', () => {
const repository = parseRepositoryUrl( 'foo/bar#mgit', {
urlTemplate: 'https://github.com/${ path }.git',
defaultBranch: 'develop',
baseBranches: [ 'master' ],
cwdPackageBranch: 'master'
} );

expect( repository ).to.deep.equal( {
url: 'https://github.com/foo/bar.git',
branch: 'mgit',
directory: 'bar'
} );
} );
} );
} );
} );