"use strict";

var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();

function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); }

var fse = require("fs-extra");
var fp = require("lodash/fp");
var NodeGit = require("../");
var Blob = NodeGit.Blob;
var Checkout = NodeGit.Checkout;
var Commit = NodeGit.Commit;
var normalizeOptions = NodeGit.Utils.normalizeOptions;
var shallowClone = NodeGit.Utils.shallowClone;
var path = require("path");
var Filter = NodeGit.Filter;
var FilterList = NodeGit.FilterList;
var Reference = NodeGit.Reference;
var Remote = NodeGit.Remote;
var Repository = NodeGit.Repository;
var Revwalk = NodeGit.Revwalk;
var Status = NodeGit.Status;
var StatusFile = NodeGit.StatusFile;
var StatusList = NodeGit.StatusList;
var Submodule = NodeGit.Submodule;
var Tag = NodeGit.Tag;
var Tree = NodeGit.Tree;
var TreeBuilder = NodeGit.Treebuilder;

var _discover = Repository.discover;
var _initExt = Repository.initExt;
var _fetchheadForeach = Repository.prototype.fetchheadForeach;
var _mergeheadForeach = Repository.prototype.mergeheadForeach;

function applySelectedLinesToTarget(originalContent, newLines, pathHunks, isStaged, reverse) {
  // 43: ascii code for '+'
  // 45: ascii code for '-'
  var lineTypes = {
    ADDED: !reverse ? 43 : 45,
    DELETED: !reverse ? 45 : 43
  };
  var newContent = "";
  var oldIndex = 0;
  var linesPromises = [];

  var oldLines = originalContent.toString().split("\n");

  // if no selected lines were sent, return the original content
  if (!newLines || newLines.length === 0) {
    return originalContent;
  }

  function lineEqualsFirstNewLine(hunkLine) {
    return hunkLine.oldLineno() === newLines[0].oldLineno() && hunkLine.newLineno() === newLines[0].newLineno();
  }

  function processSelectedLine(hunkLine) {
    // if this hunk line is a selected line find the selected line
    var newLine = newLines.filter(function (nLine) {
      return hunkLine.oldLineno() === nLine.oldLineno() && hunkLine.newLineno() === nLine.newLineno();
    });

    if (hunkLine.content().indexOf("\\ No newline at end of file") !== -1) {
      return false;
    }

    // determine what to add to the new content
    if (isStaged && newLine && newLine.length > 0 || !isStaged && (!newLine || newLine.length === 0)) {
      if (hunkLine.origin() !== lineTypes.ADDED) {
        newContent += hunkLine.content();
      }
      if (isStaged && hunkLine.origin() !== lineTypes.DELETED || !isStaged && hunkLine.origin() !== lineTypes.ADDED) {
        oldIndex++;
      }
    } else {
      switch (hunkLine.origin()) {
        case lineTypes.ADDED:
          newContent += hunkLine.content();
          if (isStaged) {
            oldIndex++;
          }
          break;
        case lineTypes.DELETED:
          if (!isStaged) {
            oldIndex++;
          }
          break;
        default:
          newContent += oldLines[oldIndex++];
          if (oldIndex < oldLines.length) {
            newContent += "\n";
          }
          break;
      }
    }
  }

  // find the affected hunk
  pathHunks.forEach(function (pathHunk) {
    linesPromises.push(pathHunk.lines());
  });

  return Promise.all(linesPromises).then(function (results) {
    for (var i = 0; i < results.length && newContent.length < 1; i++) {
      var hunkStart = isStaged || reverse ? pathHunks[i].newStart() : pathHunks[i].oldStart();
      var lines = results[i];
      if (lines.filter(lineEqualsFirstNewLine).length > 0) {
        // add content that is before the hunk
        while (hunkStart > oldIndex + 1) {
          newContent += oldLines[oldIndex++] + "\n";
        }

        // modify the lines of the hunk according to the selection
        lines.forEach(processSelectedLine);

        // add the rest of the file
        while (oldLines.length > oldIndex) {
          newContent += oldLines[oldIndex++] + (oldLines.length > oldIndex ? "\n" : "");
        }
      }
    }

    return newContent;
  });
}

function getPathHunks(repo, index, filePath, isStaged, additionalDiffOptions) {
  var diffOptions = additionalDiffOptions ? {
    flags: additionalDiffOptions
  } : undefined;

  return Promise.resolve().then(function () {
    if (isStaged) {
      return repo.getHeadCommit().then(function getTreeFromCommit(commit) {
        return commit.getTree();
      }).then(function getDiffFromTree(tree) {
        return NodeGit.Diff.treeToIndex(repo, tree, index, diffOptions);
      });
    }

    return NodeGit.Diff.indexToWorkdir(repo, index, {
      flags: NodeGit.Diff.OPTION.SHOW_UNTRACKED_CONTENT | NodeGit.Diff.OPTION.RECURSE_UNTRACKED_DIRS | (additionalDiffOptions || 0)
    });
  }).then(function (diff) {
    return NodeGit.Status.file(repo, filePath).then(function (status) {
      if (!(status & NodeGit.Status.STATUS.WT_MODIFIED) && !(status & NodeGit.Status.STATUS.INDEX_MODIFIED)) {
        return Promise.reject("Selected staging is only available on modified files.");
      }
      return diff.patches();
    });
  }).then(function (patches) {
    var pathPatch = patches.filter(function (patch) {
      return patch.newFile().path() === filePath;
    });

    if (pathPatch.length !== 1) {
      return Promise.reject("No differences found for this file.");
    }

    return pathPatch[0].hunks();
  });
}

function getReflogMessageForCommit(commit) {
  var parentCount = commit.parentcount();
  var summary = commit.summary();
  var commitType;

  if (parentCount >= 2) {
    commitType = " (merge)";
  } else if (parentCount == 0) {
    commitType = " (initial)";
  } else {
    commitType = "";
  }

  return "commit" + commitType + ": " + summary;
}

/**
 * Goes through a rebase's rebase operations and commits them if there are
 * no merge conflicts
 *
 * @param {Repository}  repository    The repository that the rebase is being
 *                                    performed in
 * @param {Rebase}      rebase        The current rebase being performed
 * @param {Signature}   signature     Identity of the one performing the rebase
 * @param {Function}    beforeNextFn  Callback to be called before each
 *                                    invocation of next(). If the callback
 *                                    returns a promise, the next() will be
 *                                    called when the promise resolves.
 * @param {Function}   beforeFinishFn Callback called before the invocation
 *                                    of finish(). If the callback returns a
 *                                    promise, finish() will be called when the
 *                                    promise resolves. This callback will be
 *                                    provided a detailed overview of the rebase
 * @return {Int|Index} An error code for an unsuccesful rebase or an index for
 *                     a rebase with conflicts
 */
