fedtools-utilities
Version:
Set of utilites for fedtools within nodejs
718 lines (671 loc) • 23.9 kB
JavaScript
/**
* Provides the git-helper class.
*
* @class git-helper
* @static
* @module fedtools-utilities
*/
var
_ = require('lodash'),
async = require('async'),
fs = require('fs'),
log = require('fedtools-logs'),
cmd = require('fedtools-commands'),
_runGitCommand;
// -- P R I V A T E M E T H O D S
/**
* Helper method to execute a git command and trap the result.
*
* @method _runGitCommand
* @private
* @async
* @param {String} cmdline A git command line.
* @param {Object} options
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {Object} done.res An object with 2 keys: stdout and stderr.
* @param {String} res.stdout The resulting success output string without
* the trailing \n.
* @param {String} res.stderr The resulting error output string without
* the trailing \n.
* @param {String} res.elapsedTime The time it took to run the command in ms.
*/
exports.runGitCommand = _runGitCommand = function (cmdline, options, done) {
var
pwd = process.cwd(),
status = false,
verbose = false,
startTime = new Date();
if (_.isFunction(options)) {
done = options;
throw new Error('options is a function, expected an object');
} else {
pwd = options.cwd || process.cwd();
status = (_.isBoolean(options.silent)) ? !options.silent : false;
verbose = (options.verbose) ? true : false;
}
async.waterfall([
function (callback) {
if (options.cwd) {
if (fs.existsSync(options.cwd)) {
return callback();
} else {
return callback(1);
}
} else {
return callback();
}
}
], function (err) {
if (!err) {
cmd.run(cmdline, {
pwd: pwd,
status: status,
verbose: verbose
}, function (error, stderr, stdout) {
if (!error && stdout) {
stdout = stdout.toString().replace(/\n$/, '');
}
if (error && stderr) {
stderr = stderr.toString().replace(/\n$/, '');
}
return done(error, {
stdout: stdout,
stderr: stderr,
elapsedTime: new Date() - startTime
});
});
} else {
return done(err, {
elapsedTime: new Date() - startTime
});
}
});
};
/**
* Helper method to extract commit messages between two tags.
*
* @method _getCommitBetweenTags
* @private
* @async
* @param {Object} options
* @param {String} options.tagA The starting tag.
* @param {String} options.tagB The ending tag.
* @param {String|Array} [options.ignore] An array (or a single string) of pattern to
* match commit messages. If matched, the commit will be ignored.
* @param {Boolean} [options.longSHA] If true, use the long SHA instead of short.
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
* @param {String} [options.commitTemplate] Template for the commit message.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {Array} done.output An array of message (for each commit).
*/
function _getCommitBetweenTags(options, done) {
var
res,
ignore,
result,
cmdline = 'git log ' + options.tagA + '..' + options.tagB;
if (options.commitTemplate) {
cmdline += ' --pretty=format:"' + options.commitTemplate + '"';
} else if (options.longSHA) {
cmdline += ' --pretty=format:" %H [%an]: %s"';
} else {
cmdline += ' --pretty=format:" %h [%an]: %s"';
}
if (options.ignore) {
ignore = (_.isArray(options.ignore)) ? new RegExp(options.ignore.join('|')) : new RegExp(
options.ignore);
}
res = cmd.run(cmdline, {
pwd: process.cwd(),
status: false,
verbose: false
});
if (res.code === 0) {
result = res.output.split('\n');
if (ignore) {
result = _.filter(result, function (item) {
if (item.match(ignore)) {
return false;
} else {
return true;
}
});
}
return done(null, result);
} else {
return done(res.code, res.output);
}
}
/**
* Finds all the tags of a git repository. Result can be narrowed by specifying
* a starting tag or an ending tag (or both).
*
* @method getAllTags
* @private
* @async
* @param {Object} options
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {String} [options.since] The first tag (non-included) to list.
* @param {String} [options.until] The last tag (included) to list.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {Array} done.result An array of objects with 2 keys: `tag` and
* the corresponding `timestamp`.
*
* @example
* _getAllTags({
* since: '0.0.92',
* until: '0.0.99'
* }, function(err, tags) {
* _.each(tags, function(item) {
* console.log('tag: ', item.tag);
* console.log('date: ', item.timestamp);
* });
* });
*/
function _getAllTags(options, done) {
var SEPARATOR = '~',
cmdline = 'git log --tags --simplify-by-decoration ' +
'--pretty=format:"%ai' + SEPARATOR + '%d"',
_mapTimeAndTag, _filterTags,
semver = require('semver');
_mapTimeAndTag = function (a, done) {
var timestamp, tag, b;
a = a.replace(/\"/g, '');
b = a.split(SEPARATOR);
timestamp = b[0];
tag = b[1].replace(/\(/g, '').replace(/\)/g, '').replace(/tag:/g, '').trim();
if (tag.match(/^HEAD/)) {
tag = tag.split(',')[1].trim();
}
if (tag && semver.valid(tag)) {
return done(null, {
tag: tag,
timestamp: timestamp
});
} else {
return done(null, null);
}
};
_filterTags = function (tagList, since, until) {
var res = tagList;
if (since && semver.valid(since) && until && semver.valid(until)) {
if (semver.gt(since, until)) {
return res;
}
}
if (since && semver.valid(since)) {
res = _.filter(res, function (item) {
if (semver.clean(item.tag) === since) {
return true;
} else {
return semver.gt(item.tag, since);
}
});
}
if (until && semver.valid(until)) {
res = _.filter(res, function (item) {
if (semver.clean(item.tag) === until) {
return true;
} else {
return semver.lt(item.tag, until);
}
});
}
return res;
};
_runGitCommand(cmdline, options, function (err, res) {
var
data = res.stdout.split('\n');
log.debug('_runGitCommand err: ', err);
async.mapSeries(data, _mapTimeAndTag, function (err, result) {
if (!err) {
return done(err, _filterTags(_.compact(result), options.since, options.until));
} else {
return done(err, result);
}
});
});
}
// -- P U B L I C M E T H O D S
/**
* Extract the most recent local SHA.
*
* @method getCurrentSHA
* @async
* @param {Object} options
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {String} [options.short=false] Flag to get the short or full SHA.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {String} done.stdOut The resulting output string without the trailing \n
*/
exports.getCurrentSHA = function (options, done) {
var cmdline = (options && options.short) ? 'git log --pretty=format:%h -1' :
'git log --pretty=format:%H -1';
_runGitCommand(cmdline, options, done);
};
/**
* Checks out a branch (fetching before in case the branch is not known locally yet).
*
* Runs `git fetch && git checkout <branch>`
*
* @method checkoutBranch
* @async
* @param {Object} options
* @param {String} options.branch The branch to checkout.
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {String} done.stdOut The resulting output string without the trailing \n
*/
exports.checkoutBranch = function (options, done) {
_runGitCommand('git fetch', options, function () {
_runGitCommand('git checkout ' + options.branch, options, done);
});
};
/**
* Hard reset a repository.
*
* Runs `git reset --hard`
*
* @method hardResetRepository
* @async
* @param {Object} options
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {String} done.stdOut The resulting output string without the trailing \n
*/
exports.hardResetRepository = function (options, done) {
_runGitCommand('git reset --hard', options, done);
};
/**
* Clones a git repository.
*
* Runs `git clone [options]`
*
* @method cloneGitRepository
* @async
* @param {Object} options
* @param {String} options.url The git repository URL.
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {String} [options.cloneArgs] Possible options to be passed to the clone command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {String} done.stdOut The resulting output string without the trailing \n
*/
exports.cloneGitRepository = function (options, done) {
var cmdClone = 'git clone';
if (options && options.cloneArgs) {
cmdClone = cmdClone.trim() + ' ' + options.cloneArgs.trim();
}
_runGitCommand(cmdClone.trim() + ' ' + options.url.trim() + ' ' + (options.name.trim() ||
''),
options, done);
};
/**
* Runs `git fetch`
*
* @method gitFetchLatestFromOrigin
* @async
* @param {Object} options
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {String} done.stdOut The resulting output string without the trailing \n
*/
exports.gitFetchLatestFromOrigin = function (options, done) {
_runGitCommand('git fetch', options, done);
};
/**
* Add the provided URL as the 'upstream' remote.
*
* Runs `git remote add upstream options.url`
*
* @method gitAddUpstreamRemote
* @async
* @param {Object} options
* @param {String} options.url The upstream URL.
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {String} done.stdOut The resulting output string without the trailing \n
*/
exports.gitAddUpstreamRemote = function (options, done) {
_runGitCommand('git remote add upstream ' + options.url, options, done);
};
/**
* Checks if the current path is a git repository.
*
* Runs `git symbolic-ref HEAD`
*
* @method isGitRepository
* @async
* @param {Object} options
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {String} done.stdOut The resulting output string without the trailing \n
*/
exports.isGitRepository = function (options, done) {
_runGitCommand('git symbolic-ref HEAD', options, done);
};
/**
* Checks if the current git repository is dirty.
*
* Runs `git describe --dirty`
*
* @method isGitRepositoryDirty
* @async
* @param {Object} options
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {Boolean} done.dirty A flag indicating if the repo is dirty.
*/
exports.isGitRepositoryDirty = function (options, done) {
_runGitCommand('git describe --dirty', options, function (err, data) {
if (!err && data && data.stdout && data.stdout.match('-dirty')) {
return done(null, true);
} else {
return done();
}
});
};
/**
* Find the current branch of a git repository.
*
* Runs `git rev-parse --abbrev-ref HEAD`
*
* @method getCurrentBranch
* @async
* @param {Object} options
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {String} done.stdOut The resulting output string without the trailing \n
*/
exports.getCurrentBranch = function (options, done) {
_runGitCommand('git rev-parse --abbrev-ref HEAD', options, done);
};
/**
* Finds the root path of a git repository.
*
* Runs `git rev-parse --show-toplevel`
*
* @method findGitRootPath
* @async
* @param {Object} options
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {String} done.stdOut The resulting output string without the trailing \n
*/
exports.findGitRootPath = function (options, done) {
_runGitCommand('git rev-parse --show-toplevel', options, done);
};
/**
* Finds the most recent tag of a given repository.
*
* Runs `git for-each-ref --count 1 --format="ref=%(refname)" --sort='-*authordate' refs/tags`
*
* @method getLatestTag
* @async
* @param {Object} options
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {Object} done.res An object with 2 keys: stdout and stderr.
* @param {String} res.stdout The resulting success output string without
* the trailing \n.
* @param {String} res.stderr The resulting error output string without
* the trailing \n. */
exports.getLatestTag = function (options, done) {
var
r,
stdout,
stderr;
_runGitCommand('git for-each-ref --count 1 --format="ref=%(refname)" --sort=\'-*authordate\' refs/tags', options, function (err, res) {
stderr = (res) ? res.stderr : null;
if (!err && res.stdout) {
r = res.stdout.replace(/"/g, '').split('/');
stdout = r.pop();
}
return done(err, {
stdout: stdout,
stderr: stderr
});
});
};
/**
* Finds all the tags of a given repository.
*
* Runs `git for-each-ref --format="ref=%(refname), %(*authorname), %(taggerdate:short), %(*subject)" --sort='-*authordate' refs/tags`
*
* @method getAllTags
* @async
* @param {Object} options
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {Array} done.data An array of tags.
*/
exports.getAllTags = function (options, done) {
var
cmdline = [];
// command to get the most recent commit SHA
cmdline[0] = 'git rev-parse HEAD';
// command to get all the tags
cmdline[1] = 'git tag -l --sort=-version:refname';
// command to get the oldest commit SHA
cmdline[2] = 'git rev-list --max-parents=0 HEAD';
async.waterfall([
function (callback) {
cmd.run(cmdline[0], {
status: false
}, function (err, stderr, stdout) {
if (!err && stdout) {
return callback(err, [stdout.replace('\n', '')]);
} else {
return callback(err);
}
});
},
function (data, callback) {
cmd.run(cmdline[1], {
status: false
}, function (err, stderr, stdout) {
if (!err && stdout) {
data.push(stdout.split('\n'));
return callback(err, data);
} else {
return callback(err, stderr);
}
});
},
function (data, callback) {
cmd.run(cmdline[2], {
status: false
}, function (err, stderr, stdout) {
if (!err && stdout) {
data.push(
stdout.replace('\n', '')
);
}
callback(null, data);
});
}
], function (err, data) {
done(err, _.compact(_.flatten(data)));
});
};
/**
* Generates a CHANGELOG type of output for a given repository.
* The default templates are:
* - tplHistoryDate: `\n__{{tag}} / {{timestamp}}__\n\n`
* - tplHistoryCommit: ` {{commitMsg}}\n` (with 4 spaces to start with)
*
* @method getChangeLog
* @async
* @param {Object} options
* @param {String} [options.tplHistoryDate]
* A mustache template for the headers. Available keys are `tag` and `timestamp`.
* @param {String} [options.tplHistoryCommit]
* A mustache template for the commit messages. Available keys are `commitMsg`.
* @param {String} [options.dateFormat='YYYY-MM-DD']
* A date format (see momentjs.com).
* @param {String|Array} [options.ignore] An array (or a single string) of pattern to
* match commit messages. If matched, the commit will be ignored.
* @param {Boolean} [options.longSHA] If true, use the long SHA instead of short.
* @param {String} [options.since] Start changelog listing after this tag.
* @param {String} [options.until] End changelog listing at this tag.
* @param {String} [options.cwd=process.cwd()] The path where to run the command.
* @param {Boolean} [options.silent=false] If true, do no log the command line.
* @param {Boolean} [options.verbose=false] If true, log stderr/stdout.
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error Not null if there is a problem with the command.
* @param {String} done.stdOut The resulting output string without the trailing \n
*
*/
exports.getChangeLog = function (options, done) {
var mustache = require('mustache'),
moment = require('moment'),
async = require('async'),
result = '',
_extractCommitsBetweenTags, _updateTemplates,
tplHistoryDate, tplHistoryCommit, dateFormat;
tplHistoryDate = options.tplHistoryDate || '\n__{{tag}} / {{timestamp}}__\n\n';
tplHistoryCommit = options.tplHistoryCommit || '{{{commitMsg}}}\n';
dateFormat = options.dateFormat || 'YYYY-MM-DD';
_updateTemplates = function (cfg, callback) {
var tagHeader, commitsList = '';
tagHeader = mustache.render(tplHistoryDate, {
tag: cfg.tags[cfg.index].tag,
timestamp: moment(new Date(cfg.tags[cfg.index].timestamp)).format(dateFormat)
});
_.each(cfg.commits, function (commit) {
commitsList += mustache.render(tplHistoryCommit, {
commitMsg: commit
});
});
if (commitsList !== '') {
result += tagHeader;
result += commitsList;
}
callback();
};
_extractCommitsBetweenTags = function (a, b, done) {
_getCommitBetweenTags({
tagA: a.tag,
tagB: b.tag,
ignore: options.ignore,
longSHA: options.longSHA || false,
commitTemplate: options.commitTemplate
}, function (err, commits) {
log.debug('_getCommitBetweenTags err: ', err);
done(null, commits);
});
};
async.waterfall([
function (callback) {
_getAllTags(options, function (err, tags) {
callback(err, tags);
});
},
function (tags, callback) {
/*jshint loopfunc:true*/
// need to extract all commits between all tags...
var i, jobs = [],
len = tags.length;
jobs[0] = function (cb) {
_extractCommitsBetweenTags(tags[1], tags[0], function (err, commits) {
log.debug('_extractCommitsBetweenTags err: ', err);
_updateTemplates({
tags: tags,
index: 0,
commits: commits
}, function () {
cb(null, tags, 1);
});
});
};
for (i = 1; i <= len; i++) {
if (tags[i + 1]) {
jobs.push(
function (tagList, param, cb) {
_extractCommitsBetweenTags(tags[param + 1], tags[param], function (err,
commits) {
_updateTemplates({
err: err,
tags: tagList,
index: param,
commits: commits
}, function () {
cb(null, tagList, param + 1);
});
});
}
);
}
}
async.waterfall(jobs, function (err) {
callback(err);
});
},
function (callback) {
callback(null, 'done');
}
], function (err) {
done(err, result);
});
};