fedtools-utilities
Version:
Set of utilites for fedtools within nodejs
1,395 lines (1,294 loc) • 40.7 kB
JavaScript
/*eslint indent: 0, no-console: 0*/
/**
* Utilities methods that can be used by multiple modules.
*
* @class utilities
* @static
* @module fedtools-utilities
*/
var
_ = require('lodash'),
async = require('async'),
path = require('path'),
fs = require('fs-extra'),
osenv = require('osenv'),
util = require('util'),
shelljs = require('shelljs'),
crypto = require('crypto'),
inquirer = require('inquirer'),
moment = require('moment'),
url = require('url'),
log = require('fedtools-logs'),
cmd = require('fedtools-commands'),
config = require('fedtools-config'),
timeTrackerStart,
_formatMillisecondsToHuman,
consoleMinions = {
'stderr': process.stderr.write,
'stdout': process.stdout.write,
'log': console.log,
'info': console.info,
'error': console.error
},
NB_SPACES_FOR_TAB = 2,
DEFAULT_TIME_PRECISION = 3,
PROMPT_PASSWORD = 'password',
PROMPT_CONFIRM = 'confirm',
PROMPT_PROMPT = 'prompt',
ALIVE_SIGNAL = 0,
KILL_SIGNAL = 'SIGINT',
CRYPTO_ALGO = 'aes-256-ctr';
// -- P R O P E R T I E S
/**
* Use this signal to check if a process is running.
*
* @property ALIVE_SIGNAL
* @type String
* @static
*/
exports.ALIVE_SIGNAL = ALIVE_SIGNAL;
/**
* Use this signal to kill a running process.
*
* @property KILL_SIGNAL
* @type String
* @static
*/
exports.KILL_SIGNAL = KILL_SIGNAL;
/**
* Use this property in conjunction with promptAndContinue.
*
* @property PROMPT_PASSWORD
* @type String
* @static
*/
exports.PROMPT_PASSWORD = PROMPT_PASSWORD;
/**
* Use this property in conjunction with promptAndContinue.
*
* @property PROMPT_CONFIRM
* @type String
* @static
*/
exports.PROMPT_CONFIRM = PROMPT_CONFIRM;
/**
* Use this property in conjunction with promptAndContinue.
*
* @property PROMPT_PROMPT
* @type String
* @static
*/
exports.PROMPT_PROMPT = PROMPT_PROMPT;
// -- P R I V A T E M E T H O D S
function _getHomeDir() {
return process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
}
// -- P U B L I C M E T H O D S
/**
* Convert milliseconds to human readeable format.
*
* @method formatMillisecondsToHuman
* @param {Number} ms The milliseconds time to format.
* @param {Number} precision The number of digit for the ms part, default to 3.
* @param {String} separator The separator string, default to ', '.
* @return {String} str A well formatted duration (ex: 7h, 2m, 5s, 6.123ms).
*/
exports.formatMillisecondsToHuman = _formatMillisecondsToHuman = function (ms, precision, separator) {
var
duration = moment.duration(ms, 'milliseconds'),
arrElapse = [];
precision = precision || DEFAULT_TIME_PRECISION;
separator = separator || ', ';
arrElapse = [
duration.get('days') ? duration.get('days') + 'd' : '',
duration.get('hours') ? duration.get('hours') + 'h' : '',
duration.get('minutes') ? duration.get('minutes') + 'm' : '',
duration.get('seconds') ? duration.get('seconds') + 's' : '',
duration.get('ms') ? duration.get('ms').toFixed(precision) + 'ms' : ''
];
return _.compact(arrElapse).join(separator);
};
/**
* Start or stop a timer.
* Stopping the timer will also log the result in minutes, seconds and milliseconds.
* The default introductory text ('Elapsed time:') can be overridden.
*
* @method timeTracker
* @param {String} type The timeTracker action type ('start' or 'stop')
* @param {String} [label] The introductory text to display when the timer stops.
* Default to "Elapsed time: "
* @param {Boolean} quiet Flag to turn logging on or off.
* Default to on.
* When off, the method returns an object
* with message and duration.
*
* @example
* utilities.timeTracker('start');
* longRunningTask();
* utilities.timeTracker('stop');
*/
exports.timeTracker = function (type, label, quiet) {
var
MILLI = 1000,
NANO = 1000000,
elapsedTotal,
formattedDuration,
intro;
if (type === 'start') {
timeTrackerStart = process.hrtime();
return timeTrackerStart;
}
if (type === 'stop') {
elapsedTotal = process.hrtime(timeTrackerStart)[0] * MILLI + process.hrtime(
timeTrackerStart)[1] / NANO;
formattedDuration = _formatMillisecondsToHuman(elapsedTotal);
intro = label ? label : 'Elapsed time: ';
if (_.isBoolean(quiet) && quiet === true) {
return {
message: intro,
duration: formattedDuration
};
} else {
log.echo(intro + formattedDuration);
}
// reset the timer
timeTrackerStart = process.hrtime();
return timeTrackerStart;
}
};
/**
* Finds the `wf2/src` path of a wria2 git repository.
*
* @method getWF2srcPath
* @async
* @param {Object} options
* @param {String} options.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 On success, this will be null.
* @param {String} done.srcPath The path to wf2/src of the wria2 repository.
*/
function _getWF2srcPath(options, done) {
var srcPath;
require('./git-helper').findGitRootPath(options, function (err, res) {
if (err || !res.stdout) {
done(err);
} else {
srcPath = path.join(res.stdout, 'wf2', 'src');
if (fs.existsSync(srcPath)) {
done(null, srcPath);
} else {
done(1);
}
}
});
}
/**
* Resolve a path and replace "~" with $HOME if needed
*
* @method resolvePath
* @param {String} pathString A path that can contain '~'.
* @return {String} A resolved path.
*/
exports.resolvePath = function (pathString) {
if (pathString) {
return path.resolve(pathString.replace('~', _getHomeDir()));
}
};
/**
* Checks if the current OS is Windows based.
*
* @method isWindows
* @return {Boolean} True on if the OS is Windows.
*/
function _isWindows() {
return (process.platform === 'win32');
}
exports.isWindows = _isWindows;
/**
* Wrap a string given a maximum length, trying not to break words.
*
* @method wordWrap
* @param {String} str The string to wrap
* @param {String} [width=75] The maximum length of the desired width.
* @return {Array} An array of wrapped strings.
*/
exports.wordWrap = function (str, width) {
var regex;
width = width || 75;
if (!str) {
return str;
}
regex = '.{1,' + width + '}(\\s|$)|\\S+?(\\s|$)';
return str.match(new RegExp(regex, 'g'));
};
/**
* Transform a string to setCamelCase.
*
* @method setCamelCase
* @param {String} input The string to convert to camelCase.
* @return {String} The input string converted into camelCase format.
*/
exports.setCamelCase = function (input) {
var str = input.toLowerCase().replace(/-(.)/g, function (match, group1) {
return group1.toUpperCase();
});
return str.charAt(0).toUpperCase() + str.slice(1);
};
/**
* Runs `npm install` in the provided folder if `node_modules` doesn't exist.
*
* @method installLocalNpmPackages
* @async
* @param {String} srcPath The path where a package.json file exists.
*
* @param {Function} done Callback to execute when done. It gets 1 argument:
* @param {String} done.error On success, this will be null.
*/
exports.installLocalNpmPackages = function (srcPath, options, done) {
var
cmdline,
yarnConfig = config.getKey(config.FEDTOOLSRCKEYS.yarnvsnpm) || 'yarn',
isYarnAvailable = _isAppInstalled({
name: 'yarn'
}),
force = false,
packageJson = path.join(srcPath, 'package.json'),
nodeModulesPath = path.join(srcPath, 'node_modules');
// For API compatibility, options may be the callback...
if (options && typeof options === 'function' && !done) {
done = options;
} else if (options && options.force) {
force = true;
}
// checking if there is a package.json file first...
if (!fs.existsSync(packageJson)) {
return done(null);
}
// otherwise install npms!
if (!fs.existsSync(nodeModulesPath) || force) {
if (yarnConfig === 'yarn' && isYarnAvailable) {
cmdline = 'yarn';
} else {
cmdline = 'npm install';
}
cmd.run(cmdline, {
status: true,
verbose: false,
pwd: srcPath
}, function (err, stderr) {
if (err) {
log.error('Unable to install npm packages!');
done(err, stderr);
} else {
done(null);
}
});
} else {
done(null);
}
};
/**
* Finds and create a temporary folder based on the OS. If /repo exists,
* it will be used on Linux/Mac.
*
* @method getTemporaryDir
* @param {String} subDir Optional: an extra sub folder to append to the
* provided temporary folder.
* @param {String} rootDir Optional: force the root path (instead of /tmp or
* /repo or whatever the system is providing)
* @return {String} Path to a temporary folder.
*/
exports.getTemporaryDir = function (subDir, rootDir) {
var
osTmpDir, tmpDir;
if (_.isString(rootDir) && fs.existsSync(rootDir)) {
osTmpDir = path.resolve(rootDir);
} else {
// forcing /repo or /tmp on linux and mac
if (process.platform === 'linux' || process.platform === 'darwin') {
if (fs.existsSync('/repo')) {
osTmpDir = '/repo';
} else {
osTmpDir = '/tmp';
}
} else {
osTmpDir = osenv.tmpdir();
}
}
tmpDir = path.join(osTmpDir, 'fedtools-tmp');
if (subDir) {
tmpDir = path.join(tmpDir, subDir);
}
fs.ensureDirSync(tmpDir);
return tmpDir;
};
/**
* Finds the HOME directory based on the OS.
*
* @method getHomeDir
* @return {String} Path to the home directory.
*/
exports.getHomeDir = _getHomeDir;
/**
* Helper method to update the framework version string in all the relevant files.
* It updates `.shifter.json` and uses Maven for the rest.
* This method must be run within a WF-RIA2 folder.
* It will display the current version and prompt the user to enter a new one.
*
* __NOTE__: local combo loader
*
* if the user enters 'build' or 'combo', the `replace-wf2_combopath` key in the
* `.shifter.json` file will be updated with `../../../wria/combo?basePath=build&`
*
* @method wria2bump
* @async
*
* @param {Function} done Callback to execute when done. It gets 1 argument:
* @param {String} done.error On success, this will be null.
*/
exports.wria2bump = function (done) {
var
shifterCfg,
yuiDocCfg,
currentVersion;
_getWF2srcPath({
cwd: process.cwd()
}, function (err, srcPath) {
var
shifterJsonFile,
yuiDocJsonFile,
questions;
if (!err && srcPath) {
shifterJsonFile = path.join(srcPath, '.shifter.json');
yuiDocJsonFile = path.join(srcPath, 'yuidoc.json');
questions = {
type: 'input',
name: 'version',
message: 'Type the new version number you want to set: ',
validate: function (val) {
if (!val) {
return 'Version cannot be empty...';
}
return true;
}
};
if (!fs.existsSync(shifterJsonFile)) {
log.error(
'Ooops! It looks like you\'re missing a .shifter.json configuration file!');
return done(-1);
} else {
shifterCfg = JSON.parse(fs.readFileSync(shifterJsonFile, 'utf8'));
currentVersion = shifterCfg['replace-wf2_version'];
log.info('The current version is: ', currentVersion);
inquirer.prompt(questions).then(function (answers) {
var
newVersion = answers.version;
shifterCfg['replace-wf2_version'] = newVersion;
// special combo loader
// if version is 'build' or 'combo', replace
// ../../../wria/combo?basePath=@WF2_VERSION@/build&
// with
// ../../../wria/combo?basePath=build&
if (newVersion === 'build' || newVersion === 'combo') {
shifterCfg['replace-wf2_combopath'] =
'../../../wria/combo?basePath=build&';
} else
if (shifterCfg['replace-wf2_combopath'] ===
'../../../wria/combo?basePath=build&') {
log.notice('Restoring replace-wf2_combopath...');
shifterCfg['replace-wf2_combopath'] =
'../../../wria/combo?basePath=@WF2_VERSION@/build&';
}
// also making sure that combo is ON
shifterCfg['replace-wf2_combine'] = 'true';
// as well as cache
shifterCfg['replace-wf2_cache_modules'] = 'true';
fs.writeFileSync(shifterJsonFile,
JSON.stringify(shifterCfg, null, NB_SPACES_FOR_TAB));
// time to update yuidoc.json, but only if the version tag exists
if (fs.existsSync(yuiDocJsonFile)) {
yuiDocCfg = JSON.parse(fs.readFileSync(yuiDocJsonFile, 'utf8'));
if (yuiDocCfg && yuiDocCfg.project && yuiDocCfg.project.version !== '') {
yuiDocCfg.project.version = newVersion;
fs.writeFileSync(yuiDocJsonFile,
JSON.stringify(yuiDocCfg, null, NB_SPACES_FOR_TAB));
}
}
async.waterfall([
function (callback) {
var cmdline = 'mvn versions:set -DnewVersion=' + newVersion +
' -DgenerateBackupPoms=false';
cmd.run(cmdline, {
pwd: path.join(srcPath, '..', '..')
}, function (err, stderr, stdout) {
callback(err, {
stderr: stderr,
stdout: stdout
});
});
}
], function (err, data) {
var
stdout, stderr;
if (data && data.stdout) {
stdout = data.stdout;
}
if (data && data.stderr) {
stderr = data.stderr;
}
if (!err) {
log.echo();
log.info('All files have been updated with the new version.');
log.info('Make sure it looks fine, then stage, commit and push!');
log.info('You can commit by typing the following:');
log.echo();
log.echo(' git commit -am "Version bump to ' + newVersion +
'. NO TICKET"');
log.echo();
}
done(err, stderr, stdout);
});
});
}
} else {
log.error('Is the current folder a wria2 path?');
log.echo();
return done(-1);
}
});
};
/**
* Checks if a program or a list of programs are available.
*
* @method isAppInstalled
* @param {Array|Object} options An object or an array of objects.
* @param {String} options.name The name of the program to check.
* @param {String} [options.error] An optional error message to display if
* the given program cannot be found.
* @return {Boolean} True on success (all programs are available).
*
* @example
* utilities.isAppInstalled([{
* name: 'mvn',
* error: 'Maven is not installed on this machine'
* }, {
* name: 'java',
* error: 'Java cannot be executed on this machine'
* }]);
*/
function _isAppInstalled(options) {
var
result = true;
if (!_.isObject(options) && !_.isArray(options)) {
throw 'Invalid argument type';
}
if (options && !_.isArray(options)) {
options = [options];
}
options.forEach(function (option) {
var
appExec = shelljs.which(option.name);
if (!appExec) {
result = false;
if (option.error) {
log.error(option.error);
}
} else {
log.debug(appExec);
}
});
return result;
};
/**
* Helper method to send a POSIX signal to a running process.
*
* @method sendSignal
* @param {String|Number} pid The recipient process id.
* @param {String} signal The signal to send.
* @return {Boolean} True on success.
*/
exports.sendSignal = function (pid, signal) {
try {
return process.kill(pid, signal);
} catch (e) {
return false;
}
};
/**
* Helper method to send an email. It is hard coded to use the local WF SMTP host.
*
* @method sendEmail
* @async
* @param {Object} options
* @param {String} options.attachments The path + filename of a local file to attach.
* @param {String} options.from The email 'From' field.
* @param {String|Array} options.to The email 'To' field (can be a string or an array of string).
* @param {String} options.subject The email 'Subject' field.
* @param {String} options.htmlBody The email 'Body' field in HTML format.
*
* @param {Function} done Callback to execute when done. It gets 1 argument:
* @param {String} done.error On success, this will be null.
*/
exports.sendEmail = function (options, done) {
// create reusable transport method (opens pool of SMTP connections)
var mailOptions,
transporter,
attachments,
nodemailer = require('nodemailer'),
htmlToText = require('nodemailer-html-to-text').htmlToText;
if (process.env.FEDTOOLS_PASSWORD) {
if (process.env.FEDTOOLS_PASSWORD === 'direct') {
transporter = nodemailer.createTransport({
port: 1024
});
} else {
transporter = nodemailer.createTransport({
service: 'Gmail',
auth: {
user: 'wfportal@gmail.com',
pass: process.env.FEDTOOLS_PASSWORD
}
});
}
} else {
transporter = nodemailer.createTransport({
host: config.getKey(config.FEDTOOLSRCKEYS.smtpserver) || 'localhost'
});
}
// plug html to text into transporter
transporter.use('compile', htmlToText());
if (options.attachments) {
attachments = [{
filename: path.basename(options.attachments),
path: options.attachments
}];
}
// setup e-mail data with unicode symbols
mailOptions = {
from: options.from || 'Fedtools <arno.versini@wellsfargo.com>',
to: options.to,
subject: options.subject,
html: options.htmlBody,
attachments: attachments ? attachments : []
};
// ignore unauthorized certificate errors
// (https://github.com/nodemailer/nodemailer/issues/357)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
// send mail with defined transport object
transporter.sendMail(mailOptions, function (error) {
if (error) {
log.red(error);
}
transporter.close(); // shut down the connection pool, no more messages
if (typeof done === 'function') {
done();
}
});
};
/**
* Helper method to move a file. It uses pipes to circumvent the `mv` limits on
* temporary folders (like /tmp).
*
* @method copyThenEraseSync
* @param {String} src The source path + file to move.
* @param {String} dst The destination path + file.
*/
exports.copyThenEraseSync = function (src, dst) {
var is = fs.createReadStream(src),
os = fs.createWriteStream(dst);
is.pipe(os);
is.on('end', function () {
fs.unlinkSync(src);
});
};
/**
* Retrieve the current version of OSX.
*
* @method getOSXVersion
* @async
*
* @param {Function} done Callback to execute when done. It gets 2 arguments:
* @param {String} done.error On success, this will be null.
* @param {String} done.version On success, this will be the OSX version.
*/
exports.getOSXVersion = function (done) {
if (require('os').type() === 'Darwin') {
cmd.run('sw_vers -productVersion', {
status: false
}, function (err, stderr, stdout) {
if (!err && stdout) {
done(null, stdout.replace(/\n$/, ''));
} else {
done(err, stderr, stdout);
}
});
}
};
/**
* Tries to open a given URL in the default browser.
*
* @method openInBrowser
* @async
*
* @param {Object} options
* @param {String} options.url The URL to open.
* @param {Boolean} [options.confirm=false] If true, prompts the user if they want to open the URL.
* @param {String} [options.message] The message to display if the user is prompted.
* @param {Boolean} [options.server] If false, do not suggest CTRL+C to stop the server.
*
* @param {Function} done Callback to execute when done. It gets 1 argument:
* @param {String} done.error On success, this will be null.
*/
exports.openInBrowser = function (options, done) {
var
cmdline,
_openURL,
destUrl = url.parse(options.url),
questions = {},
cb = done || function () {
// nothing to declare
},
msg = options.message || 'Open URL in your default browser?';
_openURL = function (arg, cb) {
cmd.run(arg, {
detached: true,
verbose: false,
status: false
}, cb);
};
if (!destUrl.host) {
// not a url, maybe a local file?
// in that case, need to wrap it with quotes
options.url = '"' + options.url + '"';
}
switch (process.platform) {
case 'darwin':
cmdline = 'open ' + options.url;
break;
case 'linux':
cmdline = 'xdg-open ' + options.url;
break;
case 'win32':
cmdline = 'start ' + options.url;
break;
default:
cmdline = 'open ' + options.url;
}
if (cmdline) {
if (_.isBoolean(options.confirm) && options.confirm) {
questions = {
type: 'confirm',
name: 'goodtogo',
message: msg,
default: true
};
inquirer.prompt(questions).then(function (answers) {
if (answers.goodtogo) {
log.echo('Opening...');
_openURL(cmdline, cb);
} else {
if (_.isBoolean(options.server) && options.server === false) {
log.echo('Bye then...');
} else {
log.echo('Bye then... (remember, CTRL+C to stop it)');
}
return cb();
}
});
} else {
_openURL(cmdline, cb);
}
} else {
log.warning('Unable to open the URL...');
return cb();
}
};
/**
* Method to obfuscate a string. This is NOT secure!
* The intend is to simply have a string that is not clearly
* visible just by reading it.
*
* @method obfuscate
* @param {String} string The string to obfuscate.
* @return {String} An obfuscated string.
*/
exports.obfuscate = function (string) {
// TODO: deprection notice in node 6 for new Buffer
// https://github.com/nodejs/node/pull/4682
return new Buffer(string).toString('base64');
};
/**
* Method to un-obfuscate a string. This is NOT secure!
* The intend is to simply have a string that is not clearly
* visible just by reading it.
*
* @method unObfuscate
* @param {String} string The string to unObfuscate.
* @return {String} A readeable string.
*/
exports.unObfuscate = function (string) {
// TODO: deprection notice in node 6 for new Buffer
// https://github.com/nodejs/node/pull/4682
return new Buffer(string, 'base64').toString();
};
exports.unicorn = function () {
// var unicornAscii = fs.readFileSync(path.join(__dirname, '..', 'data', 'unicorn.txt'), 'utf8');
// log.echo();
// log.echo(unicornAscii);
// log.echo();
return;
};
/**
* Method to toggle the console.xxx output to a file.
* It actually only take into account log, info and error.
*
* @method toggleConsole
* @param [String] file The filename of the file where to write
* the redirected logs. With no parameter,
* it re-enables the original log methods.
*/
exports.toggleConsole = function (options) {
var
logFile;
function _redirectToFile() {
return function () {
var msg = util.format.apply(this, arguments);
logFile.write(msg + '\n');
};
}
if (_.isObject(options) && options.file) {
logFile = fs.createWriteStream(options.file, {
flags: 'w'
});
console.log = _redirectToFile();
console.info = _redirectToFile();
console.error = _redirectToFile();
if (options.stderr) {
process.stderr.write = _redirectToFile();
}
if (options.stdout) {
process.stdout.write = _redirectToFile();
}
} else {
// just restoring the original
console.log = consoleMinions.log;
console.error = consoleMinions.error;
console.info = consoleMinions.info;
process.stderr.write = consoleMinions.stderr;
process.stdout.write = consoleMinions.stdout;
}
};
/**
* Harmless sudo access to prompt for password (if needed).
* This is useful to call before actually calling a real
* command that requires sudo access: it prevents messing
* up the display.
*
* @method forceAdminAccess
* @async
* @param {Function} done The callback to be called once
* the password has been entered.
*/
exports.forceAdminAccess = function (verbose, done) {
verbose = verbose || false;
if (process.platform === 'win32') {
return done();
}
if (process.getuid() !== 0) {
if (verbose) {
log.echo();
log.notice('You may have to provide your password for admin access...');
log.echo();
}
cmd.run('sudo test 1', {
status: false
}, done);
}
};
/**
* Method to automatically generate a HISTORY card based
* on git commits and tags.
*
* @method getHistoryCard
* @async
* @param {Object} options Configuration object.
* @param {String} options.cwd The path to run the git log on.
* @param {Array} options.ignore Array of strings to ignore from the
* commit logs.
* @param {Function} done The callback to be called once
* the HISTORY cards has been generated.
*/
exports.getHistoryCard = function (options, done) {
var
i,
len,
tag,
tagTitle,
sep = '~~~',
stopSign = '~stop~',
format = '',
cmdline,
ignore,
filteredTags,
fcts = [],
result = [],
MR_FILTER = [
'Merge branch \'.*\' into \'release-2..*\'',
'Merge branch \'.*\' into \'develop\''
],
AUTHOR_INDEX = 1,
TITLE_INDEX = 2,
BODY_INDEX = 3,
SHA_INDEX,
TIMESTAMP_INDEX;
require('./git-helper').getAllTags({}, function (err, res) {
if (!err && res && res.length) {
len = res.length;
if (options.ignore && options.ignore.length) {
ignore = new RegExp(options.ignore.join('|'));
}
filteredTags = _.filter(res, function (data) {
if (data.match('rc') || data.match('sp')) {
return false;
} else {
return true;
}
});
if (options.timestamp) {
format += sep + '%at';
TITLE_INDEX++;
AUTHOR_INDEX++;
BODY_INDEX++;
TIMESTAMP_INDEX = 1;
}
if (options.sha) {
format += sep + '%H';
TITLE_INDEX++;
AUTHOR_INDEX++;
BODY_INDEX++;
SHA_INDEX = (TIMESTAMP_INDEX === 1) ? 1 + 1 : 1;
}
format += sep + '%an' + sep + '%s' + sep + '%b';
len = filteredTags.length;
for (i = 0; i < len; i++) {
if (filteredTags[i] && filteredTags[i + 1]) {
// git log filteredTags[0].tag...filteredTags[1].tag --pretty=format:"(%an): %s" some/path
// --pretty=format:"%an~~~%s~~~%b~stop~"
cmdline = 'git log ' + filteredTags[i] + '...' + filteredTags[i + 1] +
' --pretty=format:"' + format + stopSign + '" ' + options.cwd;
tag = filteredTags[i];
tagTitle = (i === 0) ? 'HEAD' : tag;
(function (c, t, tt) {
fcts.push(function (callback) {
cmd.run(c, {
status: false
}, function (err, stderr, stdout) {
if (!err) {
stdout = _.map(stdout.split(stopSign), function (item) {
var buff = item.split(sep);
if (!buff) {
return;
}
// we only want to see MR commits
if (options.merge) {
if (buff[TITLE_INDEX] && buff[TITLE_INDEX].match(MR_FILTER.join('|'))) {
return {
author: buff[AUTHOR_INDEX],
title: (buff[TITLE_INDEX]) ? buff[TITLE_INDEX].replace('\n', '') : '',
body: (buff[BODY_INDEX]) ? buff[BODY_INDEX] : '',
timestamp: TIMESTAMP_INDEX ? buff[TIMESTAMP_INDEX] : '',
sha: SHA_INDEX ? buff[SHA_INDEX] : ''
};
}
} else {
// we want to ignore some entries
if ((ignore && buff[TITLE_INDEX] && buff[TITLE_INDEX].match(ignore)) ||
(!buff[AUTHOR_INDEX]) || (!buff[TITLE_INDEX])) {
return;
}
// and if we're here, no more filtering, just return the values
return {
author: buff[AUTHOR_INDEX],
title: (buff[TITLE_INDEX]) ? buff[TITLE_INDEX].replace('\n', '') : '',
body: (buff[BODY_INDEX]) ? buff[BODY_INDEX] : '',
timestamp: TIMESTAMP_INDEX ? buff[TIMESTAMP_INDEX] : '',
sha: SHA_INDEX ? buff[SHA_INDEX] : ''
};
}
});
result.push({
tag: t,
tagTitle: tt,
logs: _.compact(stdout)
});
}
callback(null);
});
});
})(cmdline, tag, tagTitle);
}
}
async.waterfall(fcts, function (err) {
done(err, result);
});
} else {
return done(err);
}
});
};
/**
* Method that checks if the current folder is a framework component
*
* @method isComponentPath
* @async
* @param {String} folder The folder to check.
* @param {Function} done Callback to be called once the verification
* is done. Arguments will not be null if the
* folder is not a component.
*/
exports.isComponentPath = function (folder, done) {
var
evidences = [
path.join(folder, 'build.json'),
path.join(folder, 'meta'),
path.join(folder, '..', 'wf2')
];
async.waterfall([
function (callback) {
fs.exists(evidences[0], function (err) {
if (err === true) {
err = null;
}
callback(err);
});
},
function (callback) {
fs.exists(evidences[1], function (err) {
if (err === true) {
err = null;
}
callback(err);
});
},
function (callback) {
fs.exists(evidences[2], function (err) {
if (err === true) {
err = null;
}
callback(err);
});
}
], function (err) {
done(err);
});
};
/**
* Method that encrypt or decrypt a given file.
*
* @method cryptographer
* @async
* @param {Object} options Configuration object.
* @param {String} options.file The file to encrypt/decrypt.
* @param {String} options.output The output file - if not provided, writes to stdout.
* @param {Boolean} options.status Flag to show command status or not.
* @param {Boolean} options.encrypt True to encrypt, false to decrypt.
* @param {String} options.password The password used to encrypt/decrypt the file.
* If not provided, it will be prompted on the CLI.
* @param {Function} done Callback to be called once the process is done. Argument
* will be null if the encryption/decryption process is successful.
*/
exports.cryptographer = function (options, done) {
var
file,
promptMsg;
function _getPassword(password, msg, cb) {
var
questions = {
type: 'password',
name: 'password',
message: msg || 'Enter password:',
validate: function (val) {
if (!val) {
return 'Password cannot be empty...';
}
return true;
}
};
if (!password) {
inquirer.prompt(questions).then(function (answers) {
cb(null, answers.password);
});
} else {
return cb(null, password);
}
}
function _encrypt(password, buffer) {
var
cipher = crypto.createCipher(CRYPTO_ALGO, password),
crypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
return crypted;
}
function _decrypt(password, buffer) {
var
decipher = crypto.createDecipher(CRYPTO_ALGO, password),
dec = Buffer.concat([decipher.update(buffer), decipher.final()]);
return dec;
}
if (!options.file || !fs.existsSync(options.file)) {
throw 'Invalid argument, file is missing';
} else {
file = path.resolve(options.file);
}
if (!options.output) {
// no output, let's use stdout and disable status logging.
options.status = false;
}
if (options.encrypt) {
promptMsg = 'Enter password to encrypt file:';
} else {
promptMsg = 'Enter password to decrypt file:';
}
_getPassword(options.password, promptMsg, function (err, pass) {
if (err) {
throw err;
}
if (options.encrypt) {
if (options.status) {
log.info('Encrypting file...');
}
fs.readFile(file, function (err, data) {
if (err) {
throw err;
}
if (!options.output) {
process.stdout.write(_encrypt(pass, data));
return done(null, options);
} else {
fs.writeFile(options.output, _encrypt(pass, data), function (err) {
if (err) {
throw err;
}
if (options.status) {
log.success(path.basename(file) + ' was successfully encrypted.');
log.echo('Encrypted file is ', options.output);
}
return done(null, options);
});
}
});
} else {
if (options.status) {
log.info('Decrypting file...');
}
fs.readFile(file, function (err, data) {
if (err) {
throw err;
}
if (!options.output) {
process.stdout.write(_decrypt(pass, data));
return done(null, options);
} else {
fs.writeFile(options.output, _decrypt(pass, data), function (err) {
if (err) {
throw err;
}
if (options.status) {
log.success(path.basename(file) + ' was successfully decrypted.');
log.echo('Decrypted file is ', options.output);
}
return done(null, options);
});
}
});
}
});
};
/**
* Method that removes lines from a file.
*
* @async
* @param {Object} options Configuration object.
* @param {String} options.file The file to remove the lines from. It has to exist.
* @param {String|Array} options.lines The lines to remove (a single string or an array of strings).
* @param {Boolean} options.write Default to true. Updates the file accordingly if true.
* @param {Function} done Callback to be called once the process is done. 1st argument
* will be null if the process is successful. 2nd argument is a string
* representation of the updated file (with the removed lines).
*/
function _removeLinesInFile(options, done) {
var
buffer,
found,
res,
newBuffer = [],
write = (_.isBoolean(options.write)) ? options.write : true;
if (!fs.existsSync(options.file)) {
return done(null, '');
}
if (!_.isArray(options.lines)) {
options.lines = [options.lines];
}
buffer = fs.readFileSync(options.file, 'utf8').split('\n');
_.each(buffer, function (line) {
found = false;
_.each(options.lines, function (val) {
if (val === line) {
found = true;
}
});
if (!found) {
// pattern not found, the line should not be removed
newBuffer.push(line);
}
});
res = newBuffer.join('\n');
if (write) {
fs.writeFileSync(options.file, res);
}
done(null, res);
}
/**
* Method that appends lines to a file.
* If the lines already exist in the file, they will be moved at
* the bottom of the file.
*
* @async
* @method appendLinesInFile
* @param {Object} options Configuration object.
* @param {String} options.file The file to remove the lines from. It has to exist.
* @param {String|Array} options.lines The lines to append (a single string or an array of strings).
* @param {Boolean} options.write Default to true. Updates the file accordingly if true.
* @param {Boolean} options.endWithNewLine Add extra new line if true.
* @param {appendLinesInFileCB} cb - The callback that handles the response.
*/
/**
* @callback appendLinesInFileCB
* @param {String} err Null if the process is successful.
* @param {String} buffer String representation of the file with new lines.
*/
exports.appendLinesInFile = function (options, done) {
var
write = (_.isBoolean(options.write)) ? options.write : true;
if (!_.isArray(options.lines)) {
options.lines = [options.lines];
}
// before adding files, let's make sure they are not in
// the file already, and if they are, remove them...
_removeLinesInFile({
lines: options.lines,
file: options.file,
write: false
}, function (err, body) {
if (err) {
throw err;
}
if (body) {
body += '\n' + options.lines.join('\n');
} else {
body = options.lines.join('\n');
}
if (options.endWithNewLine) {
body += '\n';
}
if (write) {
fs.writeFileSync(options.file, body);
}
done(null, body);
});
};
/**
* Checks if a folder is writable by the calling process.
*
* @async
* @method isFolderWritable
* @param {String|Array} dirnames The folder path(s) to check (has to be full path).
* @param {Function} done Callback to be called once the process is done. 1st argument
* will be null if the folder(s) is (are) writable.
*/
exports.isFolderWritable = function (dirnames, done) {
var
writableFlag = fs.W_OK || fs.constants.W_OK,
dirs = dirnames;
if (_.isString(dirnames)) {
dirs = [dirnames];
}
if (_.isArray(dirs)) {
async.each(dirs, function (item, callback) {
fs.access(item, writableFlag, function (err) {
if (err && err.code === 'EACCES') {
return callback(err);
}
if (_isWindows()) {
fs.stat(item, function (err) {
if (err && err.code === 'EPERM') {
return callback(err);
}
return callback(null);
});
} else {
return callback(null);
}
});
}, function (err) {
done(err);
});
} else {
return done(1);
}
};
/**
* Finds the location where node modules are globaly installed.
*
* @method getGlobalNodeModulesPath
* @return {String} loc The location of global node modules.
*/
exports.getGlobalNodeModulesPath = function () {
var res = cmd.run('npm root -g', {
status: false
});
if (res.code === 0 && res.stdout) {
return res.stdout.replace(/\n$/, '');
} else {
return '';
}
};
exports.getHelp = function (debug, options) {
var
i = 0,
MAX_OPTIONS = 10,
namespace,
_options = [];
options.i18n.loadPhrases(path.resolve(__dirname, '..', 'data', 'i18n', 'utilities'));
if (options.type === 'bump' || options.type === 'simpleserver') {
namespace = 'utilities.help.' + options.type;
}
if (namespace) {
for (i = 0; i < MAX_OPTIONS; i += 1) {
_options.push({
option: options.i18n.t(namespace + '.options.' + i + '.option'),
desc: options.i18n.t(namespace + '.options.' + i + '.desc')
});
}
return {
namespace: namespace,
synopsis: options.i18n.t(namespace + '.synopsis'),
command: options.i18n.t(namespace + '.command'),
description: options.i18n.t(namespace + '.description'),
options: _options,
examples: options.i18n.t(namespace + '.examples')
};
}
return {};
};
exports.removeLinesInFile = _removeLinesInFile;
exports.getWF2srcPath = _getWF2srcPath;
exports.isAppInstalled = _isAppInstalled;
exports.git = require('./git-helper');
exports.simpleserver = require('./simple-http-server/server');
exports.smtpserver = require('./simple-smtp-server/smtp').smtpserver;
exports.progress = require('./third/progress');
exports.targz = require('./third/targz');
exports.performance = require('./third/performance');
exports.readdir = require('./third/readdir');