function performRebase(repository, rebase, signature, beforeNextFn, beforeFinishFn) {
  var beforeNextFnResult;

  /* In the case of FF merges and a beforeFinishFn, this will fail
   * when looking for 'rewritten' so we need to handle that case.
   */
  function readRebaseMetadataFile(fileName, continueOnError) {
    return fse.readFile(path.join(repository.path(), "rebase-merge", fileName), { encoding: "utf8" }).then(fp.trim).catch(function (err) {
      if (continueOnError) {
        return null;
      }
      throw err;
    });
  }

  function calcHeadName(input) {
    return input.replace(/refs\/heads\/(.*)/, "$1");
  }

  function getPromise() {
    return rebase.next().then(function () {
      return repository.refreshIndex();
    }).then(function (index) {
      if (index.hasConflicts()) {
        throw index;
      }

      return rebase.commit(null, signature);
    }).then(function () {

      return performRebase(repository, rebase, signature, beforeNextFn, beforeFinishFn);
    }).catch(function (error) {
      if (error && error.errno === NodeGit.Error.CODE.ITEROVER) {
        var calcRewritten = fp.cond([[fp.isEmpty, fp.constant(null)], [fp.stubTrue, fp.flow([fp.split("\n"), fp.map(fp.split(" "))])]]);

        var beforeFinishFnPromise = !beforeFinishFn ? Promise.resolve() : Promise.all([readRebaseMetadataFile("onto_name"), readRebaseMetadataFile("onto"), readRebaseMetadataFile("head-name").then(calcHeadName), readRebaseMetadataFile("orig-head"), readRebaseMetadataFile("rewritten", true).then(calcRewritten)]).then(function (_ref) {
          var _ref2 = _slicedToArray(_ref, 5),
              ontoName = _ref2[0],
              ontoSha = _ref2[1],
              originalHeadName = _ref2[2],
              originalHeadSha = _ref2[3],
              rewritten = _ref2[4];

          return beforeFinishFn({
            ontoName: ontoName,
            ontoSha: ontoSha,
            originalHeadName: originalHeadName,
            originalHeadSha: originalHeadSha,
            rebase: rebase,
            rewritten: rewritten
          });
        });

        return beforeFinishFnPromise.then(function () {
          return rebase.finish(signature);
        });
      } else {
        throw error;
      }
    });
  }

  if (beforeNextFn) {
    beforeNextFnResult = beforeNextFn(rebase);
    // if beforeNextFn returns a promise, chain the promise
    return Promise.resolve(beforeNextFnResult).then(getPromise);
  }

  return getPromise();
}

/**
 * Creates a branch with the passed in name pointing to the commit
 *
 * @async
 * @param {String} startPath The base path where the lookup starts.
 * @param {Number} acrossFs If non-zero, then the lookup will not stop when a
                            filesystem device change is detected while exploring
                            parent directories.
 * @param {String} ceilingDirs A list of absolute symbolic link free paths.
                              the search will stop if any of these paths
                              are hit. This may be set to null
 * @return {String} Path of the git repository
 */
Repository.discover = function (startPath, acrossFs, ceilingDirs, callback) {
  return _discover(startPath, acrossFs, ceilingDirs).then(function (foundPath) {
    foundPath = path.resolve(foundPath);
    if (typeof callback === "function") {
      callback(null, foundPath);
    }
    return foundPath;
  }, callback);
};

// Override Repository.initExt to normalize initoptions
Repository.initExt = function (repo_path, opts) {
  opts = normalizeOptions(opts, NodeGit.RepositoryInitOptions);
  return _initExt(repo_path, opts);
};

Repository.getReferences = function (repo, type, refNamesOnly) {
  return repo.getReferences().then(function (refList) {
    var filteredRefList = refList.filter(function (reference) {
      return type === Reference.TYPE.ALL || reference.type === type;
    });

    if (refNamesOnly) {
      return filteredRefList.map(function (reference) {
        return reference.name();
      });
    }

    return filteredRefList;
  });
};

/**
 * This will set the HEAD to point to the local branch and then attempt
 * to update the index and working tree to match the content of the
 * latest commit on that branch
 *
 * @async
 * @param {String|Reference} branch the branch to checkout
 * @param {Object|CheckoutOptions} opts the options to use for the checkout
 */
Repository.prototype.checkoutBranch = function (branch, opts) {
  var repo = this;

  return repo.getReference(branch).then(function (ref) {
    if (!ref.isBranch()) {
      return false;
    }
    return repo.checkoutRef(ref, opts);
  });
};

/**
 * This will set the HEAD to point to the reference and then attempt
 * to update the index and working tree to match the content of the
 * latest commit on that reference
 *
 * @async
 * @param {Reference} reference the reference to checkout
 * @param {Object|CheckoutOptions} opts the options to use for the checkout
 */
Repository.prototype.checkoutRef = function (reference, opts) {
  var repo = this;
  opts = opts || {};

  opts.checkoutStrategy = opts.checkoutStrategy || NodeGit.Checkout.STRATEGY.SAFE | NodeGit.Checkout.STRATEGY.RECREATE_MISSING;
  return repo.getReferenceCommit(reference.name()).then(function (commit) {
    return commit.getTree();
  }).then(function (tree) {
    return Checkout.tree(repo, tree, opts);
  }).then(function () {
    var name = reference.name();
    return repo.setHead(name);
  });
};

/**
 * Continues an existing rebase
 *
 * @async
 * @param {Signature}  signature     Identity of the one performing the rebase
 * @param {Function}   beforeNextFn  Callback to be called before each step
 *                                   of the rebase. If the callback returns a
 *                                   promise, the rebase will resume when the
 *                                   promise resolves. The rebase object is
 *                                   is passed to the callback.
 * @param {Function}   beforeFinishFn Callback called before the invocation
 *                                    of finish(). If the callback returns a
 *                                    promise, finish() will be called when the
 *                                    promise resolves. This callback will be
 *                                    provided a detailed overview of the rebase
 * @param {RebaseOptions} rebaseOptions Options to initialize the rebase object
 *                                      with
 * @return {Oid|Index}  A commit id for a succesful merge or an index for a
 *                      rebase with conflicts
 */
