var semver = require('semver'); var which = require('which'); var fs = require('../util/fs'); var path = require('path'); var Q = require('q'); var execFile = require('child_process').execFile; var defaultConfig = require('../config'); var createError = require('../util/createError'); function version(logger, versionArg, options, config) { options = options || {}; config = defaultConfig(config); return bump(logger, config, versionArg, options.message); } function bump(logger, config, versionArg, message) { var cwd = config.cwd || process.cwd(); var newVersion; if (!versionArg) { throw createError('No agrument provided', 'EREADOPTIONS'); } return driver .check(cwd) .then(function() { 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(result) { logger.info( 'version', 'Bumped package version from ' + result.oldVersion + ' to ' + result.newVersion, result ); return result.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 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; }); } 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); 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() { return Q.nfcall(execFile, 'git', ['tag', tag, '-am', message], { env: process.env, cwd: cwd }); }); } }; version.readOptions = function(argv) { var cli = require('../util/cli'); var options = cli.readOptions( { message: { type: String, shorthand: 'm' } }, argv ); return [options.argv.remain[1], options]; }; module.exports = version;