Skip to content

Commit

Permalink
Make bower version behavior consistent with spec
Browse files Browse the repository at this point in the history
  • Loading branch information
hdgarrood authored and sheerun committed Apr 4, 2016
1 parent fc44462 commit a6ad9c2
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 119 deletions.
201 changes: 116 additions & 85 deletions lib/commands/version.js
Expand Up @@ -4,116 +4,147 @@ var fs = require('../util/fs');
var path = require('path');
var Q = require('q');
var execFile = require('child_process').execFile;
var Project = require('../core/Project');
var defaultConfig = require('../config');
var createError = require('../util/createError');

function version(logger, versionArg, options, config) {
var project;

options = options || {};

config = defaultConfig(config);
project = new Project(config, logger);

return bump(project, versionArg, options.message);
return bump(logger, config, versionArg, options.message);
}

function bump(project, versionArg, message) {
var cwd = project._config.cwd || process.cwd();
function bump(logger, config, versionArg, message) {
var cwd = config.cwd || process.cwd();
var newVersion;
var doGitCommit = false;

return checkGit(cwd)
.then(function (hasGit) {
doGitCommit = hasGit;
})
.then(project.getJson.bind(project))
.then(function (json) {
newVersion = getNewVersion(json.version, versionArg);
json.version = newVersion;
})
.then(project.saveJson.bind(project))
if (!versionArg) {
throw createError('No <version> agrument provided', 'EREADOPTIONS');
}

return driver.check(cwd)
.then(function () {
if (doGitCommit) {
return gitCommitAndTag(cwd, newVersion, message);
return Q.all([driver.versions(cwd), driver.currentVersion(cwd)]);
})
.spread(function (versions, currentVersion) {
if (semver.valid(versionArg)) {
newVersion = semver.valid(versionArg);
} else {
newVersion = semver.inc(currentVersion, versionArg);

if (!newVersion) {
throw createError('Invalid <version> argument: ' + versionArg, 'EINVALIDVERSION', { version: versionArg });
}
}

newVersion = (currentVersion[0] === 'v') ? 'v' + newVersion : newVersion;

if (versions) {
versions.forEach(function (version) {
if (semver.eq(version, newVersion)) {
throw createError('Version exists: ' + newVersion, 'EVERSIONEXISTS', { versions: versions, newVersion: newVersion });
}
});
}

return driver.bump(cwd, newVersion, message).then(function () {
return {
oldVersion: currentVersion,
newVersion: newVersion
}
});
})
.then(function () {
console.log('v' + newVersion);
return newVersion;
.then(function (result) {
logger.info('version', 'Bumped package version from ' + result.oldVersion + ' to ' + result.newVersion, result);

return result.newVersion;
});
}

function getNewVersion(currentVersion, versionArg) {
var newVersion = semver.valid(versionArg);
if (!newVersion) {
newVersion = semver.inc(currentVersion, versionArg);
}
if (!newVersion) {
throw createError('Invalid version argument: `' + versionArg + '`. Usage: `bower version [<newversion> | major | minor | patch]`', 'EINVALIDVERSION');
}
if (currentVersion === newVersion) {
throw createError('Version not changed', 'EVERSIONNOTCHANGED');
}
return newVersion;
}
var driver = {
check: function (cwd) {
function checkGit(cwd) {
var gitDir = path.join(cwd, '.git');
return Q.nfcall(fs.stat, gitDir)
.then(function (stat) {
if (stat.isDirectory()) {
return checkGitStatus(cwd);
}
return false;
}, function () {
//Ignore not found .git directory
return false;
});
}

function checkGit(cwd) {
var gitDir = path.join(cwd, '.git');
return Q.nfcall(fs.stat, gitDir)
.then(function (stat) {
if (stat.isDirectory()) {
return checkGitStatus(cwd);
function checkGitStatus(cwd) {
return Q.nfcall(which, 'git')
.fail(function (err) {
err.code = 'ENOGIT';
throw err;
})
.then(function () {
return Q.nfcall(execFile, 'git', ['status', '--porcelain'], {env: process.env, cwd: cwd});
})
.then(function (value) {
var stdout = value[0];
var lines = filterModifiedStatusLines(stdout);
if (lines.length) {
throw createError('Version bump requires clean working directory', 'EWORKINGDIRECTORYDIRTY');
}
return true;
});
}
return false;
}, function () {
//Ignore not found .git directory
return false;
});
}

function checkGitStatus(cwd) {
return Q.nfcall(which, 'git')
.fail(function (err) {
err.code = 'ENOGIT';
throw err;
})
.then(function () {
return Q.nfcall(execFile, 'git', ['status', '--porcelain'], {env: process.env, cwd: cwd});
})
.then(function (value) {
var stdout = value[0];
var lines = filterModifiedStatusLines(stdout);
if (lines.length) {
throw createError('Git working directory not clean.\n' + lines.join('\n'), 'EWORKINGDIRECTORYDIRTY');
function filterModifiedStatusLines(stdout) {
return stdout.trim().split('\n')
.filter(function (line) {
return line.trim() && !line.match(/^\?\? /);
}).map(function (line) {
return line.trim();
});
}
return true;
});
}

function filterModifiedStatusLines(stdout) {
return stdout.trim().split('\n')
.filter(function (line) {
return line.trim() && !line.match(/^\?\? /);
}).map(function (line) {
return line.trim();
});
}
return checkGit(cwd).then(function(hasGit) {
if (!hasGit) {
throw createError('Version bump currently supports only git repositories', 'ENOTGITREPOSITORY');
}
});
},
versions: function (cwd) {
return Q.nfcall(execFile, 'git', ['tag'], {env: process.env, cwd: cwd})
.then(function (res) {
var versions = res[0]
.split(/\r?\n/)
.filter(semver.valid);

function gitCommitAndTag(cwd, newVersion, message) {
var tag = 'v' + newVersion;
message = message || tag;
message = message.replace(/%s/g, newVersion);
return Q.nfcall(execFile, 'git', ['add', 'bower.json'], {env: process.env, cwd: cwd})
.then(function () {
return Q.nfcall(execFile, 'git', ['commit', '-m', message], {env: process.env, cwd: cwd});
})
.then(function () {
return Q.nfcall(execFile, 'git', ['tag', tag, '-am', message], {env: process.env, cwd: cwd});
});
return versions;
}, function () {
return [];
});
},
currentVersion: function (cwd) {
return Q.nfcall(execFile, 'git', ['describe', '--abbrev=0', '--tags'], {env: process.env, cwd: cwd})
.then(function (res) {
var version = res[0]
.split(/\r?\n/)
.filter(semver.valid)[0];

return version;
}, function () {
return undefined;
});
},
bump: function (cwd, tag, message) {
message = message || tag;
message = message.replace(/%s/g, tag);
return Q.nfcall(execFile, 'git', ['commit', '-m', message, '--allow-empty'], {env: process.env, cwd: cwd}) .then(function () {
Q.nfcall(execFile, 'git', ['tag', tag, '-am', message], {env: process.env, cwd: cwd});
});
}
}

// -------------------

version.readOptions = function (argv) {
var cli = require('../util/cli');
Expand Down
4 changes: 2 additions & 2 deletions lib/templates/json/help-version.json
@@ -1,8 +1,8 @@
{
"command": "version",
"description": "Run this in a package directory to bump the version and write the new data back to the bower.json file.\n\nThe newversion argument should be a valid semver string, or a valid second argument to semver.inc (one of \"build\", \"patch\", \"minor\", or \"major\"). In the second case, the existing version will be incremented\nby 1 in the specified field.\n\nIf run in a git repo, it will also create a version commit and tag, and fail if the repo is not clean.\n\nIf supplied with --message (shorthand: -m) config option, bower will use it as a commit message when creating a version commit. If the message config contains %s then that will be replaced with the resulting\nversion number. For example:\n\n bower version patch -m \"Upgrade to %s for reasons\"",
"description": "Creates an empty version commit and tag, and fail if the repo is not clean.\n\nThe <version> argument should be a valid semver string, or one of following:\nbuild, patch, minor, major.\n\nIf supplied with --message (shorthand: -m) config option, bower will use it\nas a commit message when creating a version commit. If the message config\ncontains %s then that will be replaced with the resulting version number.\n\nFor example:\n\n bower version patch -m \"Upgrade to %s for reasons\"",
"usage": [
"version [<newversion> | major | minor | patch]"
"version [<version> | major | minor | patch]"
],
"options": [
{
Expand Down
98 changes: 66 additions & 32 deletions test/commands/version.js
Expand Up @@ -3,87 +3,121 @@ var expect = require('expect.js');
var helpers = require('../helpers');
var version = helpers.require('lib/commands').version;

describe('bower list', function () {
describe('bower version', function () {

var mainPackage = new helpers.TempDir({
'bower.json': {
name: 'foobar',
version: '0.0.0'
}
});

var gitPackage = new helpers.TempDir({
'v0.0.0': {
'bower.json': {
name: 'foobar',
version: '0.0.0'
}
}
});

var packageWithoutTags = new helpers.TempDir({});


it('bumps patch version', function () {
mainPackage.prepare();
mainPackage.prepareGit();

return helpers.run(version, ['patch', {}, { cwd: mainPackage.path }]).then(function () {
expect(mainPackage.readJson('bower.json').version).to.be('0.0.1');
expect(mainPackage.latestGitTag()).to.be('0.0.1');
});
});

it('bumps minor version', function () {
mainPackage.prepare();
mainPackage.prepareGit();

return helpers.run(version, ['minor', {}, { cwd: mainPackage.path }]).then(function () {
expect(mainPackage.readJson('bower.json').version).to.be('0.1.0');
expect(mainPackage.latestGitTag()).to.be('0.1.0');
});
});

it('bumps major version', function () {
mainPackage.prepare();
mainPackage.prepareGit();

return helpers.run(version, ['major', {}, { cwd: mainPackage.path }]).then(function () {
expect(mainPackage.readJson('bower.json').version).to.be('1.0.0');
expect(mainPackage.latestGitTag()).to.be('1.0.0');
});
});

it('changes version', function () {
mainPackage.prepare();
mainPackage.prepareGit();

return helpers.run(version, ['1.2.3', {}, { cwd: mainPackage.path }]).then(function () {
expect(mainPackage.readJson('bower.json').version).to.be('1.2.3');
expect(mainPackage.latestGitTag()).to.be('1.2.3');
});
});

it('returns the new version', function () {
mainPackage.prepare();
mainPackage.prepareGit();

return helpers.run(version, ['major', {}, { cwd: mainPackage.path }]).then(function (results) {
expect(results[0]).to.be('1.0.0');
});
});

it('bumps patch version, create commit, and tag', function () {
gitPackage.prepareGit();
it('fails on a dirty git repository', function () {
mainPackage.prepareGit();
mainPackage.create({
'dirty.txt': 'This file has not been committed'
});

return helpers.run(version, ['patch', {}, { cwd: mainPackage.path }]).then(null, function (err) {
expect(err).to.be.an(Error);
expect(err.code).to.be('ENOTGITREPOSITORY');
});
});

return helpers.run(version, ['patch', {}, { cwd: gitPackage.path }]).then(function () {
expect(gitPackage.readJson('bower.json').version).to.be('0.0.1');
it('fails when the version is not changed', function () {
mainPackage.prepareGit();

var tags = gitPackage.git('tag');
expect(tags).to.be('v0.0.0\nv0.0.1\n');
var message = gitPackage.git('log', '--pretty=format:%s', '-n1');
expect(message).to.be('v0.0.1');
return helpers.run(version, ['0.0.0', {}, { cwd: mainPackage.path }]).then(null, function (err) {
expect(err).to.be.an(Error);
expect(err.code).to.be('EVERSIONNOTCHANGED');
});
});

it('bumps with custom commit message', function () {
gitPackage.prepareGit();
it('fails with an invalid argument', function () {
mainPackage.prepareGit();

return helpers.run(version, ['lol', {}, { cwd: mainPackage.path }]).then(null, function (err) {
expect(err).to.be.an(Error);
expect(err.code).to.be('EINVALIDVERSION');
});
});

return helpers.run(version, ['patch', { message: 'Bumping %s, because what'}, { cwd: gitPackage.path }]).then(function () {
expect(gitPackage.readJson('bower.json').version).to.be('0.0.1');
it('bumps with custom commit message', function () {
mainPackage.prepareGit();

var tags = gitPackage.git('tag');
return helpers.run(version, ['patch', { message: 'Bumping %s, because what'}, { cwd: mainPackage.path }]).then(function () {
var tags = mainPackage.git('tag');
expect(tags).to.be('v0.0.0\nv0.0.1\n');
var message = gitPackage.git('log', '--pretty=format:%s', '-n1');
var message = mainPackage.git('log', '--pretty=format:%s', '-n1');
expect(message).to.be('Bumping 0.0.1, because what');
});
});

it('creates commit and tags', function () {
mainPackage.prepareGit();

return helpers.run(version, ['patch', {}, { cwd: mainPackage.path }]).then(function () {
var tags = mainPackage.git('tag');
expect(tags).to.be('v0.0.0\nv0.0.1\n');
var message = mainPackage.git('log', '--pretty=format:%s', '-n1');
expect(message).to.be('v0.0.1');
});
});

it('assumes v0.0.0 when no tags exist', function () {
packageWithoutTags.prepareGit();
packageWithoutTags.create({
'index.js': 'console.log("hello, world");'
});
packageWithoutTags.git('add', '-A');
packageWithoutTags.git('commit', '-m"commit"');

return helpers.run(version, ['major', {}, { cwd: packageWithoutTags.path }]).then(function () {
expect(packageWithoutTags.latestGitTag()).to.be('1.0.0');
});
});
});

0 comments on commit a6ad9c2

Please sign in to comment.