Repository.prototype.continueRebase = function (signature, beforeNextFn, beforeFinishFn, rebaseOptions) {
  var repo = this;

  var rebase = void 0;
  var promiseChain = Promise.resolve();

  if (!signature) {
    promiseChain = promiseChain.then(function () {
      return repo.defaultSignature();
    }).then(function (signatureResult) {
      signature = signatureResult;
    });
  }

  return promiseChain.then(function () {
    return repo.refreshIndex();
  }).then(function (index) {
    if (index.hasConflicts()) {
      throw index;
    }

    return NodeGit.Rebase.open(repo, rebaseOptions);
  }).then(function (_rebase) {
    rebase = _rebase;
    return rebase.commit(null, signature).catch(function (e) {
      // If the first commit on continueRebase is a
      // "patch already applied" error,
      // interpret that as an explicit "skip commit"
      // and ignore the error.
      var errno = fp.get(["errno"], e);
      if (errno === NodeGit.Error.CODE.EAPPLIED) {
        return;
      }

      throw e;
    });
  }).then(function () {
    return performRebase(repo, rebase, signature, beforeNextFn, beforeFinishFn);
  }).then(function (error) {
    if (error) {
      throw error;
    }

    return repo.getBranchCommit("HEAD");
  });
};

/**
 * Creates a branch with the passed in name pointing to the commit
 *
 * @async
 * @param {String} name Branch name, e.g. "master"
 * @param {Commit|String|Oid} commit The commit the branch will point to
 * @param {Boolean} force Overwrite branch if it exists
 * @return {Reference}
 */
Repository.prototype.createBranch = function (name, commit, force) {
  var repo = this;

  if (commit instanceof Commit) {
    return NodeGit.Branch.create(repo, name, commit, force ? 1 : 0);
  } else {
    return repo.getCommit(commit).then(function (commit) {
      return NodeGit.Branch.create(repo, name, commit, force ? 1 : 0);
    });
  }
};

/**
 * Create a blob from a buffer
 *
 * @async
 * @param {Buffer} buffer
 * @return {Oid}
 */
Repository.prototype.createBlobFromBuffer = function (buffer) {
  return Blob.createFromBuffer(this, buffer, buffer.length);
};

/**
 * Create a commit
 *
 * @async
 * @param {String} updateRef
 * @param {Signature} author
 * @param {Signature} committer
 * @param {String} message
 * @param {Oid|String} Tree
 * @param {Array} parents
 * @return {Oid} The oid of the commit
 */
Repository.prototype.createCommit = function (updateRef, author, committer, message, tree, parents, callback) {

  var repo = this;
  var promises = [];

  parents = parents || [];

  promises.push(repo.getTree(tree));

  parents.forEach(function (parent) {
    promises.push(repo.getCommit(parent));
  });

  return Promise.all(promises).then(function (results) {
    tree = results[0];

    // Get the normalized values for our input into the function
    var parentsLength = parents.length;
    parents = [];

    for (var i = 0; i < parentsLength; i++) {
      parents.push(results[i + 1]);
    }

    return Commit.create(repo, updateRef, author, committer, null /* use default message encoding */
    , message, tree, parents.length, parents);
  }).then(function (commit) {
    if (typeof callback === "function") {
      callback(null, commit);
    }

    return commit;
  }, callback);
};

/**
 * Create a commit
 *
 * @async
 * @param {Signature} author
 * @param {Signature} committer
 * @param {String} message
 * @param {Oid|String} treeOid
 * @param {Array} parents
 * @return {String} The content of the commit object
 *                  as a string
 */
Repository.prototype.createCommitBuffer = function (author, committer, message, treeOid, parents) {

  var repo = this;
  var promises = (parents || []).reduce(function (acc, parent) {
    acc.push(repo.getCommit(parent));
    return acc;
  }, [repo.getTree(treeOid)]);

  return Promise.all(promises).then(function (_ref3) {
    var _ref4 = _toArray(_ref3),
        tree = _ref4[0],
        parentCommits = _ref4.slice(1);

    return Commit.createBuffer(repo, author, committer, null /* use default message encoding */
    , message, tree, parentCommits.length, parentCommits);
  });
};

/**
 * Create a commit that is digitally signed
 *
 * @async
 * @param {String} updateRef
 * @param {Signature} author
 * @param {Signature} committer
 * @param {String} message
 * @param {Tree|Oid|String} Tree
 * @param {Array} parents
 * @param {Function} onSignature Callback to be called with string to be signed
 * @return {Oid} The oid of the commit
 */
Repository.prototype.createCommitWithSignature = function (updateRef, author, committer, message, tree, parents, onSignature) {

  var repo = this;
  var promises = [];
  var commitContent;
  var skippedSigning;

  parents = parents || [];

  promises.push(repo.getTree(tree));

  parents.forEach(function (parent) {
    promises.push(repo.getCommit(parent));
  });

  var createCommitPromise = Promise.all(promises).then(function (results) {
    tree = results[0];

    // Get the normalized values for our input into the function
    var parentsLength = parents.length;
    parents = [];

    for (var i = 0; i < parentsLength; i++) {
      parents.push(results[i + 1]);
    }

    return Commit.createBuffer(repo, author, committer, null /* use default message encoding */
    , message, tree, parents.length, parents);
  }).then(function (commitContentResult) {
    commitContent = commitContentResult;
    if (!commitContent.endsWith("\n")) {
      commitContent += "\n";
    }
    return onSignature(commitContent);
  }).then(function (_ref5) {
    var code = _ref5.code,
        field = _ref5.field,
        signedData = _ref5.signedData;

    switch (code) {
      case NodeGit.Error.CODE.OK:
        return Commit.createWithSignature(repo, commitContent, signedData, field);
      case NodeGit.Error.CODE.PASSTHROUGH:
        skippedSigning = true;
        return Commit.create(repo, updateRef, author, committer, null /* use default message encoding */
        , message, tree, parents.length, parents);
      default:
        {
          var error = new Error("Repository.prototype.createCommitWithSignature " + ("threw with error code " + code));
          error.errno = code;
          throw error;
        }
    }
  });

  if (!updateRef) {
    return createCommitPromise;
  }

  return createCommitPromise.then(function (commitOid) {
    if (skippedSigning) {
      return commitOid;
    }

    return repo.getCommit(commitOid).then(function (commitResult) {
      return Reference.updateTerminal(repo, updateRef, commitOid, getReflogMessageForCommit(commitResult), committer);
    }).then(function () {
      return commitOid;
    });
  });
};

/**
 * Creates a new commit on HEAD from the list of passed in files
 *
 * @async
 * @param {Array} filesToAdd
 * @param {Signature} author
 * @param {Signature} committer
 * @param {String} message
 * @return {Oid} The oid of the new commit
 */
