Source: commands.js

'use strict';

var assert = require('assert'),
    Promise = require('bluebird');

/**
 *
 *
 * @param {Function} git The gitftw module
 * @returns {Function} The gitftw with the new commands
 */
module.exports = function commonCommands(git) {
  [
    commit,
    version,
    clone,
    add,
    push,
    pull,
    checkout,
    merge,
    fetch,
    getCurrentBranch,
    getRemoteBranches,
    getLocalBranches,
    removeLocalBranch,
    removeRemoteBranch,
    isClean,
    tag,
    getTags,
    removeLocalTags,
    removeRemoteTags,
    removeTags
  ].forEach(git.command);

  return git;

  //////////// Commands implementation

  /**
   * Gets current installed git version
   * Executes `git version`
   *
   * @example
   * var git = require('gitftw');
   *
   * git.version().then(console.log) //outputs 1.8.2.3
   *
   *
   * @memberof git
   * @type {command}
   * @returns {Promise} Promise Resolves with the git version
   */
  function version() {
    var args = [
      'version'
    ];

    return git(args)
        .then(parseVersion);
  }

  /**
   * Commits the staging area
   * Executes `git commit -m "First commit"`
   *
   * It does not fail when there is not anything to commit
   *
   * @example
   * var git = require('gitftw');
   *
   * git.commit({
   *   message: 'First commit'
   * });
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|String} options.message The commit message
   * @param {Resolvable|Boolean} [options.force] Replace the tip of the current branch by creating
   *   a new commit. The --amend flag
   * @param {Resolvable|Boolean} [options.noVerify] This option bypasses the pre-commit
   *   and commit-msg hooks
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with undefined
   */
  function commit(options) {
    assert.ok(options.message, 'message is mandatory');

    var args = [
      'commit',
      options.force ? '--amend' : null,
      options.noVerify ? '-n' : null,
      options.message ? '-m' : null,
      options.message ? options.message : null
    ];

    return git(args)
        .catch(passWarning)
        .then(silent);
  }

  /**
   * Clones a git repo
   *
   * If both a branch and a tag are specified, the branch takes precedence
   *
   * @example
   * var git = require('gitftw');
   *
   * git.clone({
   *  repository: 'git@github.com:jmendiara/node-lru-cache.git',
   *  directory: './cache' //optional
   * });
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|String} options.repository The git repository endpoint
   * @param {Resolvable|String} [options.directory] The directory where the repo will be cloned
   *   By default, git will clone it a new one specifed by the repo name
   * @param {Resolvable|String} [options.branch] The remote repo branch to checkout after clone.
   * @param {Resolvable|String} [options.tag] The remote repo tag to checkout after clone.
   * @param {Resolvable|String} [options.origin] Instead of using the remote name origin to keep
   *   track of the upstream repository, use this parameter value
   * @param {Resolvable|Boolean} [options.recursive] After the clone is created, initialize all
   * @param {Resolvable|Boolean} [options.bare] Make a bare Git repository.: neither remote-tracking
   *   branches nor the related configuration variables are created.
   * @param {Resolvable|Number} [options.depth] Create a shallow clone with a history truncated to
   *   the specified number of revisions.
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with undefined
   */
  function clone(options) {
    assert.ok(options.repository, 'repository is mandatory');

    var branchOrTag = options.branch || options.tag;
    var args = [
      'clone',
      options.repository,
      options.directory,
      branchOrTag ? ('-b' + branchOrTag) : null,
      options.origin ? ('-o' + options.origin) : null,
      options.recursive ? '--recursive' : null,
      options.bare ? '--bare' : null,
      options.depth ? '--depth' : null,
      options.depth ? '' + options.depth : null
    ];

    return git(args)
        .then(silent);
  }

  /**
   * Adds filenames to the stashing area
   * Issues `git add README.md`
   *
   * @example
   * var git = require('gitftw');
   *
   * git.add({
   *   files: ['README.md', 'index.js']
   * });
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|Array<String>} options.files The files to be added, relative to the cwd
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with undefined
   */
  function add(options) {
    assert.ok(options.files, 'files is mandatory');

    return options.files
        .filter(function(file) {
          //Git exits OK with empty filenames.
          //Avoid an unnecessary call to git in these cases by removing the filename
          return !!file;
        })
        .reduce(function(soFar, file) {
          var args = ['add', file];
          return soFar
              .then(gitFn(args))
              .then(silent);
        }, Promise.resolve());
  }

  /**
   * Push the change sets to server
   * Executes `git push origin master`
   *
   * Defaults to "origin", and don't follow configured refspecs
   * for the upstream
   *
   * If both a branch and a tag are specified, the branch takes precedence
   *
   * @example
   * var git = require('gitftw');
   *
   * git.push(); //the current branch to `origin`
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|String} [options.remote="origin"] The remote ref to push to
   * @param {Resolvable|String} [options.branch="HEAD"] The branch to push. HEAD will push the
   *   current branch
   * @param {Resolvable|String} [options.tag] The tag to push
   * @param {Resolvable|Boolean} [options.force] Force a remote update. Can cause the remote
   *   repository to lose commits; use it with care. --force flag
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with undefined
   */
  function push(options) {
    var branchOrTag = options.branch || options.tag;

    var args = [
      'push',
      options.remote || 'origin',
      branchOrTag || 'HEAD',
      options.force ? '--force' : null
    ];

    return git(args)
        .then(silent);
  }

  /**
   * Pulls a remote branch into the current one
   * Executes `git pull origin master`
   *
   * remote defaults to "origin", and don't follow configured refspecs
   * for the upstream
   *
   * If both a branch and a tag are specified, the branch takes precedence
   *
   * When no branch and tag are specifies, this command will try
   * to pull the actual local branch name from the remote
   *
   * @example
   * var git = require('gitftw');
   *
   * //While in master...
   * git.getCurrentBranch().then(console.log)
   * //Outputs: master
   *
   * //Pulls origin/master into the current branch (master)
   * git.pull()
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|String} [options.remote="origin"] The remote
   * @param {Resolvable|String} [options.branch=currentBranch] The remote branch to pull
   * @param {Resolvable|String} [options.tag] The remote tag to pull
   * @param {Resolvable|Boolean} [options.rebase] Make a rebase (--rebase tag)
   * @param {callback} [cb] The execution callback result
   * @return {Promise} Resolves with undefined
   */
  function pull(options) {
    options = options || {};

    var branchOrTag = options.branch || options.tag;

    var args = [
      'pull',
      options.rebase ? '--rebase' : null,
      options.remote || 'origin',
      branchOrTag || git.getCurrentBranch
    ];

    return git(args)
        .then(silent);
  }

  /**
   * Checkout a local branch
   *
   * Executes `git checkout -B issues/12`
   *
   * If you specify create, it will try to create the branch,
   * or will checkout it if it already exists
   *
   * If both a branch and a tag are specified, the branch takes precedence
   *
   * Cannot use create and orphan both together
   *
   * @example
   * var git = require('gitftw');
   *
   * git.checkout({
   *   branch: 'master'
   * });
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|String} options.branch The branch to checkout
   * @param {Resolvable|String} [options.tag] The tag to checkout
   * @param {Resolvable|Boolean} [options.create] Try to create the branch (-B flag)
   * @param {Resolvable|Boolean} [options.oldCreate] Try to create the branch (-b flag). Do
   *  not use along with 'create'.
   * @param {Resolvable|Boolean} [options.orphan] Create an orphan branch (--orphan flag)
   * @param {Resolvable|Boolean} [options.force] When switching branches, proceed even if
   *  the index or the working tree differs from HEAD. This is used to throw
   *  away local changes. (-f flag)
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with undefined
   */
  function checkout(options) {
    var branchOrTag = options.branch || options.tag;

    assert.ok(branchOrTag, 'branch or tag is mandatory');

    if ((options.create || options.oldCreate) && options.orphan) {
      throw new Error('create and orphan cannot be specified both together');
    }

    if (options.create && options.oldCreate) {
      throw new Error('create and oldCreate cannot be specified both together');
    }

    var args = [
      'checkout',
      options.create ? '-B' : options.oldCreate ? '-b' : null,
      options.orphan ? '--orphan' : null,
      branchOrTag,
      options.force ? '-f' : null
    ];

    return git(args)
        .then(silent);
  }

  /**
   * Merges branches a branch in the current one
   * Executes `git merge --no-ff origin/issues13 -m "Remote branch merge"
   *
   * @example
   * var git = require('gitftw');
   *
   * //while in master...
   * git.merge({
   *   branch: 'issue/12',
   *   message: 'Merge branch issue/12 into master'
   * });
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|String} [options.remote] The remote where the branch is located
   * @param {Resolvable|String} options.branch The branch to merge
   * @param {Resolvable|String} options.message The merge message
   * @param {Resolvable|Boolean} [options.noFF] Make a no fast forward merge,
   *  creating a new sha (--no-ff flag)
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with undefined
   */
  function merge(options) {
    assert.ok(options.branch, 'branch is mandatory');
    assert.ok(options.message, 'message is mandatory');

    var args = [
      'merge',
      options.noFF ? '--no-ff' : null,
      options.remote ? (options.remote + '/' + options.branch) : options.branch,
      '-m',
      options.message
    ];

    return git(args)
        .then(silent);
  }

  /**
   * Fetches a remote
   * Executes `git fetch origin --tags`
   *
   * remote defaults to "origin", and don't follow configured refspecs
   * for the upstream
   *
   * @example
   * var git = require('gitftw');
   *
   * git.fetch(); //fetches origin
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|String} [options.remote="origin"] The remote to fetch
   * @param {Resolvable|Boolean} [options.tags] Fetch the tags (--tags flag)
   * @param {Resolvable|Boolean} [options.prune] remove any remote-tracking references that no
   *   longer exist on the remote (--prune)
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with undefined
   */
  function fetch(options) {
    options = options || {};

    var args = [
      'fetch',
      options.remote || 'origin',
      options.tags ? '--tags' : null,
      options.prune ? '--prune' : null
    ];

    return git(args)
        .then(silent);
  }

  /**
   * Gets the current branch
   * Executes `git rev-parse --abbrev-ref HEAD`
   *
   * @example
   * var git = require('gitftw');
   *
   * //while in master...
   * git.getCurrentBranch().then(console.log); //outputs 'master';
   *
   * @memberof git
   * @type {command}
   *
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with the current branch
   */
  function getCurrentBranch() {
    var args = [
      'rev-parse',
      '--abbrev-ref',
      'HEAD'
    ];

    return git(args);
  }

  /**
   * Removes a local branch
   *
   * @example
   * var git = require('gitftw');
   *
   * //while in master...
   * git.removeLocalBranch({
   *   branch: 'master'
   * });
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|String} options.branch The branch name
   * @param {Resolvable|Boolean} [options.force] force the delete (-D)
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with undefined
   */
  function removeLocalBranch(options) {
    assert.ok(options.branch, 'branch is mandatory');

    var args = [
      'branch',
      options.force ? '-D' : '-d',
      options.branch
    ];

    return git(args)
        .then(silent);
  }

  /**
   * Removes a remote branch
   *
   * @example
   * var git = require('gitftw');
   *
   * //while in master...
   * git.removeRemoteBranch({
   *   branch: 'master'
   * });
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|String} options.branch The branch name
   * @param {Resolvable|String} [options.remote="origin"] The remote ref where the branch will be removed
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with undefined
   */
  function removeRemoteBranch(options) {
    assert.ok(options.branch, 'branch is mandatory');

    var args = [
      'push',
      options.remote || 'origin',
      ':' + options.branch
    ];

    return git(args)
        .then(silent);
  }

  /**
   * Gets the current workspace is clean or has something in the working tree
   * Executes `git diff-index --quiet HEAD .`
   *
   * @example
   * var git = require('gitftw');
   *
   * git.isClean()
   *     .then(function(clean) {
   *       if (clean) {
   *         console.log('The git workspace is clean');
   *       } else {
   *         console.log('The git workspace is dirty');
   *       }
   *     });
   *
   * @memberof git
   * @type {command}
   *
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with true if the workspace is clean.
   *   false otherwise
   */
  function isClean() {
    var args = [
      'diff-index',
      '--quiet',
      'HEAD',
      '.'
    ];

    return git(args)
        .then(function() {
          return true;
        })
        .catch(function(err) {
          if (err.code === 1) {
            return false;
          }
          return Promise.reject(err);
        });
  }

  /**
   * Gets all remote branches in a remote
   * Executes `git ls-remote --heads origin`
   *
   * @example
   * var git = require('gitftw');
   *
   * //remove local tags in 'origin'
   * git.getRemoteBranches({
   *   remote: 'upstream'
   * });
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|String} [options.remote="origin"] The remote ref to ask for branches
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with Array<String>
   */
  function getRemoteBranches(options) {
    options = options || {};
    var args = [
      'ls-remote',
      '--heads',
      options.remote || 'origin'
    ];

    return git(args)
        .then(parseRemoteBranches);

  }

  /**
   * Gets all local branches
   * Executes ` git for-each-ref --format='%(refname:short)' refs/heads/`
   *
   * @example
   * var git = require('gitftw');
   *
   * //remove local tags in 'origin'
   * git.getLocalBranches();
   *
   * @memberof git
   * @type {command}
   *
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with Array<String>
   */
  function getLocalBranches() {
    var args = [
      'for-each-ref',
      '--format=\%(refname:short)',
      'refs/heads/'
    ];

    return git(args)
        .then(parseMultiline);
  }

  /**
   * Creates a git tag
   * Executes `git tag v1.0.0 -m "v1.0.0" -a`
   *
   * @example
   * var git = require('gitftw');
   *
   * git.tag({
   *  tag: 'v1.2.0'
   * })
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|String} options.tag The tag name
   * @param {Resolvable|String} [options.message] The tag message. Mandatory when creating
   *  an annotated tag
   * @param {Resolvable|Boolean} [options.annotated] Should this tag be annotated?
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with undefined
   */
  function tag(options) {
    assert.ok(options.tag, 'tag name is mandatory');
    if (options.annotated) {
      assert.ok(options.message, 'message is mandatory when creating an annotated tag');
    }

    var args = [
      'tag',
      options.tag,
      options.annotated ? '-a' : null,
      options.message ? '-m' : null,
      options.message ? options.message : null
    ];

    return git(args)
        .then(silent);
  }

  /**
   * Gets the local tags
   * Executes `git tag`
   *
   * @example
   * var git = require('gitftw');
   *
   * git.getTags().then(console.log); //outputs ['v1.0.0', 'v1.0.1']
   *
   * @memberof git
   * @type {command}
   *
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with the array of tags
   */
  function getTags() {
    var args = [
      'tag'
    ];

    return git(args)
        .then(parseMultiline);
  }

  /**
   * Removes a set of tags from the local repo
   * Executes `git tag -d v1.0.0`
   *
   * It does not fail when the tag does not exists
   *
   * @example
   * var git = require('gitftw');
   *
   * //remove all local tags
   * git.removeLocalTags({
   *   tags: git.getTags //It's a resolvable. You can specify also ['v1.0.0', 'v1.0.1']
   * });
   *
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|Array<String>} options.tags The tags to remove
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with undefined
   */
  function removeLocalTags(options) {
    assert.ok(options.tags, 'tags is mandatory');

    return options.tags
        .reduce(function(soFar, tag) {
          var args = [
            'tag',
            '-d',
            tag
          ];
          return soFar
              .then(gitFn(args))
              .catch(passWarning)
              .then(silent);
        }, Promise.resolve());
  }

  /**
   * Removes a tag from the remote repo
   * Executes `git push origin :refs/tags/v1.0.0`
   *
   * @example
   * var git = require('gitftw');
   *
   * //remove local tags in 'origin'
   * git.removeRemoteTags({
   *   tags: git.getTags //It's a resolvable. You can specify also ['v1.0.0', 'v1.0.1']
   * });
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|String} [options.remote="origin"] The remote ref where the tag will be removed
   * @param {Resolvable|Array<String>} options.tags The tags to remove
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with undefined
   */
  function removeRemoteTags(options) {
    assert.ok(options.tags, 'tag is mandatory');

    return options.tags
        .reduce(function(soFar, tag) {
          var args = [
            'push',
            options.remote || 'origin',
            ':refs/tags/' + tag
          ];

          return soFar
              .then(gitFn(args))
              .then(silent);
        }, Promise.resolve());
  }

  /**
   * Removes a set of tags from the remote and local repo
   *
   * @example
   * var git = require('gitftw');
   *
   * //remove all local tags in local and remote 'origin'
   * git.removeTags({
   *   tags: git.getTags //It's a resolvable. You can specify also ['v1.0.0', 'v1.0.1']
   * });
   *
   * @memberof git
   * @type {command}
   *
   * @param {Object} options The options object. All its properties are {@link Resolvable}
   * @param {Resolvable|String} [options.remote="origin"] The remote ref where the tag will be removed
   * @param {Resolvable|Array<String>} options.tags The tags to remove
   * @param {callback} [cb] The execution callback result
   * @returns {Promise} Promise Resolves with undefined
   */
  function removeTags(options) {
    return Promise.resolve()
        .then(git.removeLocalTags.bind(null, options))
        .then(git.removeRemoteTags.bind(null, options));
  }

  //////////////// Utilities

  /**
   * An identity fn over git to make more functional style code
   * inside promises
   *
   * @param {Array}  args
   * @returns {Function}
   */
  function gitFn(args) {
    return function() {
      return git(args);
    };
  }
};

