git-shizzle
Version:
Does git shizzle
265 lines (220 loc) • 6.62 kB
JavaScript
;
var debug = require('diagnostics')('git-shizzle')
, formatter = require('git-format')
, shelly = require('shelljs')
, fuse = require('fusing')
, path = require('path');
//
// Latest upgrade of shelljs does not honor the { silent } when it encounters
// an error. We have to globally silent the utility in order for the messages
// to stop.
//
shelly.config.silent = true;
/**
* Create a human readable interface for interacting with the git binary that is
* installed on the users host system. This allows us to interact with the git
* in the given directory.
*
* The beauty of this system is that it allows human readable chaining:
*
* ```js
* git().push('origin master');
* ```
*
* @constructor
* @param {String} dir The directory in which we should execute these commands.
* @api public
*/
function Git(dir) {
if (!(this instanceof Git)) return new Git();
this.fuse();
this.__dirname = dir;
var git = this;
this.parse = this._parsers.reduce(function reduce(parsers, parser) {
parsers[parser.method] = function proxy(params, fn) {
var async = 'function' === typeof arguments[arguments.length - 1]
, args = parser.params +' '
, format;
if ('string' === typeof params) {
args += params;
} else if ('function' === typeof params) {
fn = params;
}
//
// Extract the format so we can use it in our parser.
//
format = formatter.extract(args) || '';
args = formatter.reformat(args);
if (!async) return parser.fn(git[parser.cmd || parser.method](args), format, formatter);
return git[parser.cmd || parser.method](args, function async(code, output) {
if (+code) return fn(code, output);
fn(code, parser.fn(output, format, formatter));
});
};
return parsers;
}, Object.create(null));
}
fuse(Git);
/**
* List of all commands that are available for git.
*
* @type {Array}
* @public
*/
Git.commands = [];
/**
* The path to the `git` binary.
*
* @type {String}
* @public
*/
try {
Git.path = shelly.which('git').stdout;
} catch (e) {
shelly.echo(e);
shelly.echo('This environment does not have a git binary');
}
//
// This is where all the magic happens. We're going to extract all the commands
// that this `git` binary supports and introduce them as API's on the prototype.
var help = ['git', Git.path].reduce(function find(memo, path) {
if (memo) return memo;
var exec = shelly.exec(path + ' help -a', { silent: true });
if (exec.stdout) {
Git.path = path;
return exec.stdout;
}
return memo;
}, '');
help.split(/([\w|\-]+)\s{2,}/g).filter(function filter(line) {
var trimmed = line.trim();
//
// Assume that every command is lowercase, this \w in the RegExp also includes
// uppercase or mixed case strings. Which can actually capture $PATH\n in the
// output. Additionally we need to remove all lines that still have spaces
// after they're trimmed.
//
return trimmed.length
&& !~trimmed.indexOf(' ')
&& line === line.toLowerCase();
}).map(function map(line) {
return line.trim();
}).forEach(function each(cmd) {
var method = cmd
, index;
//
// Some these methods contain dashes, it's a pain to write git()['symbolic-ref']
// so we're transforming these cases to JS compatible method name.
//
while (~(index = method.indexOf('-'))) {
method = [
method.slice(0, index),
method.slice(index + 1, index + 2).toUpperCase(),
method.slice(index + 2)
].join('');
}
/**
* Execute the introduced/parsed command.
*
* @param {String} params Additional command line flags.
* @param {Function} fn Completion callback if you want async support.
* @returns {String}
* @api public
*/
Git.readable(method, function proxycmd(params, fn) {
var git = Git.path +' '+ cmd +' '
, format;
if ('string' === typeof params) git += params;
if ('function' === typeof params) fn = params;
shelly.cd(this.__dirname);
debug('executing cmd', git);
var res = shelly.exec(git.trim(), { silent: true }, fn ? function cb(code, stdout, stderr) {
if (+code) return fn(new Error((stderr || 'Incorrect code #'+ code).trim()));
fn(undefined, stdout);
} : undefined);
//
// Make sure we throw a code in sync mode instead of returning the error
// body.
//
if (!fn && +res.code) {
throw new Error((res.stderr || 'Incorrect code #'+ res.code).trim());
}
return res.stdout || '';
});
Git.commands.push(cmd);
});
/**
* CD in to another directory.
*
* @type {Function}
* @returns {Git}
* @api public
*/
Git.readable('cd', function (dir) {
this.__dirname = path.join(this.__dirname, dir);
debug('updated the directory to', this.__dirname);
return this;
});
/**
* Add a new parser
*
* @param {String} method Name of the method we parse output from
* @param {String} args The function params/args.
* @param {Function} fn The parser function.
* @returns {Git}
* @api private
*/
Git.writable('_parsers', []);
Git.parse = function parse(method, data, fn) {
data.params = data.params || data.args;
data.method = data.method || method;
data.fn = data.fn || fn;
Git.prototype._parsers.push(data);
return Git;
};
/**
* Parse a list of tags out of the git logs.
*
* @type {Function}
* @public
*/
Git.parse('tags', {
params: '--date-order --tags --simplify-by-decoration --pretty=format:"%ai %h %d %s %cr %ae"',
cmd: 'log'
}, function parse(output, format, formatter) {
return output.split(/\n/).map(function map(line) {
var meta = formatter(line, format);
//
// Only return if it's an actual tag.
//
if (~meta.ref.indexOf('tag:')) return meta;
}).filter(Boolean);
});
/**
* Parse the changes out of git log.
*
* @type {Function}
* @public
*/
Git.parse('changes', {
params: '--name-status --pretty=format:"%ai %h %d %s %cr %ae"',
cmd: 'log'
}, function parse(output, format, formatter) {
var commited = { M: 'modified', A: 'added', D: 'deleted' }
, sep = formatter.separator;
return output.split(/\n{2}/).slice(10).map(function reformat(line) {
var changes = line.slice(line.lastIndexOf(sep) + sep.length).trim()
, meta = formatter(line, format);
meta.changes = changes.split(/\n/).reduce(function reduce(memo, change) {
var type = commited[change.charAt(0)];
if (!memo[type]) memo[type] = [];
memo[type].push(change.slice(2));
return memo;
}, {});
return meta;
});
});
//
// Expose the module.
//
module.exports = Git;