Repository.prototype.createCommitOnHead = function (filesToAdd, author, committer, message, callback) {

  var repo = this;

  return repo.refreshIndex().then(function (index) {
    if (!filesToAdd) {
      filesToAdd = [];
    }

    return filesToAdd.reduce(function (lastFilePromise, filePath) {
      return lastFilePromise.then(function () {
        return index.addByPath(filePath);
      });
    }, Promise.resolve()).then(function () {
      return index.write();
    }).then(function () {
      return index.writeTree();
    });
  }).then(function (treeOid) {
    return repo.getHeadCommit().then(function (parent) {
      if (parent !== null) {
        // To handle a fresh repo with no commits
        parent = [parent];
      }
      return repo.createCommit("HEAD", author, committer, message, treeOid, parent);
    });
  }, callback);
};

/**
 * Creates a new lightweight tag
 *
 * @async
 * @param {String|Oid} String sha or Oid
 * @param {String} name the name of the tag
 * @return {Reference}
 */
Repository.prototype.createLightweightTag = function (oid, name, callback) {
  var repository = this;

  return Commit.lookup(repository, oid).then(function (commit) {
    // Final argument is `force` which overwrites any previous tag
    return Tag.createLightweight(repository, name, commit, 0);
  }).then(function () {
    return Reference.lookup(repository, "refs/tags/" + name);
  });
};

/**
 * Instantiate a new revision walker for browsing the Repository"s history.
 * See also `Commit.prototype.history()`
 *
 * @return {Revwalk}
 */
Repository.prototype.createRevWalk = function () {
  return Revwalk.create(this);
};

/**
 * Creates a new annotated tag
 *
 * @async
 * @param {String|Oid} String sha or Oid
 * @param {String} name the name of the tag
 * @param {String} message the description that will be attached to the
 * annotated tag
 * @return {Tag}
 */
Repository.prototype.createTag = function (oid, name, message, callback) {
  var repository = this;
  var signature = null;

  return repository.defaultSignature().then(function (signatureResult) {
    signature = signatureResult;
    return Commit.lookup(repository, oid);
  }).then(function (commit) {
    // Final argument is `force` which overwrites any previous tag
    return Tag.create(repository, name, commit, signature, message, 0);
  }).then(function (tagOid) {
    return repository.getTag(tagOid, callback);
  });
};

/**
 * Gets the default signature for the default user and now timestamp
 *
 * @async
 * @return {Signature}
 */
Repository.prototype.defaultSignature = function () {
  return NodeGit.Signature.default(this).then(function (result) {
    if (!result || !result.name()) {
      result = NodeGit.Signature.now("unknown", "unknown@example.com");
    }
    return result;
  }).catch(function () {
    return NodeGit.Signature.now("unknown", "unknown@example.com");
  });
};

/**
 * Deletes a tag from a repository by the tag name.
 *
 * @async
 * @param {String} Short or full tag name
 */
Repository.prototype.deleteTagByName = function (name) {
  var repository = this;

  name = ~name.indexOf("refs/tags/") ? name.substr(10) : name;

  return Tag.delete(repository, name);
};

/**
 * Discard line selection of a specified file.
 * Assumes selected lines are unstaged.
 *
 * @async
 * @param {String} filePath The relative path of this file in the repo
 * @param {Array} selectedLines The array of DiffLine objects
 *                            selected for discarding
 * @return {Number} 0 or an error code
 */
Repository.prototype.discardLines = function (filePath, selectedLines, additionalDiffOptions) {
  var repo = this;
  var fullFilePath = path.join(repo.workdir(), filePath);
  var index;
  var originalContent;
  var filterList;

  return repo.refreshIndex().then(function (indexResult) {
    index = indexResult;
    return FilterList.load(repo, null, filePath, Filter.MODE.CLEAN, Filter.FLAG.DEFAULT);
  }).then(function (_filterList) {
    filterList = _filterList;

    if (filterList) {
      return filterList.applyToFile(repo, filePath);
    }

    return fse.readFile(fullFilePath, "utf8");
  }).then(function (content) {
    originalContent = content;
    return getPathHunks(repo, index, filePath, false, additionalDiffOptions);
  }).then(function (hunks) {
    return applySelectedLinesToTarget(originalContent, selectedLines, hunks, false, true);
  }).then(function (newContent) {
    return FilterList.load(repo, null, filePath, Filter.MODE.SMUDGE, Filter.FLAG.DEFAULT).then(function (_filterList) {
      filterList = _filterList;
      if (filterList) {
        /* jshint ignore:start */
        // We need the constructor for the check in NodeGit's C++ layer
        // to accept an object, and this seems to be a nice way to do it
        return filterList.applyToData(new String(newContent));
        /* jshint ignore:end */
      }

      return newContent;
    });
  }).then(function (filteredContent) {
    return fse.writeFile(fullFilePath, filteredContent);
  });
};

/**
 * Fetches from a remote
 *
 * @async
 * @param {String|Remote} remote
 * @param {Object|FetchOptions} fetchOptions Options for the fetch, includes
 *                                           callbacks for fetching
 */
Repository.prototype.fetch = function (remote, fetchOptions, callback) {
  var repo = this;

  function finallyFn(error) {
    if (typeof callback === "function") {
      callback(error);
    }
  }

  return repo.getRemote(remote).then(function (remote) {
    return remote.fetch(null, fetchOptions, "Fetch from " + remote).then(function () {
      return remote.disconnect();
    });
  }).then(finallyFn).catch(function (error) {
    finallyFn(error);
    throw error;
  });
};

/**
 * Fetches from all remotes. This is done in series due to deadlocking issues
 * with fetching from many remotes that can happen.
 *
 * @async
 * @param {Object|FetchOptions} fetchOptions Options for the fetch, includes
 *                                           callbacks for fetching
 * @param {Function} callback
 */
Repository.prototype.fetchAll = function (fetchOptions, callback) {
  var repo = this;

  function createCallbackWrapper(fn, remote) {
    return function () {
      var args = Array.prototype.slice.call(arguments);
      args.push(remote);

      return fn.apply(this, args);
    }.bind(this);
  }

  fetchOptions = fetchOptions || {};

  var remoteCallbacks = fetchOptions.callbacks || {};

  var credentials = remoteCallbacks.credentials;
  var certificateCheck = remoteCallbacks.certificateCheck;
  var transferProgress = remoteCallbacks.transferProgress;

  return repo.getRemoteNames().then(function (remotes) {
    return remotes.reduce(function (fetchPromise, remote) {
      var wrappedFetchOptions = shallowClone(fetchOptions);
      var wrappedRemoteCallbacks = shallowClone(remoteCallbacks);

      if (credentials) {
        wrappedRemoteCallbacks.credentials = createCallbackWrapper(credentials, remote);
      }

      if (certificateCheck) {
        wrappedRemoteCallbacks.certificateCheck = createCallbackWrapper(certificateCheck, remote);
      }

      if (transferProgress) {
        wrappedRemoteCallbacks.transferProgress = createCallbackWrapper(transferProgress, remote);
      }

      wrappedFetchOptions.callbacks = wrappedRemoteCallbacks;

      return fetchPromise.then(function () {
        return repo.fetch(remote, wrappedFetchOptions);
      });
    }, Promise.resolve());
  }).then(function () {
    if (typeof callback === "function") {
      callback();
    }
  });
};

