'use strict';
var spawn = require('child_process').spawn,
concat = require('concat-stream'),
which = require('which').sync,
assert = require('assert'),
EventEmitter2 = require('eventemitter2').EventEmitter2,
Promise = require('bluebird'),
resolvable = require('./resolvable');
/**
* The event emmiter for GitFTW
*
* * Fires `command` the git command executed
*
* * Fires `result` the result from the command line
*
* @example
* var git = require('gitftw');
*
* //Add a listener to the issued git command. Output it
* git.events.on('command', console.info);
*
* //Add a listener to the result of the git command. Output it with >
* git.events.on('result', function(res) {
* console.log('> ' + res.split('\n').join('\n> '))
* });
*
* @memberof git
* @name events
* @type {EventEmmiter2}
*/
var events = new EventEmitter2();
/**
* The full path to git shell, lazy loaded
*
* @private
* @type {String}
*/
var gitCmd,
executionPromise,
executionCount = 0;
/**
* Reads an stream
*
* @private
* @param {Stream} stream
* @returns {Promise} resolves with the value
*/
function readStream(stream) {
return new Promise(function(resolve) {
stream.pipe(concat(function(data) {
resolve(data.toString().trim());
}));
});
}
/**
* Creates a promise for a Git Execution
*
* @private
* @param {Resolvable|Array<String|null>} args The arguments to pass to git command
* @returns {Promise}
*/
function createGitCmdPromise(args) {
return new Promise(function(resolve, reject) {
//Remove null values from the final git arguments
args = args.filter(function(arg) {
/*jshint eqnull:true */
return arg != null;
});
/**
* @name git#command
* @event
* @param {String} String the command issued
*/
events.emit('command', [gitCmd].concat(args).join(' '));
var proc = spawn(gitCmd, args);
var output = Promise.join(readStream(proc.stdout), readStream(proc.stderr), function(stdout, stderr) {
//Some warnings (like code === 1) are in stdout
//fatal are in stderr. So try both
return stderr || stdout;
}).tap(function(output) {
/**
* @name git#result
* @event
* @param {String} String the captured output
*/
events.emit('result', output);
});
proc.on('close', function(code) {
//Some weird behaviours can arise with stdout and stderr values
//cause writting to then is sync in *nix and async in windows.
//Also, excessive treatment or long outputs that cause a drain
//in the stderr & stdout streams, could lead us to having this proc
//closed (and this callback called), and no complete values captured
//in an eventual closure variable set then we emit the 'result' event
//So using promises solves this syncronization
output.then(function(output) {
if (code !== 0) {
var error = new Error('git exited with an error');
error.code = code;
error.output = output;
reject(error);
} else {
resolve(output);
}
});
});
proc.on('error', function(error) {
reject(new Error(error));
});
});
}
/**
* Spawns a git process with the provider arguments
* This function is called when you call directly the require of `gitftw`
*
* If you provide a second parameter
*
* DISCLAIMER: I've not found any way to document this in jsdoc and this
* template in a proper way. Sorry for the possible missunderstanding
*
* @example
* var git = require('gitftw');
*
* //executes a `git version`
* git(['version'], function(err, data) { console.log(data);});
* //or
* git(['version']).then(console.log);
*
* @fires command the git command executed
* @fires result the result from the command line
*
* @param {Resolvable|Array<String|null>} args The arguments to pass to git command
* @param {callback} [cb] The execution callback result
* @returns {Promise} Promise Resolves with the git output
* Rejects with an invalid/not found git cmd
* Rejects with an error with the git cmd spawn
* Rejects with git exits with an error
*/
function spawnGit(args) {
//don't bother with throws, they are catched by promises
gitCmd = gitCmd || which('git');
assert.ok(args, 'arguments to git is mandatory');
executionCount++;
if (executionPromise) {
executionPromise = executionPromise.then(
createGitCmdPromise.bind(null, args),
createGitCmdPromise.bind(null, args)
);
} else {
executionPromise = createGitCmdPromise(args);
}
return executionPromise.finally(function() {
if (--executionCount === 0) {
executionPromise = null;
}
});
}
/**
* Decorates a function resolving its resolvables before calling it,
* adding node callback api
*
* @private
* @param {Function} fn The function to decorate
* @param {Object} [options] The options object passed to the command
* @param {callback} [cb] Callback used when in callback mode
* @returns {Promise|undefined} A promise when in promise API
*/
function decorator(fn, options, cb) {
if (typeof options === 'function') {
cb = options;
options = null;
}
return resolvable(options)
.then(fn)
.nodeify(cb);
}
/**
* The module function
*
* It Spawns a child process for the git command line, capturing the output
*
* @example
* var git = require('gitftw');
*
* //Execute a `git version`
* git(['version'], function(err, data) { console.log(data);});
* //or
* git(['version']).then(console.log);
*
* @see {@link spawnGit} for a detailed description
* @namespace
*
*/
var git = decorator.bind(null, spawnGit);
/**
* Creates a {@link command} in this module.
* Third party developers must use it to create their owns
* parameters
*
* @memberof git
* @alias command
* @param {command} fn The named function implementing a command
* @returns {Function} function The gitftw module, for chainability
*/
function createCommand(fn) {
assert.ok(fn.name, 'commands must be named functions');
git[fn.name] = decorator.bind(null, fn);
return git;
}
Object.defineProperty(git, 'command', {
value: createCommand,
enumerable: false,
writable: false
});
Object.defineProperty(git, 'events', {
value: events,
enumerable: false,
writable: false
});
module.exports = git;
/**
* A gitftw command implementation.
*
* It only receives as parameter an options object.
* The options object is {@link Resolvable|resolved} before reaching your
* implementation
*
* The dual exported API (for Promises and node callbacks) is also managed for you.
* Therefore you only have to deal with one paradigm in the implementation: The promises one.
* It's safe to throw errors, and you have to return a promise or a value.
*
* For most use cases, you dont have to known too much of promises. Probably you will have to
* call the `git([arguments])` that creates a promise, and you only will have to deal with
* the output parsing and return it
*
* One extra thing: It must be a named function. The implemented command available in the
* `gitftw` module, will be that name function.
*
* @example Creating a command
*
* var git = require('gitftw');
*
* //implement a command as a named function
* function doSomethingAwesome(options) {
* //All properties in the options object are resolved here.
* //read the docs about what an *optional* `resolvable` concept is
* var resolvable = git.getCurrentBranch;
* //issue a `git awesome master param`
* return git(['awesome', resolvable, options.name])
* .then(parseAwesomeCommandResult);
* }
*
* //implement an optional command parsing
* function parseAwesomeCommandResult(res) {
* var lines = res.split('\n');
* if (lines[0] !== 'expected result') {
* //feel free to throw. It will be catched by the
* //promise engine and rejected or callbacked with err for you
* throw new Error('unable to parse the awesome');
* }
* //If everything goes well, return the command output
* return lines[1];
* }
*
* //register a command
* git.command(doSomethingAwesome);
*
* //Now it's available in the git module
* git.doSomethingAwesome({
* //available in the command implementation as options.name
* name: 'param'
* };
*
* @example Creating commands in your module
* //your custom commands in mycommands.js
* module.exports = function(git) {
* //doSomethingAwesome is defined elsewhere
* git.command(doSomethingAwesome);
*
* //for chainable api: return git itself
* return git;
* }
*
* //////
* The userland: using your commands
* var git = require('gitftw');
* require('./mycommands')(git)
*
* git.doSomethingAwesome();
*
* @callback command
* @param {Object} options The options to this command. All its properties are {@link Resolvable}
* @returns {Promise} Promise The execution promise
*/
/**
* A Typical node callback
*
* @callback callback
* @param {Error|null} err The error (if any)
* @param {*|undefined} result The result of executing the command
*/