@figma/nodegit
Version:
Node.js libgit2 asynchronous native bindings
1,842 lines (1,664 loc) • 53.6 kB
JavaScript
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 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 _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) {
const calcRewritten = fp.cond([
[fp.isEmpty, fp.constant(null)],
[fp.stubTrue, 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", true).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();
}
/**
* Look for a git repository, returning its path.
*
* @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) {
return _discover(startPath, acrossFs, ceilingDirs)
.then(function(foundPath) {
return path.resolve(foundPath);
});
};
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
) {
const repo = this;
let rebase;
let promiseChain = Promise.resolve();
if (!signature) {
promiseChain = promiseChain
.then(() => repo.defaultSignature())
.then((signatureResult) => {
signature = signatureResult;
});
}
return promiseChain
.then(() => repo.refreshIndex())
.then((index) => {
if (index.hasConflicts()) {
throw index;
}
return NodeGit.Rebase.open(repo, rebaseOptions);
})
.then((_rebase) => {
rebase = _rebase;
return rebase.commit(null, signature)
.catch((e) => {
// If the first commit on continueRebase is a
// "patch already applied" error,
// interpret that as an explicit "skip commit"
// and ignore the error.
const errno = fp.get(["errno"], e);
if (errno === NodeGit.Error.CODE.EAPPLIED) {
return;
}
throw e;
});
})
.then(() => {
return performRebase(
repo,
rebase,
signature,
beforeNextFn,
beforeFinishFn
);
})
.then((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) {
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
);
});
};
/**
* 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) {
const repo = this;
const promises = (parents || [])
.reduce(function(acc, parent) {
acc.push(repo.getCommit(parent));
return acc;
}, [repo.getTree(treeOid)]);
return Promise.all(promises)
.then(function([tree, ...parentCommits]) {
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));
});
const 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({ code, field, 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: {
const 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) {
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
);
});
});
};
/**
* 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) {
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) {
const repository = this;
let signature = null;
return repository.defaultSignature()
.then((signatureResult) => {
signature = signatureResult;
return Commit.lookup(repository, oid);
})
.then((commit) => {
// Final argument is `force` which overwrites any previous tag
return Tag.create(repository, name, commit, signature, message, 0);
})
.then((tagOid) => {
return repository.getTag(tagOid);
});
};
/**
* Gets the default signature for the default user and now timestamp
*
* @async
* @return {Signature}
*/
Repository.prototype.defaultSignature = function() {
return NodeGit.Signature.default(this)
.then((result) => {
if (!result || !result.name()) {
result = NodeGit.Signature.now("unknown", "unknown@example.com");
}
return result;
})
.catch(() => {
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)
{
var repo = this;
return repo.getRemote(remote)
.then(function(remote) {
return remote.fetch(null, fetchOptions, "Fetch from " + remote)
.then(function() {
return remote.disconnect();
});
});
};
/**
* 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
*/
Repository.prototype.fetchAll = function(fetchOptions) {
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());
});
};
/**
* @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) {
var repository = this;
return Blob.lookup(repository, oid).then(function(blob) {
blob.repo = repository;
return blob;
});
};
/**
* 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) {
return this.getReference(name);
};
/**
* 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) {
return this.getReferenceCommit(name);
};
/**
* Retrieve the commit identified by oid.
*
* @async
* @param {String|Oid} String sha or Oid
* @return {Commit}
*/
Repository.prototype.getCommit = function(oid) {
var repository = this;
return Commit.lookup(repository, oid);
};
/**
* 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() {
var repo = this;
return Reference.nameToId(repo, "HEAD")
.then(function(head) {
return repo.getCommit(head);
})
.catch(function() {
return null;
});
};
/**
* Retrieve the master branch commit.
*
* @async
* @return {Commit}
*/
Repository.prototype.getMasterCommit = function() {
return this.getBranchCommit("master");
};
/**
* 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) {
var repository = this;
return Reference.dwim(this, name).then(function(reference) {
if (reference.isSymbolic()) {
return reference.resolve().then(function(reference) {
reference.repo = repository;
return reference;
});
}
reference.repo = repository;
return reference;
});
};
/**
* 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) {
var repository = this;
return this.getReference(name).then(function(reference) {
return repository.getCommit(reference.target());
});
};
/**
* 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
* @return {Remote} The remote object
*/
Repository.prototype.getRemote = function(remote) {
if (remote instanceof NodeGit.Remote) {
return Promise.resolve(remote);
}
return NodeGit.Remote.lookup(this, remote);
};
/**
* Lists out the remotes in the given repository.
*
* @async
* @return {Object} Promise object.
*/
Repository.prototype.getRemoteNames = function() {
return Remote.list(this);
};
/**
* 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(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() {
var names = [];
var submoduleCallback = function(submodule, name, payload) {
names.push(name);
};
return Submodule.foreach(this, submoduleCallback).then(function() {
return names;
});
};
/**
* Retrieve the tag represented by the oid.
*
* @async
* @param {String|Oid} String sha or Oid
* @return {Tag}
*/
Repository.prototype.getTag = function(oid) {
var repository = this;
return Tag.lookup(repository, oid).then(function(reference) {
reference.repo = repository;
return reference;
});
};
/**
* Retrieve the tag represented by the tag name.
*
* @async
* @param {String} Short or full tag name
* @return {Tag}
*/
Repository.prototype.getTagByName = function(name) {
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;
return reference;
});
};
/**
* Retrieve the tree represented by the oid.
*
* @async
* @param {String|Oid} String sha or Oid
* @return {Tree}
*/
Repository.prototype.getTree = function(oid) {
var repository = this;
return Tree.lookup(repository, oid).then(function(tree) {
tree.repo = repository;
return tree;
});
};
/**
* 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
)
{
const repo = this;
let branchCommit;
let upstreamCommit;
let ontoCommit;
let mergeOptions = (rebaseOptions || {}).mergeOptions;
let promiseChain = Promise.resolve();
if (!signature) {
promiseChain = promiseChain
.then(() => repo.defaultSignature())
.then((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() {
var repo = this;
repo.setIndex(); // clear the index
return repo.index();
};
/**
* 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
) {
const repo = this;
let fromBranch;
let toBranch;
// Support old parameter `processMergeMessageCallback`
const isOldOptionParameter = typeof mergeBranchOptions === "function";
if (isOldOptionParameter) {
console.error("DeprecationWarning: Repository#mergeBranches parameter " +
"processMergeMessageCallback, use MergeBranchOptions");
}
const processMergeMessageCallback = mergeBranchOptions &&
(isOldOptionParameter ?
mergeBranchOptions :
mergeBranchOptions.processMergeMessageCallback) ||
function (message) { return message; };
const signingCallback = mergeBranchOptions && mergeBranchOptions.signingCb;
mergePreference = mergePreference || NodeGit.Merge.PREFERENCE.NONE;
let promiseChain = Promise.resolve();
if (!signature) {
promiseChain = promiseChain
.then(() => repo.defaultSignature())
.then((signatureResult) => {
signature = signatureResult;
});
}
return promiseChain.then(() => Promise.all([
repo.getBranch(to),
repo.getBranch(from)
]))
.then((objects) => {
toBranch = objects[0];
fromBranch = objects[1];
return Promise.all([
repo.getBranchCommit(toBranch),
repo.getBranchCommit(fromBranch)
]);
})
.then((branchCommits) => {
var toCommitOid = branchCommits[0].toString();
var fromCommitOid = branchCommits[1].toString();
return NodeGit.Merge.base(repo, toCommitOid, fromCommitOid)
.then((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((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(() => {
return toBranch.setTarget(
fromCommitOid,
message)
.then(() => {
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((headRef) => {
return headRef.resolve();
})
.then((headRef) => {
updateHead = !!headRef && (headRef.name() === toBranch.name());
return NodeGit.Merge.commits(
repo,
toCommitOid,
fromCommitOid,
mergeOptions
);
})
.then((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((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(([oid, message]) => {
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((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((branch) => {
return repo.getBranchCommit(branch);
})
.then((branchCommit) => {
return branchCommit.getTree();
})
.then((tree) => {
var opts = {
checkoutStrategy: NodeGit.Checkout.STRATEGY.SAFE |
NodeGit.Checkout.STRATEGY.RECREATE_MISSING
};
return NodeGit.Checkout.tree(repo, tree, opts);
})
.then(() => {
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,