/**
 * @async
 * @param {FetchheadForeachCb} callback The callback function to be called on
 * each entry
 */
Repository.prototype.fetchheadForeach = function (callback) {
  return _fetchheadForeach.call(this, callback, null);
};

/**
 * Retrieve the blob represented by the oid.
 *
 * @async
 * @param {String|Oid} String sha or Oid
 * @return {Blob}
 */
Repository.prototype.getBlob = function (oid, callback) {
  var repository = this;

  return Blob.lookup(repository, oid).then(function (blob) {
    blob.repo = repository;

    if (typeof callback === "function") {
      callback(null, blob);
    }

    return blob;
  }, callback);
};

/**
* Look up a branch. Alias for `getReference`
*
* @async
* @param {String|Reference} name Ref name, e.g. "master", "refs/heads/master"
*                              or Branch Ref
* @return {Reference}
*/
Repository.prototype.getBranch = function (name, callback) {
  return this.getReference(name, callback);
};

/**
* Look up a branch's most recent commit. Alias to `getReferenceCommit`
*
* @async
* @param {String|Reference} name Ref name, e.g. "master", "refs/heads/master"
*                          or Branch Ref
* @return {Commit}
*/
Repository.prototype.getBranchCommit = function (name, callback) {
  return this.getReferenceCommit(name, callback);
};

/**
 * Retrieve the commit identified by oid.
 *
 * @async
 * @param {String|Oid} String sha or Oid
 * @return {Commit}
 */
Repository.prototype.getCommit = function (oid, callback) {
  var repository = this;

  return Commit.lookup(repository, oid).then(function (commit) {
    commit.repo = repository;

    if (typeof callback === "function") {
      callback(null, commit);
    }

    return commit;
  }, callback);
};

/**
 * Gets the branch that HEAD currently points to
 * Is an alias to head()
 *
 * @async
 * @return {Reference}
 */
Repository.prototype.getCurrentBranch = function () {
  return this.head();
};

/**
 * Retrieve the commit that HEAD is currently pointing to
 *
 * @async
 * @return {Commit}
 */
Repository.prototype.getHeadCommit = function (callback) {
  var repo = this;

  return Reference.nameToId(repo, "HEAD").then(function (head) {
    return repo.getCommit(head, callback);
  }).catch(function () {
    return null;
  });
};

/**
 * Retrieve the master branch commit.
 *
 * @async
 * @return {Commit}
 */
Repository.prototype.getMasterCommit = function (callback) {
  return this.getBranchCommit("master", callback);
};

/**
 * Lookup the reference with the given name.
 *
 * @async
 * @param {String|Reference} name Ref name, e.g. "master", "refs/heads/master"
 *                               or Branch Ref
 * @return {Reference}
 */
Repository.prototype.getReference = function (name, callback) {
  var repository = this;

  return Reference.dwim(this, name).then(function (reference) {
    if (reference.isSymbolic()) {
      return reference.resolve().then(function (reference) {
        reference.repo = repository;

        if (typeof callback === "function") {
          callback(null, reference);
        }

        return reference;
      }, callback);
    } else {
      reference.repo = repository;
      if (typeof callback === "function") {
        callback(null, reference);
      }
      return reference;
    }
  }, callback);
};

/**
 * Look up a refs's commit.
 *
 * @async
 * @param {String|Reference} name Ref name, e.g. "master", "refs/heads/master"
 *                              or Branch Ref
 * @return {Commit}
 */
Repository.prototype.getReferenceCommit = function (name, callback) {
  var repository = this;

  return this.getReference(name).then(function (reference) {
    return repository.getCommit(reference.target()).then(function (commit) {
      if (typeof callback === "function") {
        callback(null, commit);
      }

      return commit;
    });
  }, callback);
};

/**
 * Lookup reference names for a repository.
 *
 * @async
 * @param {Reference.TYPE} type Type of reference to look up
 * @return {Array<String>}
 */
Repository.prototype.getReferenceNames = function (type) {
  return Repository.getReferences(this, type, true);
};

/**
 * Lookup references for a repository.
 *
 * @async
 * @param {Reference.TYPE} type Type of reference to look up
 * @return {Array<Reference>}
 */

/**
 * Gets a remote from the repo
 *
 * @async
 * @param {String|Remote} remote
 * @param {Function} callback
 * @return {Remote} The remote object
 */
Repository.prototype.getRemote = function (remote, callback) {
  if (remote instanceof NodeGit.Remote) {
    return Promise.resolve(remote).then(function (remoteObj) {
      if (typeof callback === "function") {
        callback(null, remoteObj);
      }

      return remoteObj;
    }, callback);
  }

  return NodeGit.Remote.lookup(this, remote).then(function (remoteObj) {
    if (typeof callback === "function") {
      callback(null, remoteObj);
    }

    return remoteObj;
  }, callback);
};

/**
* Lists out the remotes in the given repository.
*
* @async
* @param {Function} Optional callback
* @return {Object} Promise object.
*/
Repository.prototype.getRemoteNames = function (callback) {
  return Remote.list(this).then(function (remotes) {
    if (typeof callback === "function") {
      callback(null, remotes);
    }

    return remotes;
  }, callback);
};

/**
 * Get the status of a repo to it's working directory
 *
 * @async
 * @param {obj} opts
 * @return {Array<StatusFile>}
 */
Repository.prototype.getStatus = function (opts) {
  var statuses = [];
  var statusCallback = function statusCallback(path, status) {
    statuses.push(new StatusFile({ path: path, status: status }));
  };

  if (!opts) {
    opts = {
      flags: Status.OPT.INCLUDE_UNTRACKED | Status.OPT.RECURSE_UNTRACKED_DIRS
    };
  }

  return Status.foreachExt(this, opts, statusCallback).then(function () {
    return statuses;
  });
};

/**
 * Get extended statuses of a repo to it's working directory. Status entries
 * have `status`, `headToIndex` delta, and `indexToWorkdir` deltas
 *
 * @async
 * @param {obj} opts
 * @return {Array<StatusFile>}
 */
