var promisify = require("promisify-node"); var fse = promisify(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 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 = []; // split the original file into lines 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) { if (!(NodeGit.Status.file(repo, filePath) & NodeGit.Status.STATUS.WT_MODIFIED) && !(NodeGit.Status.file(repo, filePath) & 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(); }); } /** * 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; function readRebaseMetadataFile(fileName) { return fse.readFile( path.join(repository.path(), "rebase-merge", fileName), { encoding: "utf8" } ) .then(fp.trim); } 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; } rebase.commit(null, signature); return performRebase( repository, rebase, signature, beforeNextFn, beforeFinishFn ); }); }, function(error) { if (error && error.errno === NodeGit.Error.CODE.ITEROVER) { const calcRewritten = fp.flow([ fp.split("\n"), fp.map(fp.split(" ")) ]); const beforeFinishFnPromise = !beforeFinishFn ? Promise.resolve() : Promise.all([ readRebaseMetadataFile("onto_name"), readRebaseMetadataFile("onto"), readRebaseMetadataFile("head-name").then(calcHeadName), readRebaseMetadataFile("orig-head"), readRebaseMetadataFile("rewritten").then(calcRewritten) ]) .then(function([ ontoName, ontoSha, originalHeadName, originalHeadSha, rewritten ]) { return beforeFinishFn({ ontoName, ontoSha, originalHeadName, originalHeadSha, rebase, 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, callback) { return Reference.list(repo).then(function(refList) { var refFilterPromises = []; var filteredRefs = []; refList.forEach(function(refName) { refFilterPromises.push(Reference.lookup(repo, refName) .then(function(ref) { if (type == Reference.TYPE.LISTALL || ref.type() == type) { if (refNamesOnly) { filteredRefs.push(refName); return; } if (ref.isSymbolic()) { return ref.resolve().then(function(resolvedRef) { resolvedRef.repo = repo; filteredRefs.push(resolvedRef); }) .catch(function() { // If we can't resolve the ref then just ignore it. }); } else { filteredRefs.push(ref); } } }) ); }); return Promise.all(refFilterPromises).then(function() { if (typeof callback === "function") { callback(null, filteredRefs); } return filteredRefs; }, callback); }); }; /** * 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 * @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 ) { var repo = this; signature = signature || repo.defaultSignature(); return repo.refreshIndex() .then(function(index) { if (index.hasConflicts()) { throw index; } return NodeGit.Rebase.open(repo); }) .then(function(rebase) { rebase.commit(null, signature); 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 {bool} 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 * * @param {Buffer} buffer * @return {Oid} */ Repository.prototype.createBlobFromBuffer = function(buffer, callback) { return Blob.createFromBuffer(this, buffer, buffer.length, callback); }; /** * Create a commit * * @async * @param {String} updateRef * @param {Signature} author * @param {Signature} committer * @param {String} message * @param {Tree|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); }; /** * 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()` * * @param {String|Oid} String sha or Oid * @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 = repository.defaultSignature(); 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 * @return {Signature} */ Repository.prototype.defaultSignature = function() { var result = NodeGit.Signature.default(this); if (!result || !result.name()) { result = NodeGit.Signature.now("unknown", "unknown@example.com"); } return result; }; /** * 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; return repo.refreshIndex() .then(function(indexResult) { index = indexResult; 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 fse.writeFile(fullFilePath, newContent); }); }; /** * 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.getRemotes() .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} */ Repository.prototype.getReferenceNames = function(type, callback) { return Repository.getReferences(this, type, true, callback); }; /** * Lookup references for a repository. * * @async * @param {Reference.TYPE} type Type of reference to look up * @return {Array} */ Repository.prototype.getReferences = function(type, callback) { return Repository.getReferences(this, type, false, callback); }; /** * 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.getRemotes = 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} */ Repository.prototype.getStatus = function(opts) { var statuses = []; var statusCallback = function(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} */ 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} */ Repository.prototype.getSubmoduleNames = function(callback) { var names = []; var submoduleCallback = function(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 * @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; var upstreamCommit; var ontoCommit; var mergeOptions = (rebaseOptions || {}).mergeOptions; signature = signature || repo.defaultSignature(); 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 * @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, processMergeMessageCallback ) { var repo = this; var fromBranch; var toBranch; processMergeMessageCallback = processMergeMessageCallback || function (message) { return message; }; mergePreference = mergePreference || NodeGit.Merge.PREFERENCE.NONE; mergeOptions = normalizeOptions(mergeOptions, NodeGit.MergeOptions); signature = signature || repo.defaultSignature(); 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([oid, message]) { 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; filePaths = filePaths.filter(function(p) { return ( (NodeGit.Status.file(repo, p) & NodeGit.Status.STATUS.WT_MODIFIED) || (NodeGit.Status.file(repo, p) & NodeGit.Status.STATUS.INDEX_MODIFIED) ); }); 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); var newOid = repo.createBlobFromBuffer(newContentBuffer); 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; };