diff --git a/source/git-api.js b/source/git-api.js index 38146d58c..e4f894320 100644 --- a/source/git-api.js +++ b/source/git-api.js @@ -155,20 +155,38 @@ exports.registerApi = env => { { maxWait: 2000 } ); - const autoStashExecuteAndPop = (commands, repoPath, allowedCodes, outPipe, inPipe, timeout) => { + const repoPs = {}; + + /** + * memoize nodegit opened repos + * @param {string} repoPath the path to the repository + * @returns {Promise} + */ + const getRepo = repoPath => { + if (!repoPs[repoPath]) { + repoPs[repoPath] = nodegit.Repository.open(repoPath); + } + return repoPs[repoPath]; + }; + + const autoStash = async (repoPath, fn) => { if (config.autoStashAndPop) { - return gitPromise.stashExecuteAndPop( - commands, - repoPath, - allowedCodes, - outPipe, - inPipe, - timeout - ); + const repo = await getRepo(repoPath); + const signature = await repo.defaultSignature(); + const oid = await nodegit.Stash.save(repo, signature, 'Ungit: automatic stash', 0); + const out = await fn(); + if (!oid) return out; + let index; + await nodegit.Stash.foreach(repo, (i, _msg, stashOid) => { + if (stashOid === oid) index = i; + }); + if (index != null) await nodegit.Stash.pop(repo, index); + return out; } else { - return gitPromise(commands, repoPath, allowedCodes, outPipe, inPipe, timeout); + return fn(); } }; + const jsonResultOrFailProm = (res, promise) => // TODO shouldn't this be a boolean instead of an object? promise @@ -307,14 +325,19 @@ exports.registerApi = env => { } ); - app.post(`${exports.pathPrefix}/reset`, ensureAuthenticated, ensurePathExists, (req, res) => { - jsonResultOrFailProm( - res, - autoStashExecuteAndPop(['reset', `--${req.body.mode}`, req.body.to], req.body.path) - ) - .then(emitGitDirectoryChanged.bind(null, req.body.path)) - .then(emitWorkingTreeChanged.bind(null, req.body.path)); - }); + app.post( + `${exports.pathPrefix}/reset`, + ensureAuthenticated, + ensurePathExists, + jw(async req => { + const repoPath = req.body.path; + await autoStash(repoPath, () => + gitPromise(['reset', `--${req.body.mode}`, req.body.to], repoPath) + ); + await emitGitDirectoryChanged(repoPath); + await emitWorkingTreeChanged(repoPath); + }) + ); app.get(`${exports.pathPrefix}/diff`, ensureAuthenticated, ensurePathExists, (req, res) => { const isIgnoreWhiteSpace = req.query.whiteSpace === 'true' ? true : false; @@ -564,12 +587,15 @@ exports.registerApi = env => { } ); - app.get(`${exports.pathPrefix}/tags`, ensureAuthenticated, ensurePathExists, (req, res) => { - let pathToRepo = req.query.path; - nodegit.Repository.open(pathToRepo).then(function (repo) { - jsonResultOrFailProm(res, nodegit.Tag.list(repo)); - }); - }); + app.get( + `${exports.pathPrefix}/tags`, + ensureAuthenticated, + ensurePathExists, + jw(req => { + let pathToRepo = req.query.path; + return nodegit.Repository.open(pathToRepo).then(repo => nodegit.Tag.list(repo)); + }) + ); app.get( `${exports.pathPrefix}/remote/tags`, @@ -643,28 +669,31 @@ exports.registerApi = env => { } ); - app.post(`${exports.pathPrefix}/checkout`, ensureAuthenticated, ensurePathExists, (req, res) => { - const arg = !!req.body.sha1 - ? ['checkout', '-b', req.body.name.trim(), req.body.sha1] - : ['checkout', req.body.name.trim()]; - - jsonResultOrFailProm(res, autoStashExecuteAndPop(arg, req.body.path)) - .then(emitGitDirectoryChanged.bind(null, req.body.path)) - .then(emitWorkingTreeChanged.bind(null, req.body.path)); - }); + app.post( + `${exports.pathPrefix}/checkout`, + ensureAuthenticated, + ensurePathExists, + jw(async req => { + const arg = !!req.body.sha1 + ? ['checkout', '-b', req.body.name.trim(), req.body.sha1] + : ['checkout', req.body.name.trim()]; + const repoPath = req.body.path; + await autoStash(repoPath, () => gitPromise(arg, repoPath)); + await emitGitDirectoryChanged(repoPath); + await emitWorkingTreeChanged(repoPath); + }) + ); app.post( `${exports.pathPrefix}/cherrypick`, ensureAuthenticated, ensurePathExists, - (req, res) => { - jsonResultOrFailProm( - res, - autoStashExecuteAndPop(['cherry-pick', req.body.name.trim()], req.body.path) - ) - .then(emitGitDirectoryChanged.bind(null, req.body.path)) - .then(emitWorkingTreeChanged.bind(null, req.body.path)); - } + jw(async req => { + const repoPath = req.body.path; + await autoStash(repoPath, () => gitPromise(['cherry-pick', req.body.name.trim()], repoPath)); + await emitGitDirectoryChanged(repoPath); + await emitWorkingTreeChanged(repoPath); + }) ); app.get(`${exports.pathPrefix}/checkout`, ensureAuthenticated, ensurePathExists, (req, res) => { @@ -919,13 +948,60 @@ exports.registerApi = env => { jsonResultOrFailProm(res, task); }); - app.get(`${exports.pathPrefix}/stashes`, ensureAuthenticated, ensurePathExists, (req, res) => { - const task = gitPromise( - ['stash', 'list', '--decorate=full', '--pretty=fuller', '-z', '--parents', '--numstat'], - req.query.path - ).then(gitParser.parseGitLog); - jsonResultOrFailProm(res, task); + /** + * @param {nodegit.Commit} c + */ + const formatCommit = c => ({ + commitDate: c.date().toJSON(), + message: c.message(), + sha1: c.sha(), }); + /** + * @param {nodegit.Commit} c + */ + const getFileStats = async c => { + const diffList = await c.getDiff(); + // Each diff has the entire patch set for some reason + const patches = await diffList[0]?.patches(); + if (!patches?.length) return []; + + return patches.map(patch => { + const stats = patch.lineStats(); + const oldFileName = patch.oldFile().path(); + const displayName = patch.newFile().path(); + return { + additions: stats.total_additions, + deletions: stats.total_deletions, + fileName: displayName, + oldFileName, + displayName, + // TODO figure out how to get this + type: 'text', + }; + }); + }; + + app.get( + `${exports.pathPrefix}/stashes`, + ensureAuthenticated, + ensurePathExists, + jw(async req => { + const repo = await getRepo(req.query.path); + const oids = []; + await nodegit.Stash.foreach(repo, (index, message, oid) => { + oids.push(oid); + }); + const stashes = await Promise.all(oids.map(oid => repo.getCommit(oid))); + return Promise.all( + stashes.map(async (stash, index) => ({ + ...formatCommit(stash), + reflogId: `${index}`, + reflogName: `stash@{${index}}`, + fileLineDiffs: await getFileStats(stash), + })) + ); + }) + ); app.post(`${exports.pathPrefix}/stashes`, ensureAuthenticated, ensurePathExists, (req, res) => { jsonResultOrFailProm( diff --git a/source/git-promise.js b/source/git-promise.js index 6507fafed..648a9d6f3 100644 --- a/source/git-promise.js +++ b/source/git-promise.js @@ -130,13 +130,13 @@ const gitExecutorProm = (args, retryCount) => { /** * Returns a promise that executes git command with given arguments * @function - * @param {obj|array} commands - An object that represents all parameters or first parameter only, which is an array of commands - * @param {string} repoPath - path to the git repository + * @param {object|Array} commands - An object that represents all parameters or first parameter only, which is an array of commands + * @param {string=} repoPath - path to the git repository * @param {boolean=} allowError - true if return code of 1 is acceptable as some cases errors are acceptable - * @param {stream=} outPipe - if this argument exists, stdout is piped to this object - * @param {stream=} inPipe - if this argument exists, data is piped to stdin process on start - * @param {timeout=} timeout - execution timeout, default is 2 mins - * @returns {promise} execution promise + * @param {Stream=} outPipe - if this argument exists, stdout is piped to this object + * @param {Stream=} inPipe - if this argument exists, data is piped to stdin process on start + * @param {number=} timeout - execution timeout in ms, default is 2 mins + * @returns {Promise} execution promise * @example getGitExecuteTask({ commands: ['show'], repoPath: '/tmp' }); * @example getGitExecuteTask(['show'], '/tmp'); */