Repository.prototype.getStatusExt = function (opts) {
  var statuses = [];

  if (!opts) {
    opts = {
      flags: Status.OPT.INCLUDE_UNTRACKED | Status.OPT.RECURSE_UNTRACKED_DIRS | Status.OPT.RENAMES_INDEX_TO_WORKDIR | Status.OPT.RENAMES_HEAD_TO_INDEX | Status.OPT.RENAMES_FROM_REWRITES
    };
  }

  return StatusList.create(this, opts).then(function (list) {
    for (var i = 0; i < list.entrycount(); i++) {
      var entry = Status.byIndex(list, i);
      statuses.push(new StatusFile({ entry: entry }));
    }

    return statuses;
  });
};

/**
 * Get the names of the submodules in the repository.
 *
 * @async
 * @return {Array<String>}
 */
Repository.prototype.getSubmoduleNames = function (callback) {
  var names = [];
  var submoduleCallback = function submoduleCallback(submodule, name, payload) {
    names.push(name);
  };

  return Submodule.foreach(this, submoduleCallback).then(function () {
    if (typeof callback === "function") {
      callback(null, names);
    }

    return names;
  });
};

/**
 * Retrieve the tag represented by the oid.
 *
 * @async
 * @param {String|Oid} String sha or Oid
 * @return {Tag}
 */
Repository.prototype.getTag = function (oid, callback) {
  var repository = this;

  return Tag.lookup(repository, oid).then(function (reference) {
    reference.repo = repository;

    if (typeof callback === "function") {
      callback(null, reference);
    }

    return reference;
  }, callback);
};

/**
 * Retrieve the tag represented by the tag name.
 *
 * @async
 * @param {String} Short or full tag name
 * @return {Tag}
 */
Repository.prototype.getTagByName = function (name, callback) {
  var repo = this;

  name = ~name.indexOf("refs/tags/") ? name : "refs/tags/" + name;

  return Reference.nameToId(repo, name).then(function (oid) {
    return Tag.lookup(repo, oid).then(function (reference) {
      reference.repo = repo;

      if (typeof callback === "function") {
        callback(null, reference);
      }

      return reference;
    });
  }, callback);
};

/**
 * Retrieve the tree represented by the oid.
 *
 * @async
 * @param {String|Oid} String sha or Oid
 * @return {Tree}
 */
Repository.prototype.getTree = function (oid, callback) {
  var repository = this;

  return Tree.lookup(repository, oid).then(function (tree) {
    tree.repo = repository;

    if (typeof callback === "function") {
      callback(null, tree);
    }

    return tree;
  }, callback);
};

/**
 * Returns true if the repository is in the APPLY_MAILBOX or
 * APPLY_MAILBOX_OR_REBASE state.
 * @return {Boolean}
 */
Repository.prototype.isApplyingMailbox = function () {
  var state = this.state();
  return state === NodeGit.Repository.STATE.APPLY_MAILBOX || state === NodeGit.Repository.STATE.APPLY_MAILBOX_OR_REBASE;
};

/**
 * Returns true if the repository is in the BISECT state.
 * @return {Boolean}
 */
Repository.prototype.isBisecting = function () {
  return this.state() === NodeGit.Repository.STATE.BISECT;
};

/**
 * Returns true if the repository is in the CHERRYPICK state.
 * @return {Boolean}
 */
Repository.prototype.isCherrypicking = function () {
  return this.state() === NodeGit.Repository.STATE.CHERRYPICK;
};

/**
 * Returns true if the repository is in the default NONE state.
 * @return {Boolean}
 */
Repository.prototype.isDefaultState = function () {
  return this.state() === NodeGit.Repository.STATE.NONE;
};

/**
 * Returns true if the repository is in the MERGE state.
 * @return {Boolean}
 */
Repository.prototype.isMerging = function () {
  return this.state() === NodeGit.Repository.STATE.MERGE;
};

/**
 * Returns true if the repository is in the REBASE, REBASE_INTERACTIVE, or
 * REBASE_MERGE state.
 * @return {Boolean}
 */
Repository.prototype.isRebasing = function () {
  var state = this.state();
  return state === NodeGit.Repository.STATE.REBASE || state === NodeGit.Repository.STATE.REBASE_INTERACTIVE || state === NodeGit.Repository.STATE.REBASE_MERGE;
};

/**
 * Returns true if the repository is in the REVERT state.
 * @return {Boolean}
 */
Repository.prototype.isReverting = function () {
  return this.state() === NodeGit.Repository.STATE.REVERT;
};

/**
 * Rebases a branch onto another branch
 *
 * @async
 * @param {String}     branch
 * @param {String}     upstream
 * @param {String}     onto
 * @param {Signature}  signature     Identity of the one performing the rebase
 * @param {Function}   beforeNextFn  Callback to be called before each step
 *                                   of the rebase.  If the callback returns a
 *                                   promise, the rebase will resume when the
 *                                   promise resolves.  The rebase object is
 *                                   is passed to the callback.
 * @param {Function}   beforeFinishFn Callback called before the invocation
 *                                    of finish(). If the callback returns a
 *                                    promise, finish() will be called when the
 *                                    promise resolves. This callback will be
 *                                    provided a detailed overview of the rebase
 * @param {RebaseOptions} rebaseOptions Options to initialize the rebase object
 *                                      with
 * @return {Oid|Index}  A commit id for a succesful merge or an index for a
 *                      rebase with conflicts
 */
Repository.prototype.rebaseBranches = function (branch, upstream, onto, signature, beforeNextFn, beforeFinishFn, rebaseOptions) {
  var repo = this;
  var branchCommit = void 0;
  var upstreamCommit = void 0;
  var ontoCommit = void 0;
  var mergeOptions = (rebaseOptions || {}).mergeOptions;

  var promiseChain = Promise.resolve();

  if (!signature) {
    promiseChain = promiseChain.then(function () {
      return repo.defaultSignature();
    }).then(function (signatureResult) {
      signature = signatureResult;
    });
  }

  return Promise.all([repo.getReference(branch), upstream ? repo.getReference(upstream) : null, onto ? repo.getReference(onto) : null]).then(function (refs) {
    return Promise.all([NodeGit.AnnotatedCommit.fromRef(repo, refs[0]), upstream ? NodeGit.AnnotatedCommit.fromRef(repo, refs[1]) : null, onto ? NodeGit.AnnotatedCommit.fromRef(repo, refs[2]) : null]);
  }).then(function (annotatedCommits) {
    branchCommit = annotatedCommits[0];
    upstreamCommit = annotatedCommits[1];
    ontoCommit = annotatedCommits[2];

    return NodeGit.Merge.base(repo, branchCommit.id(), upstreamCommit.id());
  }).then(function (oid) {
    if (oid.toString() === branchCommit.id().toString()) {
      // we just need to fast-forward
      return repo.mergeBranches(branch, upstream, null, null, mergeOptions).then(function () {
        // checkout 'branch' to match the behavior of rebase
        return repo.checkoutBranch(branch);
      });
    } else if (oid.toString() === upstreamCommit.id().toString()) {
      // 'branch' is already on top of 'upstream'
      // checkout 'branch' to match the behavior of rebase
      return repo.checkoutBranch(branch);
    }

    return NodeGit.Rebase.init(repo, branchCommit, upstreamCommit, ontoCommit, rebaseOptions).then(function (rebase) {
      return performRebase(repo, rebase, signature, beforeNextFn, beforeFinishFn);
    }).then(function (error) {
      if (error) {
        throw error;
      }
    });
  }).then(function () {
    return repo.getBranchCommit("HEAD");
  });
};