/**
 * Analizes the error given and rejects when it's not a warning
 *
 * @private
 * @param {Error} err The recoverable error
 * @returns {String|Promise} the error message String when its a recoverable error
 *  a rejected promise otherwise
 */
function passWarning(err) {
  if (err.code === 1) {
    //This is a recoverable error. We dont wanna fail
    return err.output;
  }

  return Promise.reject(err);
}

/**
 * Parses the git version
 * @private
 * @param {Resolvable|String} str The 'git version' command output
 * @throws {Error} parsing error
 * @returns {String} the version number
 */
function parseVersion(str) {
  var match = /git version ([0-9\.]+)/.exec(str);

  if (match) {
    return match[1];
  } else {
    throw new Error('Unable to parse version response', str);
  }
}

/**
 * Parses multiline git result
 * @private
 * @param {String} str A multiline command output
 * @returns {Array<String>} the single lines
 */
function parseMultiline(str) {
  if (!str) {
    return [];
  }

  return str.split(/\n/);
}

/**
 * Parses ls-remote --heads
 * @private
 * @param {String} str The 'git ls-remote --heads origin' command output
 * @returns {Array<String>} the remote branches
 */
function parseRemoteBranches(str) {
  var res = [],
      regex = /[\w]{40}\s+refs\/heads\/(.+)/g,
      match;

  while ((match = regex.exec(str)) != null) {
    if (match.index === regex.lastIndex) {
      regex.lastIndex++;
    }
    res.push(match[1]);
  }

  if (!res.length) {
    throw new Error('Unable to parse ls-remote response', str);
  }
  return res;
}

/**
 * A function used to remove the commands output
 * @private
 */
function silent() {
  return '';
}