From 9054f6405a27c13bdfe0145d055356a92447cc4f Mon Sep 17 00:00:00 2001 From: Harry Garrood Date: Fri, 8 Jan 2016 00:58:32 +0000 Subject: [PATCH] Make bower version behavior consistent with spec --- lib/commands/version.js | 203 ++++++++++++++++----------- lib/templates/json/help-version.json | 4 +- test/commands/version.js | 100 ++++++++----- test/helpers.js | 15 ++ 4 files changed, 202 insertions(+), 120 deletions(-) diff --git a/lib/commands/version.js b/lib/commands/version.js index 778d907b6..9cb672f27 100644 --- a/lib/commands/version.js +++ b/lib/commands/version.js @@ -4,116 +4,149 @@ 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 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) { + currentVersion = currentVersion || '0.0.0'; + + if (semver.valid(versionArg)) { + newVersion = semver.valid(versionArg); + } else { + newVersion = semver.inc(currentVersion, versionArg); + + if (!newVersion) { + throw createError('Invalid 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 [ | 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'); diff --git a/lib/templates/json/help-version.json b/lib/templates/json/help-version.json index 605ef200f..d6bc76d0c 100644 --- a/lib/templates/json/help-version.json +++ b/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 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 [ | major | minor | patch]" + "version [ | major | minor | patch]" ], "options": [ { diff --git a/test/commands/version.js b/test/commands/version.js index 3eab1aa4b..b70087377 100644 --- a/test/commands/version.js +++ b/test/commands/version.js @@ -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'); + expect(results[0]).to.be('v1.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: gitPackage.path }]).then(function () { - expect(gitPackage.readJson('bower.json').version).to.be('0.0.1'); + return helpers.run(version, ['patch', {}, { cwd: mainPackage.path }]).then(null, function (err) { + expect(err).to.be.an(Error); + expect(err.code).to.be('ENOTGITREPOSITORY'); + }); + }); - 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'); + it('fails when the version already exists', function () { + mainPackage.prepareGit(); + + 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('EVERSIONEXISTS'); + }); + }); + + 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'); }); }); it('bumps with custom commit message', function () { - gitPackage.prepareGit(); + mainPackage.prepareGit(); - 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'); + 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 = mainPackage.git('log', '--pretty=format:%s', '-n1'); + expect(message).to.be('Bumping v0.0.1, because what'); + }); + }); + + it('creates commit and tags', function () { + mainPackage.prepareGit(); - var tags = gitPackage.git('tag'); + 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 = gitPackage.git('log', '--pretty=format:%s', '-n1'); - expect(message).to.be('Bumping 0.0.1, because what'); + 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'); }); }); }); diff --git a/test/helpers.js b/test/helpers.js index 7c3005c50..9ae69ce69 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -14,6 +14,7 @@ var proxyquire = require('proxyquire').noCallThru().noPreserveCache(); var spawnSync = require('spawn-sync'); var config = require('../lib/config'); var nock = require('nock'); +var semver = require('semver'); // For better promise errors Q.longStackSupport = true; @@ -155,6 +156,20 @@ exports.TempDir = (function () { } }; + TempDir.prototype.latestGitTag = function () { + var versions = this.git('tag') + .split(/\r?\n/) + .map(function (t) { return t[0] == 'v' ? t.slice(1) : t; }) + .filter(semver.valid) + .sort(semver.compare); + + if (versions.length >= 1) { + return versions[versions.length - 1]; + } else { + throw new Error('No valid git version tags found.'); + } + }; + TempDir.prototype.exists = function (name) { return fs.existsSync(path.join(this.path, name)); };