/**
 * Grabs a fresh copy of the index from the repository. Invalidates
 * all previously grabbed indexes
 *
 * @async
 * @return {Index}
 */
Repository.prototype.refreshIndex = function (callback) {
  var repo = this;

  repo.setIndex(); // clear the index

  return repo.index().then(function (index) {
    if (typeof callback === "function") {
      callback(null, index);
    }

    return index;
  }, callback);
};

/**
 * Merge a branch onto another branch
 *
 * @async
 * @param {String|Reference}        to
 * @param {String|Reference}        from
 * @param {Signature}         signature
 * @param {Merge.PREFERENCE}  mergePreference
 * @param {MergeOptions}      mergeOptions
 * @param {MergeBranchOptions} mergeBranchOptions
 * @return {Oid|Index}  A commit id for a succesful merge or an index for a
 *                      merge with conflicts
 */
Repository.prototype.mergeBranches = function (to, from, signature, mergePreference, mergeOptions, mergeBranchOptions) {
  var repo = this;
  var fromBranch = void 0;
  var toBranch = void 0;
  // Support old parameter `processMergeMessageCallback`
  var isOldOptionParameter = typeof mergeBranchOptions === "function";
  if (isOldOptionParameter) {
    console.error("DeprecationWarning: Repository#mergeBranches parameter " + "processMergeMessageCallback, use MergeBranchOptions");
  }
  var processMergeMessageCallback = mergeBranchOptions && (isOldOptionParameter ? mergeBranchOptions : mergeBranchOptions.processMergeMessageCallback) || function (message) {
    return message;
  };
  var signingCallback = mergeBranchOptions && mergeBranchOptions.signingCb;

  mergePreference = mergePreference || NodeGit.Merge.PREFERENCE.NONE;
  mergeOptions = normalizeOptions(mergeOptions, NodeGit.MergeOptions);

  var promiseChain = Promise.resolve();

  if (!signature) {
    promiseChain = promiseChain.then(function () {
      return repo.defaultSignature();
    }).then(function (signatureResult) {
      signature = signatureResult;
    });
  }

  return promiseChain.then(function () {
    return Promise.all([repo.getBranch(to), repo.getBranch(from)]);
  }).then(function (objects) {
    toBranch = objects[0];
    fromBranch = objects[1];

    return Promise.all([repo.getBranchCommit(toBranch), repo.getBranchCommit(fromBranch)]);
  }).then(function (branchCommits) {
    var toCommitOid = branchCommits[0].toString();
    var fromCommitOid = branchCommits[1].toString();

    return NodeGit.Merge.base(repo, toCommitOid, fromCommitOid).then(function (baseCommit) {
      if (baseCommit.toString() == fromCommitOid) {
        // The commit we're merging to is already in our history.
        // nothing to do so just return the commit the branch is on
        return toCommitOid;
      } else if (baseCommit.toString() == toCommitOid && mergePreference !== NodeGit.Merge.PREFERENCE.NO_FASTFORWARD) {
        // fast forward
        var message = "Fast forward branch " + toBranch.shorthand() + " to branch " + fromBranch.shorthand();

        return branchCommits[1].getTree().then(function (tree) {
          if (toBranch.isHead()) {
            // Checkout the tree if we're on the branch
            var opts = {
              checkoutStrategy: NodeGit.Checkout.STRATEGY.SAFE | NodeGit.Checkout.STRATEGY.RECREATE_MISSING
            };
            return NodeGit.Checkout.tree(repo, tree, opts);
          }
        }).then(function () {
          return toBranch.setTarget(fromCommitOid, message).then(function () {
            return fromCommitOid;
          });
        });
      } else if (mergePreference !== NodeGit.Merge.PREFERENCE.FASTFORWARD_ONLY) {
        var updateHead;
        // We have to merge. Lets do it!
        return NodeGit.Reference.lookup(repo, "HEAD").then(function (headRef) {
          return headRef.resolve();
        }).then(function (headRef) {
          updateHead = !!headRef && headRef.name() === toBranch.name();
          return NodeGit.Merge.commits(repo, toCommitOid, fromCommitOid, mergeOptions);
        }).then(function (index) {
          // if we have conflicts then throw the index
          if (index.hasConflicts()) {
            throw index;
          }

          // No conflicts so just go ahead with the merge
          return index.writeTreeTo(repo);
        }).then(function (oid) {
          var mergeDecorator;
          if (fromBranch.isTag()) {
            mergeDecorator = "tag";
          } else if (fromBranch.isRemote()) {
            mergeDecorator = "remote-tracking branch";
          } else {
            mergeDecorator = "branch";
          }

          var message = "Merge " + mergeDecorator + " '" + fromBranch.shorthand() + "'";

          // https://github.com/git/git/blob/master/builtin/fmt-merge-msg.c#L456-L459
          if (toBranch.shorthand() !== "master") {
            message += " into " + toBranch.shorthand();
          }

          return Promise.all([oid, processMergeMessageCallback(message)]);
        }).then(function (_ref6) {
          var _ref7 = _slicedToArray(_ref6, 2),
              oid = _ref7[0],
              message = _ref7[1];

          if (signingCallback) {
            return repo.createCommitWithSignature(toBranch.name(), signature, signature, message, oid, [toCommitOid, fromCommitOid], signingCallback);
          }
          return repo.createCommit(toBranch.name(), signature, signature, message, oid, [toCommitOid, fromCommitOid]);
        }).then(function (commit) {
          // we've updated the checked out branch, so make sure we update
          // head so that our index isn't messed up
          if (updateHead) {
            return repo.getBranch(to).then(function (branch) {
              return repo.getBranchCommit(branch);
            }).then(function (branchCommit) {
              return branchCommit.getTree();
            }).then(function (tree) {
              var opts = {
                checkoutStrategy: NodeGit.Checkout.STRATEGY.SAFE | NodeGit.Checkout.STRATEGY.RECREATE_MISSING
              };
              return NodeGit.Checkout.tree(repo, tree, opts);
            }).then(function () {
              return commit;
            });
          } else {
            return commit;
          }
        });
      } else {
        // A non fast-forwardable merge with ff-only
        return toCommitOid;
      }
    });
  });
};

