Skip to content

Commit

Permalink
Merge pull request #105 from cksource/t/103
Browse files Browse the repository at this point in the history
Feature: Support for base branches. Closes #103.
  • Loading branch information
Reinmar committed Jul 15, 2019
2 parents 4591908 + e5cd291 commit 51eded0
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 7 deletions.
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'
} );
} );
} );
} );
} );

0 comments on commit 51eded0

Please sign in to comment.