/**
 * @async
 * @param {MergeheadForeachCb} callback The callback function to be called on
 * each entry
 */
Repository.prototype.mergeheadForeach = function (callback) {
  return _mergeheadForeach.call(this, callback, null);
};

/**
 * Stages or unstages line selection of a specified file
 *
 * @async
 * @param {String|Array} filePath The relative path of this file in the repo
 * @param {Boolean} stageNew Set to stage new filemode. Unset to unstage.
 * @return {Number} 0 or an error code
 */
Repository.prototype.stageFilemode = function (filePath, stageNew, additionalDiffOptions) {
  var repo = this;
  var index;
  var diffOptions = additionalDiffOptions ? {
    flags: additionalDiffOptions
  } : undefined;
  var diffPromise = stageNew ? NodeGit.Diff.indexToWorkdir(repo, index, {
    flags: NodeGit.Diff.OPTION.SHOW_UNTRACKED_CONTENT | NodeGit.Diff.OPTION.RECURSE_UNTRACKED_DIRS | (additionalDiffOptions || 0)
  }) : repo.getHeadCommit().then(function getTreeFromCommit(commit) {
    return commit.getTree();
  }).then(function getDiffFromTree(tree) {
    return NodeGit.Diff.treeToIndex(repo, tree, index, diffOptions);
  });
  var filePaths = filePath instanceof Array ? filePath : [filePath];

  var indexLock = repo.path().replace(".git/", "") + ".git/index.lock";

  return fse.remove(indexLock).then(function () {
    return repo.refreshIndex();
  }).then(function (indexResult) {
    index = indexResult;
  }).then(function () {
    return diffPromise;
  }).then(function (diff) {
    var origLength = filePaths.length;
    var fileFilterPromises = fp.map(function (p) {
      return NodeGit.Status.file(repo, p).then(function (status) {
        return {
          path: p,
          filter: status & NodeGit.Status.STATUS.WT_MODIFIED || status & NodeGit.Status.STATUS.INDEX_MODIFIED
        };
      });
    }, filePaths);

    return Promise.all(fileFilterPromises).then(function (results) {
      filePaths = fp.flow([fp.filter(function (filterResult) {
        return filterResult.filter;
      }), fp.map(function (filterResult) {
        return filterResult.path;
      })])(results);

      if (filePaths.length === 0 && origLength > 0) {
        return Promise.reject("Selected staging is only available on modified files.");
      }
      return diff.patches();
    });
  }).then(function (patches) {
    var pathPatches = patches.filter(function (patch) {
      return ~filePaths.indexOf(patch.newFile().path());
    });
    if (pathPatches.length === 0) {
      return Promise.reject("No differences found for this file.");
    }

    return pathPatches.reduce(function (lastIndexAddPromise, pathPatch) {
      var entry = index.getByPath(pathPatch.newFile().path(), 0);

      entry.mode = stageNew ? pathPatch.newFile().mode() : pathPatch.oldFile().mode();

      return lastIndexAddPromise.then(function () {
        return index.add(entry);
      });
    }, Promise.resolve());
  }).then(function () {
    return index.write();
  });
};

/**
 * Stages or unstages line selection of a specified file
 *
 * @async
 * @param {String} filePath The relative path of this file in the repo
 * @param {Array} selectedLines The array of DiffLine objects
 *                            selected for staging or unstaging
 * @param {Boolean} isStaged Are the selected lines currently staged
 * @return {Number} 0 or an error code
 */
Repository.prototype.stageLines = function (filePath, selectedLines, isSelectionStaged, additionalDiffOptions) {

  var repo = this;
  var index;
  var originalBlob;

  // The following chain checks if there is a patch with no hunks left for the
  // file, and no filemode changes were done on the file. It is then safe to
  // stage the entire file so the file doesn't show as having unstaged changes
  // in `git status`. Also, check if there are no type changes.
  var lastHunkStagedPromise = function lastHunkStagedPromise(result) {
    return NodeGit.Diff.indexToWorkdir(repo, index, {
      flags: NodeGit.Diff.OPTION.SHOW_UNTRACKED_CONTENT | NodeGit.Diff.OPTION.RECURSE_UNTRACKED_DIRS | (additionalDiffOptions || 0)
    }).then(function (diff) {
      return diff.patches();
    }).then(function (patches) {
      var pathPatch = patches.filter(function (patch) {
        return patch.newFile().path() === filePath;
      });
      var emptyPatch = false;
      if (pathPatch.length > 0) {
        // No hunks, unchanged file mode, and no type changes.
        emptyPatch = pathPatch[0].size() === 0 && pathPatch[0].oldFile().mode() === pathPatch[0].newFile().mode() && !pathPatch[0].isTypeChange();
      }
      if (emptyPatch) {
        return index.addByPath(filePath).then(function () {
          return index.write();
        });
      }

      return result;
    });
  };

  return repo.refreshIndex().then(function (indexResult) {
    index = indexResult;
    var pathOid = index.getByPath(filePath).id;

    return repo.getBlob(pathOid);
  }).then(function (blob) {
    originalBlob = blob;

    return getPathHunks(repo, index, filePath, isSelectionStaged, additionalDiffOptions);
  }).then(function (hunks) {
    return applySelectedLinesToTarget(originalBlob, selectedLines, hunks, isSelectionStaged);
  }).then(function (newContent) {
    var newContentBuffer = new Buffer(newContent);

    return repo.createBlobFromBuffer(newContentBuffer);
  }).then(function (newOid) {
    return repo.getBlob(newOid);
  }).then(function (newBlob) {
    var entry = index.getByPath(filePath, 0);
    entry.id = newBlob.id();
    entry.path = filePath;
    entry.fileSize = newBlob.content().length;

    return index.add(entry);
  }).then(function () {
    return index.write();
  }).then(function (result) {
    if (isSelectionStaged) {
      return result;
    }

    return lastHunkStagedPromise(result);
  });
};

/**
 * Create a new tree builder.
 *
 * @param {Tree} tree
 */
Repository.prototype.treeBuilder = function () {
  var builder = TreeBuilder.create(null);

  builder.root = builder;
  builder.repo = this;

  return builder;
};