From 70a08254520cba014d19fe2e81df091cb84938c6 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 11 Oct 2017 20:39:31 -0400 Subject: [PATCH 001/402] Add `stash drop`. Addresses https://github.com/twosigma/git-meta/issues/367 --- node/lib/cmd/stash.js | 12 ++++++++- node/lib/util/stash_util.js | 54 ++++++++++++++++++++++--------------- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/node/lib/cmd/stash.js b/node/lib/cmd/stash.js index c93231cb9..8550957fc 100644 --- a/node/lib/cmd/stash.js +++ b/node/lib/cmd/stash.js @@ -56,7 +56,8 @@ exports.configureParser = function (parser) { parser.addArgument("type", { help: ` -'save' to save a stash, 'pop' to restore, 'list' to show stashes; 'save' is +'save' to save a stash, 'pop' to restore, 'list' to show stashes, 'drop' to \ +discard a stash; 'save' is default`, type: "string", nargs: "?", @@ -127,6 +128,14 @@ const doList = co.wrap(function *() { process.stdout.write(list); }); +const doDrop = co.wrap(function *(args) { + const GitUtil = require("../../lib/util/git_util"); + const StashUtil = require("../../lib/util/stash_util"); + const repo = yield GitUtil.getCurrentRepo(); + const index = (null === args.stash) ? 0 : args.stash; + yield StashUtil.removeStash(repo, index); +}); + /** * Execute the `stash` command according to the specified `args`. * @@ -139,6 +148,7 @@ exports.executeableSubcommand = function (args) { case "pop" : return doPop(args); case "save": return doSave(args); case "list": return doList(args); + case "drop": return doDrop(args); default: { console.error(`Invalid type ${colors.red(args.type)}`); process.exit(1); diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 26e1fc1ee..2a6ac062c 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -333,6 +333,33 @@ ${colors.red(name)}`); return result; }); +/** + * Return the sha of the stash at the specified `index` in the specified + * `repo`. Throw a `UserError` if there is no stash at the specified index. + * + * @param {NodeGit.Repository} repo + * @param {Number} index + * @return {String} + */ +const getStashSha = co.wrap(function *(repo, index) { + + let stashRef; + try { + stashRef = yield NodeGit.Reference.lookup(repo, metaStashRef); + } + catch (e) { + throw new UserError("No stash found."); + } + + const log = yield NodeGit.Reflog.read(repo, metaStashRef); + const count = log.entrycount(); + if (count <= index) { + throw new UserError( +`Invalid stash index: ${colors.red(index)}, max is ${count - 1}.`); + } + return log.entryByIndex(index).idNew().tostrS(); +}); + /** * Remove, from the stash queue for the specified `repo`, the stash at the * specified `index`. Throw a `UserError` if no such stash exists. If @@ -347,10 +374,8 @@ exports.removeStash = co.wrap(function *(repo, index) { assert.instanceOf(repo, NodeGit.Repository); assert.isNumber(index); const log = yield NodeGit.Reflog.read(repo, metaStashRef); + const stashSha = yield getStashSha(repo, index); const count = log.entrycount(); - if (count <= index) { - throw new UserError(`Invalid stash index: ${colors.red(index)}.`); - } log.drop(index, 1 /* rewrite previous entry */); log.write(); @@ -369,6 +394,9 @@ exports.removeStash = co.wrap(function *(repo, index) { NodeGit.Reference.remove(repo, metaStashRef); } } + const refText = `${metaStashRef}@{${index}}`; + console.log(`\ +Dropped ${colors.green(refText)} ${colors.blue(stashSha)}`); }); /** @@ -382,23 +410,7 @@ exports.pop = co.wrap(function *(repo, index) { assert.instanceOf(repo, NodeGit.Repository); assert.isNumber(index); - // Try to look up the meta stash; return early if not found. - - let stashRef; - try { - stashRef = yield NodeGit.Reference.lookup(repo, metaStashRef); - } - catch (e) { - console.warn("No meta stash found."); - return; // RETURN - } - - const log = yield NodeGit.Reflog.read(repo, metaStashRef); - const count = log.entrycount(); - if (count <= index) { - throw new UserError(`Invalid stash index: ${colors.red(index)}.`); - } - const stashSha = log.entryByIndex(index).idNew().tostrS(); + const stashSha = yield getStashSha(repo, index); const applyResult = yield exports.apply(repo, stashSha); const status = yield StatusUtil.getRepoStatus(repo); @@ -408,8 +420,6 @@ exports.pop = co.wrap(function *(repo, index) { if (null !== applyResult) { yield exports.removeStash(repo, index); - console.log(`\ -Dropped ${colors.green(metaStashRef + "@{0}")} ${colors.blue(stashSha)}`); // Clean up sub-repo meta-refs From 3017fd8ec8d30042c87583dfc79e6de2bd20673c Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 20 Oct 2017 17:15:13 -0400 Subject: [PATCH 002/402] Fix "nothing to pop" test case Previously it was warning but not throwing an exception; now it is, but I forgot to update the test driver. --- node/test/util/stash_util.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index 8e61b8fb9..8a8c72bf6 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -558,6 +558,7 @@ x=E:Os Bss=ss! const cases = { "nothing to pop": { init: "x=S", + fails: true, }, "failed": { init: ` From 9f6d247ee508a506a2cdfc3084eb9ef952492605 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sat, 7 Oct 2017 17:36:53 -0400 Subject: [PATCH 003/402] Speedup `getRepoStatus` and clients. The status functions are only supposed to report changed/interesting things, but it was loading a `RepoAST.Submodule` object for each submodule, even those not open and unchanged. Now, it will process open submodules and those with changes between the index and last commit. The latter information will be determined via `DiffUtil`. This change propagated through several clients of `getRepoStatus` who assumed that the `submodules` property of the returned `RepoStatus` object contained *all* current submodules. Now it contains only the interesting (changed or open) submodules. --- node/lib/cmd/commit.js | 10 -- node/lib/cmd/status.js | 6 +- node/lib/cmd/submodule.js | 61 ++++++-- node/lib/util/cherrypick.js | 17 +- node/lib/util/commit.js | 146 +++++++++-------- node/lib/util/merge.js | 33 ++-- node/lib/util/print_status_util.js | 35 +++-- node/lib/util/status_util.js | 80 +++++++--- node/lib/util/submodule_change.js | 86 ++++++++++ node/lib/util/submodule_util.js | 88 +++++------ node/lib/util/synthetic_branch_util.js | 10 +- node/test/util/commit.js | 208 +++---------------------- node/test/util/print_status_util.js | 87 +++-------- node/test/util/stash_util.js | 4 +- node/test/util/status_util.js | 70 +++++++-- node/test/util/submodule_change.js | 43 +++++ node/test/util/submodule_util.js | 113 ++++---------- 17 files changed, 550 insertions(+), 547 deletions(-) create mode 100644 node/lib/util/submodule_change.js create mode 100644 node/test/util/submodule_change.js diff --git a/node/lib/cmd/commit.js b/node/lib/cmd/commit.js index 8f5451c8a..f0338278e 100644 --- a/node/lib/cmd/commit.js +++ b/node/lib/cmd/commit.js @@ -68,14 +68,6 @@ exports.configureParser = function (parser) { required: false, help: "commit message; if not specified will prompt" }); - parser.addArgument(["--meta"], { - required: false, - action: "storeConst", - constant: true, - help: ` -Include changes to the meta-repo; disabled by default to prevent mistakes.`, - defaultValue: false, - }); parser.addArgument(["--amend"], { required: false, action: "storeConst", @@ -123,7 +115,6 @@ const doCommit = co.wrap(function *(args) { yield Commit.doCommitCommand(repo, cwd, args.message, - args.meta, args.all, args.file, args.interactive, @@ -146,7 +137,6 @@ const doAmend = co.wrap(function *(args) { yield Commit.doAmendCommand(repo, process.cwd(), args.message, - args.meta, args.all, args.interactive, args.no_edit ? null : GitUtil.editMessage); diff --git a/node/lib/cmd/status.js b/node/lib/cmd/status.js index 357b264a0..b0cdd56aa 100644 --- a/node/lib/cmd/status.js +++ b/node/lib/cmd/status.js @@ -90,12 +90,10 @@ exports.executeableSubcommand = co.wrap(function *(args) { const repo = yield GitUtil.getCurrentRepo(); const workdir = repo.workdir(); const cwd = process.cwd(); - const paths = yield args.path.map(filename => { - return GitUtil.resolveRelativePath(workdir, cwd, filename); - }); const repoStatus = yield StatusUtil.getRepoStatus(repo, { showMetaChanges: args.meta, - paths: paths, + cwd: cwd, + paths: args.path, }); // Compute the current directory relative to the working directory of the diff --git a/node/lib/cmd/submodule.js b/node/lib/cmd/submodule.js index 09be0cd85..7d3ede73b 100644 --- a/node/lib/cmd/submodule.js +++ b/node/lib/cmd/submodule.js @@ -122,27 +122,64 @@ ancestor of M (or M itself) that references S (or a descendant of S)`, }; const doStatusCommand = co.wrap(function *(paths, verbose) { - const path = require("path"); + // TODO: this is too big for a cmd; need to move some of this into a + // utility and write a test. - const GitUtil = require("../util/git_util"); - const PrintStatusUtil = require("../util/print_status_util"); - const StatusUtil = require("../util/status_util"); + const path = require("path"); + + const GitUtil = require("../util/git_util"); + const StatusUtil = require("../util/status_util"); + const SubmoduleConfigUtil = require("../util/submodule_config_util"); + const SubmoduleUtil = require("../util/submodule_util"); + const PrintStatusUtil = require("../util/print_status_util"); const repo = yield GitUtil.getCurrentRepo(); const workdir = repo.workdir(); const cwd = process.cwd(); - - paths = yield paths.map(filename => { - return GitUtil.resolveRelativePath(workdir, cwd, filename); - }); + const relCwd = path.relative(workdir, cwd); + const head = yield repo.getHeadCommit(); + const tree = yield head.getTree(); const status = yield StatusUtil.getRepoStatus(repo, { paths: paths, showMetaChanges: false, }); - const relCwd = path.relative(workdir, cwd); - process.stdout.write(PrintStatusUtil.printSubmoduleStatus(status, - relCwd, - verbose)); + paths = yield paths.map(filename => { + return GitUtil.resolveRelativePath(workdir, cwd, filename); + }); + const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, head); + const allSubs = Object.keys(urls); + const subs = status.submodules; + const openList = Object.keys(subs).filter(name => { + return null !== subs[name].workdir; + }); + const open = new Set(openList); + let pathsToUse = allSubs; + if (0 !== paths.length) { + pathsToUse = Object.keys(yield SubmoduleUtil.resolvePaths(relCwd, + paths, + allSubs, + openList)); + } + const pathsSet = new Set(pathsToUse); + const subShas = {}; + for (let i = 0; i < allSubs.length; ++i) { + const name = allSubs[i]; + if (pathsSet.has(name) && (verbose || open.has(name))) { + const sub = subs[name]; + if (undefined === sub) { + const entry = yield tree.entryByPath(name); + subShas[name] = entry.sha(); + } + else { + subShas[name] = sub.index && sub.index.sha; + } + } + } + const result = PrintStatusUtil.printSubmoduleStatus(relCwd, + subShas, + open, + verbose); + process.stdout.write(result); }); const doFindCommand = co.wrap(function *(path, metaCommittish, subCommittish) { diff --git a/node/lib/util/cherrypick.js b/node/lib/util/cherrypick.js index 5126e4fad..e70a833e1 100644 --- a/node/lib/util/cherrypick.js +++ b/node/lib/util/cherrypick.js @@ -129,13 +129,16 @@ ${colors.green(commitSha)}.`); // Cherry-pick each submodule changed in `commit`. - Object.keys(changes.changed).forEach(subName => { - const headSub = headSubs[subName]; - const commitSub = commitSubs[subName]; - const headSha = headSub.sha; - if (undefined !== commitSub && - headSha !== commitSub.sha) { - pickers.push(picker(subName, headSha, commitSub.sha)); + Object.keys(changes).forEach(subName => { + const change = changes[subName]; + if (null !== change.oldSha && null !== change.newSha) { + const headSub = headSubs[subName]; + const commitSub = commitSubs[subName]; + const headSha = headSub.sha; + if (undefined !== commitSub && + headSha !== commitSub.sha) { + pickers.push(picker(subName, headSha, commitSub.sha)); + } } }); diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 9c0b41a27..b352d9d64 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -49,6 +49,8 @@ const Open = require("./open"); const RepoStatus = require("./repo_status"); const PrintStatusUtil = require("./print_status_util"); const StatusUtil = require("./status_util"); +const Submodule = require("./submodule"); +const SubmoduleConfigUtil = require("./submodule_config_util"); const SubmoduleUtil = require("./submodule_util"); const TreeUtil = require("./tree_util"); const UserError = require("./user_error"); @@ -720,7 +722,7 @@ exports.getCommitMetaData = function (commit) { * amended. * * @param {RepoStatus.Submodule} status - * @param {Submodule|null} old + * @param {String|null} old * @return {Object} * @return {CommitMetaData|null} return.oldCommit if sub in last commit * @return {RepoStatus.Submodule|null} return.status null if shouldn't exist @@ -729,6 +731,10 @@ exports.getSubmoduleAmendStatus = co.wrap(function *(status, old, getRepo, all) { + assert.instanceOf(status, RepoStatus.Submodule); + if (null !== old) { + assert.instanceOf(old, Submodule); + } const index = status.index; const commit = status.commit; const workdir = status.workdir; @@ -807,17 +813,15 @@ exports.getSubmoduleAmendStatus = co.wrap(function *(status, * Return the status object describing an amend commit to be created in the * specified `repo` and a map containing the submodules to have amend * commits created mapped to `CommitMetaData` objects describing their current - * commits; submodules with staged changes not in this map receive - * normal commits. Format paths relative to the specified `cwd`. Include - * changes to the meta-repo if the specified `includeMeta` is true; ignore them - * otherwise. Auto-stage modifications (to tracked files) if the specified - * `all` is true. + * commits; submodules with staged changes not in this map receive normal + * commits. Format paths relative to the specified `cwd`. Ignore + * non-submodule changes to the meta-repo. Auto-stage modifications (to + * tracked files) if the specified `all` is true. * * @param {NodeGit.Repository} repo * @param {Object} [options] * @param {Boolean} [options.all = false] * @param {String} [options.cwd = ""] - * @param {Boolean} [options.includeMeta = false] * * @return {Object} * @return {RepoStatus} return.status @@ -845,55 +849,89 @@ exports.getAmendStatus = co.wrap(function *(repo, options) { else { assert.isString(cwd); } - let includeMeta = options.includeMeta; - if (undefined === includeMeta) { - includeMeta = false; - } - else { - assert.isBoolean(includeMeta); - } const baseStatus = yield exports.getCommitStatus(repo, cwd, { - showMetaChanges: includeMeta, all: all, }); const head = yield repo.getHeadCommit(); + const headTree = yield head.getTree(); - // If we're including the meta-repo, load its changes. - - let metaStaged = {}; - let metaWorkdir = {}; - if (includeMeta) { - const changes = yield getAmendStatusForRepo(repo, all); - metaStaged = changes.staged; - metaWorkdir = changes.workdir; - } + const newUrls = + yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, head); // Read the state of the commits in the commit before the one to be // amended. - let oldSubs = {}; + let oldUrls = {}; const parent = yield GitUtil.getParentCommit(repo, head); let parentTree = null; if (null !== parent) { - const treeId = parent.treeId(); - parentTree = yield NodeGit.Tree.lookup(repo, treeId); - oldSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, parent); + parentTree = yield parent.getTree(); + oldUrls = + yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, parent); } - + const diff = + yield NodeGit.Diff.treeToTree(repo, parentTree, headTree, null); + const changes = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff); const submodules = baseStatus.submodules; // holds resulting sub statuses const opener = new Open.Opener(repo, null); const subsToAmend = {}; // holds map of subs to amend to their commit info - // Loop through submodules that were currently exist in either the last - // commit or the index, adjusting their "base" status to reflect the amend - // change. + // Loop through submodules that either have changes against the current + // commit, or were changed in the current commit. - yield Object.keys(submodules).map(co.wrap(function *(name) { - const currentSub = submodules[name]; - const old = oldSubs[name] || null; + const subsToInspect = Array.from(new Set( + Object.keys(submodules).concat(Object.keys(changes)))); + + yield subsToInspect.map(co.wrap(function *(name) { + const change = changes[name]; + let currentSub = submodules[name]; + let old = null; + if (undefined !== change) { + // We handle deleted submodules later. TODO: this should not be a + // special case when we've done the refactoring noted below. + + if (null === change.newSha) { + return; // RETURN + } + // This submodule was affected by the commit; record it's old sha + // if it wasn't added. + + if (null !== change.oldSha) { + old = new Submodule(oldUrls[name], change.oldSha); + } + + if (undefined === currentSub) { + // This submodule is not open though; we need to construct a + // `RepoStatus.Submodule` object for it as if it had been + // loaded; the commit and index parts of this object are the + // same as they cannot have been changed. + // + // TODO: refactor this and `getSubmoduleAmendStatus` to be + // less-wonky, specifically to not deal in terms of + // `RepoAST.Submodule` objects. + + const url = newUrls[name]; + const Submodule = RepoStatus.Submodule; + currentSub = new Submodule({ + commit: new Submodule.Commit(change.newSha, url), + index: new Submodule.Index(change.newSha, + url, + Submodule.COMMIT_RELATION.SAME), + }); + } + } + else { + // This submodule was opened but not changed. Populate 'old' with + // current commit value, if it exists. + + const commit = currentSub.commit; + if (null !== commit) { + old = new Submodule(commit.url, commit.sha); + } + } const getRepo = () => opener.getSubrepo(name); const result = yield exports.getSubmoduleAmendStatus(currentSub, @@ -920,23 +958,21 @@ exports.getAmendStatus = co.wrap(function *(repo, options) { // Look for subs that were removed in the commit we are amending; reflect // their status. - Object.keys(oldSubs).forEach(name => { + Object.keys(changes).forEach(name => { // If we find one, create a status entry for it reflecting its // deletion. - const sub = submodules[name]; - if (undefined === sub) { - const old = oldSubs[name]; + const change = changes[name]; + if (null === change.newSha) { submodules[name] = new RepoStatus.Submodule({ - commit: new RepoStatus.Submodule.Commit(old.sha, old.url), + commit: new RepoStatus.Submodule.Commit(change.sha, + oldUrls[name]), index: null, }); } }); const resultStatus = baseStatus.copy({ - staged: metaStaged, - workdir: metaWorkdir, submodules: submodules, }); @@ -1471,11 +1507,9 @@ exports.calculateAllRepoStatus = function (normalStatus, toWorkdirStatus) { /** * Return the status of the specified `repo` indicating a commit that would be * performed, including all (tracked) modified files if the specified `all` is - * provided (default false) and the state of them meta-repo if the specified - * `showMetaChanges` is true (default is false). Restrict status to the - * specified `paths` if nonempty (default []), using the specified `cwd` to - * resolve their meaning. The behavior undefined unless - * `0 === paths.length || !all`. + * provided (default false). Restrict status to the specified `paths` if + * nonempty (default []), using the specified `cwd` to resolve their meaning. + * The behavior undefined unless `0 === paths.length || !all`. * * @param {NodeGit.Repository} repo * @param {String} cwd @@ -1523,17 +1557,11 @@ exports.getCommitStatus = co.wrap(function *(repo, cwd, options) { if (0 !== options.paths.length) { // Doing path-based status. First, we need to compute the // `commitStatus` object that reflects the paths requested by the user. - // First, we need to resolve the relative paths. - - const paths = yield options.paths.map(filename => { - return GitUtil.resolveRelativePath(repo.workdir(), cwd, filename); - }); - - // Now we get the path-based status. const requestedStatus = yield StatusUtil.getRepoStatus(repo, { + cwd: cwd, + paths: options.paths, showMetaChanges: options.showMetaChanges, - paths: paths, }); return exports.calculatePathCommitStatus(baseStatus, requestedStatus); @@ -1831,7 +1859,6 @@ function abortForNoMessage() { exports.doCommitCommand = co.wrap(function *(repo, cwd, message, - meta, all, paths, interactive, @@ -1841,7 +1868,6 @@ exports.doCommitCommand = co.wrap(function *(repo, if (null !== message) { assert.isString(message); } - assert.isBoolean(meta); assert.isBoolean(all); assert.isArray(paths); assert.isBoolean(interactive); @@ -1850,7 +1876,6 @@ exports.doCommitCommand = co.wrap(function *(repo, const workdir = repo.workdir(); const relCwd = path.relative(workdir, cwd); const repoStatus = yield exports.getCommitStatus(repo, cwd, { - showMetaChanges: meta, all: all, paths: paths, }); @@ -1958,7 +1983,6 @@ exports.doCommitCommand = co.wrap(function *(repo, * @param {NodeGit.Repository} repo * @param {String} cwd * @param {String|null} message - * @param {Boolean} meta * @param {Boolean} all * @param {Boolean} interactive * @param {(repo, txt) -> Promise(String) | null} editMessage @@ -1969,7 +1993,6 @@ exports.doCommitCommand = co.wrap(function *(repo, exports.doAmendCommand = co.wrap(function *(repo, cwd, message, - meta, all, interactive, editMessage) { @@ -1978,7 +2001,6 @@ exports.doAmendCommand = co.wrap(function *(repo, if (null !== message) { assert.isString(message); } - assert.isBoolean(meta); assert.isBoolean(all); assert.isBoolean(interactive); if (null !== editMessage) { @@ -1988,7 +2010,6 @@ exports.doAmendCommand = co.wrap(function *(repo, const workdir = repo.workdir(); const relCwd = path.relative(workdir, cwd); const amendStatus = yield exports.getAmendStatus(repo, { - includeMeta: meta, all: all, cwd: relCwd, }); @@ -2062,5 +2083,4 @@ You can make this commit using the interactive ('-i') commit option.`; all, message, subMessages); - }); diff --git a/node/lib/util/merge.js b/node/lib/util/merge.js index a7f7fb560..d83197f34 100644 --- a/node/lib/util/merge.js +++ b/node/lib/util/merge.js @@ -35,11 +35,12 @@ const co = require("co"); const colors = require("colors"); const NodeGit = require("nodegit"); -const GitUtil = require("./git_util"); -const Open = require("./open"); -const RepoStatus = require("./repo_status"); -const SubmoduleUtil = require("./submodule_util"); -const UserError = require("./user_error"); +const GitUtil = require("./git_util"); +const Open = require("./open"); +const RepoStatus = require("./repo_status"); +const SubmoduleConfigUtil = require("./submodule_config_util"); +const SubmoduleUtil = require("./submodule_util"); +const UserError = require("./user_error"); /** * @enum {MODE} @@ -156,7 +157,10 @@ ${colors.red(commitSha)}.`); const subCommits = {}; // Record of merge commits in submodules. - const subs = metaRepoStatus.submodules; + const subUrls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(metaRepo, + head); + const headTree = yield head.getTree(); + //const subs = metaRepoStatus.submodules; const opener = new Open.Opener(metaRepo, null); const subFetcher = yield opener.fetcher(); @@ -174,7 +178,7 @@ ${colors.red(commitSha)}.`); // If it's not a submodule move on. - if (!(path in subs)) { + if (!(path in subUrls)) { return; // RETURN } @@ -182,8 +186,8 @@ ${colors.red(commitSha)}.`); const subSha = entry.id.tostrS(); const subCommitId = NodeGit.Oid.fromString(subSha); - const sub = subs[path]; - const subHeadSha = sub.commit.sha; + const subEntry = yield headTree.entryByPath(path); + const subHeadSha = subEntry.sha(); const subCommitSha = subCommitId.tostrS(); // Exit early without opening if we have the same commit as the one @@ -193,16 +197,7 @@ ${colors.red(commitSha)}.`); return; // RETURN } - let subRepo; - if (null === sub.workdir) { - // If this submodule's not open, open it. - - console.log(`Opening ${colors.blue(path)}.`); - subRepo = yield opener.getSubrepo(path); - } - else { - subRepo = yield opener.getSubrepo(path); - } + const subRepo = yield opener.getSubrepo(path); // Fetch commit to merge. diff --git a/node/lib/util/print_status_util.js b/node/lib/util/print_status_util.js index a6c9c258e..f9f9c3113 100644 --- a/node/lib/util/print_status_util.js +++ b/node/lib/util/print_status_util.js @@ -40,9 +40,9 @@ const assert = require("chai").assert; const colors = require("colors/safe"); const path = require("path"); -const GitUtil = require("../util/git_util"); -const Rebase = require("../util/rebase"); -const RepoStatus = require("../util/repo_status"); +const GitUtil = require("./git_util"); +const Rebase = require("./rebase"); +const RepoStatus = require("./repo_status"); /** * This value-semantic class describes a line entry to be printed in a status @@ -437,34 +437,41 @@ Untracked files: }; /** - * Print a string describing the status of the submodules in the specified - * `status`; show closed submodules if the specified `showClosed` is true; show - * names relative to the specified `relCwd`. + * Print a string describing the status of the specified `subsToPrint`. Use + * the specified `openSubs` set to determine which submodules are open. Unless + * the specified `true === showClosed`, do not print closed sub modules. Use + * the specified `relCwd` to display relative paths. * - * @param {RepoStatus} status * @param {String} relCwd + * @param {Object} subsToPrint map from name to sha or null if deleted + * @param {Set(String)} openSubs set of names of open submodules * @param {Boolean} showClosed * @return {String} */ -exports.printSubmoduleStatus = function (status, relCwd, showClosed) { - assert.instanceOf(status, RepoStatus); +exports.printSubmoduleStatus = function (relCwd, + subsToPrint, + openSubs, + showClosed) { assert.isString(relCwd); + assert.isObject(subsToPrint); assert.isBoolean(showClosed); + let result = ""; - const subStats = status.submodules; if (showClosed) { result = `${colors.grey("All submodules:")}\n`; } else { result = `${colors.grey("Open submodules:")}\n`; } - const names = Object.keys(subStats).sort(); + const names = Object.keys(subsToPrint).sort(); names.forEach(name => { const relName = path.relative(relCwd, name); - const sub = subStats[name]; - const isVis = null !== sub.workdir; + const isVis = openSubs.has(name); const visStr = isVis ? " " : "-"; - const sha = (sub.index && sub.index.sha) || ""; + let sha = subsToPrint[name]; + if (null === sha) { + sha = ""; + } if (isVis || showClosed) { result += `${visStr} ${sha} ${colors.cyan(relName)}\n`; } diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 124a222e2..d23524a43 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -292,13 +292,15 @@ exports.getSubmoduleStatus = co.wrap(function *(repo, * true), return the status of changes in `repo`; otherwise, show only changes * in submobules. If the optionally specified `ignoreIndex` is specified, * calculate the status matching the workdir to the underlying commit rather - * than against the index. + * than against the index. If the specified `options.cwd` is provided, resolve + * paths in the context of that directory. * * @async * @param {NodeGit.Repository} repo * @param {Object} [options] * @param {Boolean} [options.showAllUntracked] * @param {String []} [options.paths] + * @param {String} [options.cwd] * @param {Boolean} [options.showMetaChanges] * @param {Boolean} [options.ignoreIndex] * @return {RepoStatus} @@ -327,7 +329,7 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { assert.isArray(options.paths); } if (undefined === options.showMetaChanges) { - options.showMetaChanges = true; + options.showMetaChanges = false; } else { assert.isBoolean(options.showMetaChanges); @@ -338,7 +340,14 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { else { assert.isBoolean(options.ignoreIndex); } - + if (undefined !== options.cwd) { + assert.isString(options.cwd); + options.paths = yield options.paths.map(filename => { + return GitUtil.resolveRelativePath(repo.workdir(), + options.cwd, + filename); + }); + } const headCommit = yield repo.getHeadCommit(); let args = { @@ -395,48 +404,68 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { const openArray = yield SubmoduleUtil.listOpenSubmodules(repo); const openSet = new Set(openArray); const index = yield repo.index(); + const headTree = yield headCommit.getTree(); + const diff = yield NodeGit.Diff.treeToIndex(repo, headTree, index); + const changes = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff); const indexUrls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); - const indexNames = Object.keys(indexUrls); const headUrls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, headCommit); - // No paths specified, so we'll do all submodules, restricing to open + // No paths specified, so we'll do all submodules, restricting to open // ones based on options. let filterPaths; // map from sub name to paths to use - let subsToList; // array of subs that will be in result const filtering = 0 !== options.paths.length; + + // Will look at submodules that are open or have changes. TODO: we're + // ignoring changes affecting only the `.gitmodules` file for now. + + let subsToList = Array.from(new Set( + openArray.concat(Object.keys(changes)))); + if (filtering) { filterPaths = yield SubmoduleUtil.resolvePaths(repo.workdir(), options.paths, - indexNames, + subsToList, openArray); subsToList = Object.keys(filterPaths); } - else { - // Compute the list by joining the list of submodules listed in the - // index and on head. - subsToList = Array.from(new Set( - Object.keys(headUrls).concat(indexNames))); - } - const commitTree = yield headCommit.getTree(); // Make a list of promises to read the status for each submodule, then // evaluate them in parallel. const subStatMakers = subsToList.map(co.wrap(function *(name) { - const headUrl = headUrls[name]; - let headSha = null; - if (undefined !== headUrl) { - headSha = (yield commitTree.entryByPath(name)).sha(); - } + const headUrl = headUrls[name] || null; + const indexUrl = indexUrls[name] || null; + let headSha = null; let indexSha = null; - const entry = index.getByPath(name); - if (entry) { - indexSha = entry.id.tostrS(); - } let subRepo = null; + + // Load commit information available based on whether the submodule + // was added, removed, changed, or just open. + + const change = changes[name]; + if (undefined !== change) { + headSha = change.oldSha; + indexSha = change.newSha; + } + else { + // Just open, we need to load its sha. Unfortunately, the diff + // we did above doesn't catch new submodules with unstaged + // commits; validate that we have commit and index URLs and + // entries before trying to read them. + + if (null !== headUrl) { + headSha = (yield headTree.entryByPath(name)).sha(); + } + if (null !== indexUrl) { + const indexEntry = index.getByPath(name); + if (undefined !== indexEntry) { + indexSha = indexEntry.id.tostrS(); + } + } + } let status = null; if (openSet.has(name)) { subRepo = yield SubmoduleUtil.getRepo(repo, name); @@ -444,12 +473,13 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { paths: filtering ? filterPaths[name] : [], showAllUntracked: options.showAllUntracked, ignoreIndex: options.ignoreIndex, + showMetaChanges: true, }); } return yield exports.getSubmoduleStatus(subRepo, status, - indexUrls[name] || null, - headUrls[name] || null, + indexUrl, + headUrl, indexSha, headSha); })); diff --git a/node/lib/util/submodule_change.js b/node/lib/util/submodule_change.js new file mode 100644 index 000000000..32e86fb7e --- /dev/null +++ b/node/lib/util/submodule_change.js @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2017, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; + +/** + * @class SubmoduleChanges.Change + * + * This class represents a sha change to a submodule. + */ +class SubmoduleChange { + + /** + * Creat a new `Changed` object having the specified `oldSha` and `newSha` + * values. The behavior is undefined if `oldSha === newSha`. Note that a + * null `oldSha` implies that the submodule was added, a null `newSha` + * implies that it was removed, and if neither is null, the submodule was + * changed. + * + * @param {String | null} oldSha + * @param {String | null} newSha + */ + constructor(oldSha, newSha) { + assert.notEqual(oldSha, newSha); + if (null !== oldSha) { + assert.isString(oldSha); + } + if (null !== newSha) { + assert.isString(newSha); + } + this.d_oldSha = oldSha; + this.d_newSha = newSha; + Object.freeze(this); + } + + /** + * This property represents the previous value of the sha for a submodule + * change. If this value is null, then the submodule was added. + * + * @property {String | null} oldSha + */ + get oldSha() { + return this.d_oldSha; + } + + /** + * This property represents the new value of a sha for a submodule. If it + * is null, then the submodule was removed. + * + * @property {String | null} newSha + */ + get newSha() { + return this.d_newSha; + } +} + +module.exports = SubmoduleChange; diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index b1549bd6c..72169ad4e 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -43,6 +43,7 @@ const path = require("path"); const GitUtil = require("./git_util"); const Submodule = require("./submodule"); +const SubmoduleChange = require("./submodule_change"); const SubmoduleFetcher = require("./submodule_fetcher"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -305,41 +306,18 @@ exports.getSubmoduleRepos = co.wrap(function *(repo) { }); /** - * Return a summary of the submodule SHAs changed by the specified `commitId` in - * the specified `repo`, and flag denoting whether or not the `.gitmodules` - * file was changed. + * Return a summary of the submodule SHA changes in the specified `diff`. + * TODO: Test this separately from `getSubmoduleChanges`. * * @asycn - * @param {NodeGit.Repository} repo - * @param {NodeGit.Commit} commit - * @return {Object} - * @return {Object} return.added map from path to SHA - * @return {Object} return.changed map from path to new and old SHAs - * @return {Object} return.removed map from path to SHA - * @return {Boolean} return.modulesFileChanged true if modules file changed + * @param {NodeGit.Diff} diff + * @return {Object} map from name to `SubmoduleChange` */ -exports.getSubmoduleChanges = co.wrap(function *(repo, commit) { - assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(commit, NodeGit.Commit); - - // We calculate the changes of a commit against it's first parent. If it - // has no parents, then the calculation is against an empty tree. - - let parentTree = null; - const parents = yield commit.getParents(); - if (0 !== parents.length) { - parentTree = yield parents[0].getTree(); - } +exports.getSubmoduleChangesFromDiff = function (diff) { + assert.instanceOf(diff, NodeGit.Diff); - const tree = yield commit.getTree(); - const diff = yield NodeGit.Diff.treeToTree(repo, parentTree, tree, null); const num = diff.numDeltas(); - const result = { - added: {}, - changed: {}, - removed: {}, - modulesFileChanged: false, - }; + const result = {}; const DELTA = NodeGit.Diff.DELTA; const COMMIT = NodeGit.TreeEntry.FILEMODE.COMMIT; for (let i = 0; i < num; ++i) { @@ -358,38 +336,58 @@ exports.getSubmoduleChanges = co.wrap(function *(repo, commit) { const newFile = delta.newFile(); const path = newFile.path(); if (COMMIT === newFile.mode()) { - result.changed[path] = { - "new": newFile.id().tostrS(), - "old": delta.oldFile().id().tostrS(), - }; - } - else if (SubmoduleConfigUtil.modulesFileName === path) { - result.modulesFileChanged = true; + result[path] = new SubmoduleChange( + delta.oldFile().id().tostrS(), + newFile.id().tostrS()); } } break; case DELTA.ADDED: { const newFile = delta.newFile(); const path = newFile.path(); if (COMMIT === newFile.mode()) { - result.added[newFile.path()] = newFile.id().tostrS(); - } - else if (SubmoduleConfigUtil.modulesFileName === path) { - result.modulesFileChanged = true; + result[path] = new SubmoduleChange(null, + newFile.id().tostrS()); } } break; case DELTA.DELETED: { const oldFile = delta.oldFile(); const path = oldFile.path(); if (COMMIT === oldFile.mode()) { - result.removed[oldFile.path()] = oldFile.id().tostrS(); - } - else if (SubmoduleConfigUtil.modulesFileName === path) { - result.modulesFileChanged = true; + result[path] = new SubmoduleChange(oldFile.id().tostrS(), + null); } } break; } } return result; +}; + +/** + * Return a summary of the submodule SHAs changed by the specified `commitId` + * in the specified `repo`, and flag denoting whether or not the `.gitmodules` + * file was changed. + * + * @asycn + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + * @return {Object} map from name to `SubmoduleChange` + */ +exports.getSubmoduleChanges = co.wrap(function *(repo, commit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(commit, NodeGit.Commit); + + // We calculate the changes of a commit against its first parent. If it + // has no parents, then the calculation is against an empty tree. + + let parentTree = null; + const parents = yield commit.getParents(); + if (0 !== parents.length) { + parentTree = yield parents[0].getTree(); + } + + const tree = yield commit.getTree(); + const diff = yield NodeGit.Diff.treeToTree(repo, parentTree, tree, null); + return yield exports.getSubmoduleChangesFromDiff(diff); }); /** diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 76375bc6d..3cab0b346 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -135,8 +135,14 @@ function* checkSubmodules(repo, commit) { const getChanges = SubmoduleUtil.getSubmoduleChanges; const changes = yield getChanges(repo, commit); const allChanges = [ - Object.keys(changes.added), - Object.keys(changes.changed) + Object.keys(changes).filter(changeName => { + const change = changes[changeName]; + return null === change.oldSha; + }), + Object.keys(changes).filter(changeName => { + const change = changes[changeName]; + return null !== change.oldSha && null !== change.newSha; + }), ]; const result = allChanges.map(function *(changeSet) { const result = changeSet.map(function *(path) { diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 7c57b62a5..9ca8903ab 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -683,7 +683,6 @@ Untracked files: expected: base, options: { all: false, - showMetaChanges: true, paths: [], }, }, @@ -691,107 +690,9 @@ Untracked files: state: "x=S:W foo=bar", expected: base, }, - "with meta": { + "with meta, ignored": { state: "x=S:W foo=bar", - options: { - showMetaChanges: true, - }, - expected: base.copy({ - workdir: { foo: FILESTATUS.ADDED }, - }), - }, - "staged": { - state: "x=S:I a=b", - options: { - showMetaChanges: true, - all: true, - }, - expected: base.copy({ - staged: { - a: FILESTATUS.ADDED, - }, - }), - }, - "without paths": { - state: "x=S:I foo=bar,baz=bam", - options: { - showMetaChanges: true, - }, - expected: base.copy({ - staged: { - foo: FILESTATUS.ADDED, - baz: FILESTATUS.ADDED, - }, - }), - }, - "with paths": { - state: "x=S:I foo/bar=bar,baz=bam", - options: { - showMetaChanges: true, - paths: [ "foo" ], - }, - expected: base.copy({ - staged: { - "foo/bar": FILESTATUS.ADDED, - }, - workdir: { - "baz": FILESTATUS.ADDED, - }, - }), - }, - "without relative path": { - state: "x=S:I foo/bar=bar,bar=bam", - options: { - showMetaChanges: true, - paths: [ "bar" ], - }, - expected: base.copy({ - staged: { - "bar": FILESTATUS.ADDED, - }, - workdir: { - "foo/bar": FILESTATUS.ADDED, - }, - }), - }, - "with relative path": { - state: "x=S:I foo/bar=bar,bar=bam", - options: { - showMetaChanges: true, - paths: [ "bar" ], - }, - workdir: "foo", - expected: base.copy({ - staged: { - "foo/bar": FILESTATUS.ADDED, - }, - workdir: { - "bar": FILESTATUS.ADDED, - }, - }), - }, - "without all": { - state: "x=S:W README.md=88", - options: { - showMetaChanges: true, - }, - expected: base.copy({ - workdir: { - "README.md": FILESTATUS.MODIFIED, - }, - }), - }, - "with all": { - state: "x=S:W README.md=88", - options: { - showMetaChanges: true, - all: true, - }, - expected: base.copy({ - staged: { - "README.md": FILESTATUS.MODIFIED, - }, - }), + expected: base, }, "sub staged": { state: "a=B|x=U:C3-2;Bmaster=3;Os I a=b", @@ -1000,11 +901,14 @@ x=E:Cx-2 x=Sq:1;Bmaster=x;I s=~,x=~`, // Will always use subrepo 's' in repo 'x' const cases = { "unchanged": { - input: "a=B|x=U:C3-2;Bmaster=3", + input: "a=B|x=U:C3-2;Bmaster=3;Os", expected: { status: new Submodule({ commit: new Submodule.Commit("1", "a"), index: new Submodule.Index("1", "a", RELATION.SAME), + workdir: new Submodule.Workdir(new RepoStatus({ + headCommit: "1", + }), RELATION.SAME), }), }, }, @@ -1022,11 +926,14 @@ x=E:Cx-2 x=Sq:1;Bmaster=x;I s=~,x=~`, }, }, "added in commit": { - input: "a=B|x=U", + input: "a=B|x=U:Os", expected: { status: new Submodule({ commit: null, index: new Submodule.Index("1", "a", null), + workdir: new Submodule.Workdir(new RepoStatus({ + headCommit: "1", + }), RELATION.SAME), }), }, }, @@ -1187,51 +1094,13 @@ a=B:Chi#a-1;Ba=a|x=U:C3-2 s=Sa:a;Bmaster=3;Os W README.md=888`, }), }, }, - "include meta": { - state: "x=S:C2-1;Bmaster=2;I a=b;W README.md=888", - includeMeta: true, - expected: { - status: new RepoStatus({ - currentBranchName: "master", - headCommit: "2", - staged: { - a: FILESTATUS.ADDED, - "2": FILESTATUS.ADDED, - }, - workdir: { - "README.md": FILESTATUS.MODIFIED, - }, - }), - }, - }, - "include meta, all": { - state: "x=S:C2-1;Bmaster=2;I a=b;W README.md=888", - includeMeta: true, - all: true, - expected: { - status: new RepoStatus({ - currentBranchName: "master", - headCommit: "2", - staged: { - a: FILESTATUS.ADDED, - "2": FILESTATUS.ADDED, - "README.md": FILESTATUS.MODIFIED, - }, - }), - }, - }, "sub, no amend": { state: "a=B|x=U:C3-2;Bmaster=3", expected: { status: new RepoStatus({ currentBranchName: "master", headCommit: "3", - submodules: { - s: new Submodule({ - commit: new Submodule.Commit("1", "a"), - index: new Submodule.Index("1", "a", SAME), - }), - }, + submodules: {}, }), }, }, @@ -1470,15 +1339,6 @@ x=N:Cfoo\n#x README.md=hello world;*=master;Bmaster=x`, input: "x=N:Cm#1;H=1", expected: "x=N:Cx 1=1;H=x", }, - "meta change": { - input: "x=S:C2-1;Bmaster=2;I README.md=3", - expected: "x=S:Cx-1 README.md=3,2=2;Bmaster=x", - }, - "meta staged": { - input: "x=S:C2-1;Bmaster=2;W README.md=3", - expected: "x=S:Cx-1 README.md=3,2=2;Bmaster=x", - all: true, - }, "repo with new sha in index": { input: "a=B:Ca-1;Bmaster=a|x=U:C3-2;I s=Sa:a;Bmaster=3", expected: "x=U:Cx-2 3=3,s=Sa:a;Bmaster=x", @@ -3052,41 +2912,28 @@ and for d "nothing to commit": { initial: "x=S", }, - "no meta, no commit": { - initial: "x=S:I a=b", - meta: false, - }, - "meta commit": { + "meta changes, ignored": { initial: "x=S:I a=b", message: "foo\n", - expected: "x=S:Cfoo\n#x-1 a=b;Bmaster=x", - }, - "meta commit, with editor": { - initial: "x=S:I a=b", - editor: () => Promise.resolve("haha"), - expected: "x=S:Chaha\n#x-1 a=b;Bmaster=x", - }, - "interactive meta commit, but do nothing": { - initial: "x=S:I a=b", - interactive: true, - editor: (_, content) => Promise.resolve(content), - fails: true, }, "no all": { - initial: "x=S:W README.md=2", + initial: "a=B|x=U:Os W README.md=2", + message: "foo", }, "all": { - initial: "x=S:W README.md=2", - all: true, + initial: "a=B|x=U:Os W README.md=2", message: "foo", - expected: "x=S:Cfoo\n#x-1 README.md=2;Bmaster=x", + all: true, + expected: ` +x=S:Cfoo\n#x-2 s=Sa:s;Os Cfoo\n#s-1 README.md=2!H=s;Bmaster=x`, }, "paths, cwd": { - initial: "x=S:I a/b=b,b=d", + initial: "a=B|x=U:Os I a/b=b,b=d", message: "foo", paths: ["b"], - cwd: "a", - expected: "x=S:Cfoo\n#x-1 a/b=b;I b=d;Bmaster=x", + cwd: "s/a", + expected: ` +x=S:Cfoo\n#x-2 s=Sa:s;Os Cfoo\n#s-1 a/b=b!I b=d!H=s;Bmaster=x`, }, "uncommitable": { initial: "a=B|x=S:I a=Sa:;Oa", @@ -3131,12 +2978,10 @@ x=U:Cfoo\n#x-2 s=Sa:s;Os Cbar\n#s-1 a=b!H=s;Bmaster=x`, const editor = c.editor || (() => { assert(false, "no editor"); }); - const meta = undefined === c.meta ? true : false; const result = yield Commit.doCommitCommand( repo, cwd, c.message || null, - meta, c.all || false, c.paths || [], c.interactive || false, @@ -3190,17 +3035,10 @@ x=U:Chola\n#x-2 s=Sa:s;Bmaster=x;Os Cthere\n#s-1 a=a!H=s`, message: "foo", expected: "x=S:Cfoo\n#x-1 2=2;Bmaster=x;W README.md=8", }, - "amend with all": { - initial: "x=S:C2-1;Bmaster=2;W README.md=8", - message: "foo", - all: true, - expected: "x=S:Cfoo\n#x-1 2=2,README.md=8;Bmaster=x", - }, "amend with all but no meta": { initial: "x=S:C2-1;Bmaster=2;W README.md=8", message: "foo", all: true, - meta: false, expected: "x=S:Cfoo\n#x-1 2=2;Bmaster=x;W README.md=8", }, "mismatch": { @@ -3254,12 +3092,10 @@ x=U:Chola\n#x-2 s=Sa:s;Bmaster=x;Os Cthere\n#s-1 a=a!H=s`, if (undefined === editor) { editor = () => assert(false, "no editor"); } - const meta = undefined === c.meta ? true : false; const result = yield Commit.doAmendCommand( repo, cwd, c.message || null, - meta, c.all || false, c.interactive || false, editor); diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js index d2007cde7..7448db689 100644 --- a/node/test/util/print_status_util.js +++ b/node/test/util/print_status_util.js @@ -794,15 +794,17 @@ Changes to be committed: describe("printSubmoduleStatus", function () { const cases = { "empty show closed": { - status: new RepoStatus(), relCwd: "", + subsToPrint: {}, + openSubs: new Set(), showClosed: true, expected: `\ ${colors.grey("All submodules:")} `, }, "empty no show closed": { - status: new RepoStatus(), + subsToPrint: {}, + openSubs: new Set(), relCwd: "", showClosed: false, expected: `\ @@ -810,14 +812,8 @@ ${colors.grey("Open submodules:")} `, }, "a closed sub, not shown": { - status: new RepoStatus({ - submodules: { - foo: new Submodule({ - commit: new Commit("1", "/a"), - index: new Index("1", "/a", RELATION.SAME), - }), - }, - }), + subsToPrint: { foo: "1", }, + openSubs: new Set(), relCwd: "", showClosed: false, expected: `\ @@ -825,14 +821,8 @@ ${colors.grey("Open submodules:")} `, }, "a closed sub, shown": { - status: new RepoStatus({ - submodules: { - foo: new Submodule({ - commit: new Commit("1", "/a"), - index: new Index("1", "/a", RELATION.SAME), - }), - }, - }), + subsToPrint: { foo: "1", }, + openSubs: new Set(), relCwd: "", showClosed: true, expected: `\ @@ -841,17 +831,10 @@ ${colors.grey("All submodules:")} `, }, "an open sub": { - status: new RepoStatus({ - submodules: { - bar: new Submodule({ - commit: new Commit("1", "/a"), - index: new Index("1", "/a", RELATION.SAME), - workdir: new Workdir(new RepoStatus({ - headCommit: "1", - }), RELATION.SAME), - }), - }, - }), + subsToPrint: { + bar: "1", + }, + openSubs: new Set(["bar"]), relCwd: "", showClosed: true, expected: `\ @@ -860,21 +843,11 @@ ${colors.grey("All submodules:")} `, }, "an open sub and closed": { - status: new RepoStatus({ - submodules: { - foo: new Submodule({ - commit: new Commit("1", "/a"), - index: new Index("1", "/a", RELATION.SAME), - }), - bar: new Submodule({ - commit: new Commit("1", "/a"), - index: new Index("1", "/a", RELATION.SAME), - workdir: new Workdir(new RepoStatus({ - headCommit: "1", - }), RELATION.SAME), - }), - }, - }), + subsToPrint: { + foo: "1", + bar: "1", + }, + openSubs: new Set(["bar"]), relCwd: "", showClosed: true, expected: `\ @@ -884,17 +857,8 @@ ${colors.grey("All submodules:")} `, }, "with relative workdir": { - status: new RepoStatus({ - submodules: { - bar: new Submodule({ - commit: new Commit("1", "/a"), - index: new Index("1", "/a", RELATION.SAME), - workdir: new Workdir(new RepoStatus({ - headCommit: "1", - }), RELATION.SAME), - }), - }, - }), + subsToPrint: { bar: "1", }, + openSubs: new Set(["bar"]), relCwd: "q", showClosed: true, expected: `\ @@ -903,14 +867,8 @@ ${colors.grey("All submodules:")} `, }, "deleted": { - status: new RepoStatus({ - submodules: { - bar: new Submodule({ - commit: new Commit("1", "/a"), - index: null, - }), - }, - }), + subsToPrint: { bar: null }, + openSubs: new Set(), relCwd: "", showClosed: true, expected: `\ @@ -923,8 +881,9 @@ ${colors.grey("All submodules:")} const c = cases[caseName]; it(caseName, function () { const result = PrintStatusUtil.printSubmoduleStatus( - c.status, c.relCwd, + c.subsToPrint, + c.openSubs, c.showClosed); const resultLines = result.split("\n"); const expectedLines = c.expected.split("\n"); diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index 8a8c72bf6..c1d69f7ea 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -153,7 +153,9 @@ x=E:Ci#i foo=bar,1=1;Cw#w foo=bar,1=1;Bi=i;Bw=w`, it(caseName, co.wrap(function *() { const stasher = co.wrap(function *(repos) { const repo = repos.x; - const status = yield StatusUtil.getRepoStatus(repo); + const status = yield StatusUtil.getRepoStatus(repo, { + showMetaChanges: true, + }); const includeUntracked = c.includeUntracked || false; const result = yield StashUtil.stashRepo(repo, status, diff --git a/node/test/util/status_util.js b/node/test/util/status_util.js index e3c7fef73..a316d3d1c 100644 --- a/node/test/util/status_util.js +++ b/node/test/util/status_util.js @@ -32,6 +32,7 @@ const assert = require("chai").assert; const co = require("co"); +const path = require("path"); const Rebase = require("../../lib/util/rebase"); const RepoAST = require("../../lib/util/repo_ast"); @@ -502,6 +503,9 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, }, "staged change": { state: "x=S:I README.md=whoohoo", + options: { + showMetaChanges: true, + }, expected: new RepoStatus({ currentBranchName: "master", headCommit: "1", @@ -518,7 +522,7 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, "x/y/q": FILESTATUS.ADDED, }, }), - options: { showAllUntracked: true, }, + options: { showAllUntracked: true, showMetaChanges: true }, }, "ignore meta": { state: "x=S:I README.md=whoohoo", @@ -527,16 +531,16 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, headCommit: "1", staged: {}, }), - options: { showMetaChanges: false }, }, // The logic for filtering is tested earlier; here, we just need to // validate that the option is propagated properly. "path filtered out in meta": { - state: "x=S:I x/y=a,README.md=sss", + state: "x=S:I x/y=a,README.md=sss,y=foo", options: { paths: ["README.md"], + showMetaChanges: true, }, expected: new RepoStatus({ currentBranchName: "master", @@ -544,6 +548,19 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, staged: { "README.md": FILESTATUS.MODIFIED }, }), }, + "path resolved with cwd": { + state: "x=S:I x/y=a,README.md=sss,y=foo", + options: { + cwd: "x", + paths: ["y"], + showMetaChanges: true, + }, + expected: new RepoStatus({ + currentBranchName: "master", + headCommit: "1", + staged: { "x/y": FILESTATUS.ADDED }, + }), + }, // Submodules are tested earlier, but we need to test a few // concerns: @@ -643,9 +660,17 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, "x/": FILESTATUS.ADDED, }, }), + options: { showMetaChanges: true, }, }, - "filtered out": { + "no changes, ingored": { state: "a=B|x=U", + expected: new RepoStatus({ + currentBranchName: "master", + headCommit: "2", + }), + }, + "filtered out": { + state: "a=B:Ca-1;Ba=a|x=U:I s=Sa:a", options: { paths: ["README.md"], }, @@ -655,19 +680,14 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, }), }, "filtered in": { - state: "a=B|x=U", + state: "a=B:Ca-1;Ba=a|x=U:I s=Sa:a", options: { - paths: ["s"], + paths: ["README.md"], }, expected: new RepoStatus({ currentBranchName: "master", headCommit: "2", - submodules: { - s: new Submodule({ - commit: new Commit("1", "a"), - index: new Index("1", "a", RELATION.SAME), - }), - }, + submodules: {}, }), }, "deep filter": { @@ -723,13 +743,37 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, }, }), }, + "new with staged": { + state: "a=B|x=S:I s=Sa:;Os I q=r", + expected: new RepoStatus({ + headCommit: "1", + currentBranchName: "master", + submodules: { + s: new Submodule({ + commit: null, + index: new Index(null, "a", null), + workdir: new Workdir(new RepoStatus({ + headCommit: null, + staged: { + q: FILESTATUS.ADDED, + }, + }), null), + }), + }, + }), + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; it(caseName, co.wrap(function *() { const w = yield RepoASTTestUtil.createMultiRepos(c.state); + const options = c.options || {}; + if (undefined !== options.cwd) { + options.cwd = path.join(w.repos.x.workdir(), + options.cwd); + } const result = yield StatusUtil.getRepoStatus(w.repos.x, - c.options); + options); assert.instanceOf(result, RepoStatus); const mappedResult = StatusUtil.remapRepoStatus(result, w.commitMap, diff --git a/node/test/util/submodule_change.js b/node/test/util/submodule_change.js new file mode 100644 index 000000000..c55cb6e0b --- /dev/null +++ b/node/test/util/submodule_change.js @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2017, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; + +const SubmoduleChange = require("../../lib/util/submodule_change"); + +describe("SubmoduleChange", function () { + it("breathing", function () { + const change = new SubmoduleChange("old", "new"); + assert.equal(change.oldSha, "old"); + assert.equal(change.newSha, "new"); + }); +}); diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index 6db494d21..45c5e77e2 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -37,6 +37,7 @@ const path = require("path"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); const TestUtil = require("../../lib/util/test_util"); +const SubmoduleChange = require("../../lib/util/submodule_change"); const Submodule = require("../../lib/util/submodule"); const SubmoduleUtil = require("../../lib/util/submodule_util"); const SubmoduleConfigUtil = require("../../lib/util/submodule_config_util"); @@ -363,113 +364,72 @@ describe("SubmoduleUtil", function () { "trivial": { state: "S", from: "1", - added: {}, - changed: {}, - removed: {}, - modules: false, + result: {}, }, "changed something else": { state: "S:C2-1 README.md=foo;H=2", from: "2", - added: {}, - changed: {}, - removed: {}, - modules: false, + result: {}, }, "removed something else": { state: "S:C2-1 README.md;H=2", from: "2", - added: {}, - changed: {}, - removed: {}, - modules: false, + result: {}, }, "not on current commit": { state: "S:C2-1 x=Sa:1;H=2", from: "1", - added: {}, - changed: {}, - removed: {}, - modules: false, + result: {}, }, "added one": { state: "S:C2-1 x=Sa:1;H=2", from: "2", - added: { - "x": "1", + result: { + "x": new SubmoduleChange(null, "1"), }, - changed: {}, - removed: {}, - modules: true, }, "added two": { state: "S:C2-1 a=Sa:1,x=Sa:1;H=2", from: "2", - added: { - a: "1", - x: "1", + result: { + a: new SubmoduleChange(null, "1"), + x: new SubmoduleChange(null, "1"), }, - changed: {}, - removed: {}, - modules: true, }, "changed one": { state: "S:C3-2 a=Sa:2;C2-1 a=Sa:1,x=Sa:1;H=3", from: "3", - added: {}, - changed: { - a: { - "new": "2", - old: "1", - }, + result: { + a: new SubmoduleChange("1", "2"), }, - removed: {}, - modules: false, }, "changed url": { state: "S:C3-2 a=Sb:1;C2-1 a=Sa:1,x=Sa:1;H=3", from: "3", - added: {}, - changed: {}, - removed: {}, - modules: true, + result: {}, }, "changed and added": { state: "S:C3-2 a=Sa:2,c=Sa:2;C2-1 a=Sa:1,x=Sa:1;H=3", from: "3", - added: { - c: "2", + result: { + a: new SubmoduleChange("1", "2"), + c: new SubmoduleChange(null, "2"), }, - changed: { - a: { - "new": "2", - old: "1", - }, - }, - removed: {}, - modules: true, }, "removed one": { state: "S:C3-2 a=;C2-1 a=Sa:1,x=Sa:1;H=3", from: "3", - added: {}, - changed: {}, - removed: { - "a": "1", + result: { + a: new SubmoduleChange("1", null), }, - modules: true, }, "added and removed": { state: "S:C3-2 a,c=Sa:2;C2-1 a=Sa:1,x=Sa:1;H=3", from: "3", - added: { - c: "2", - }, - changed: {}, - removed: { - a: "1", + result: { + c: new SubmoduleChange(null, "2"), + a: new SubmoduleChange("1", null), }, - modules: true, }, }; Object.keys(cases).forEach(caseName => { @@ -483,30 +443,19 @@ describe("SubmoduleUtil", function () { const changes = yield SubmoduleUtil.getSubmoduleChanges(repo, commit); + const commitMap = written.commitMap; + // map the logical commits in the expected results to the // actual commit ids - const commitMap = written.oldCommitMap; - const expAdded = Object.assign({}, c.added); - for (let name in expAdded) { - expAdded[name] = commitMap[expAdded[name]]; - } - const expChanged = Object.assign({}, c.changed); - for (let name in expChanged) { - expChanged[name] = { - "new": commitMap[expChanged[name]["new"]], - "old": commitMap[expChanged[name].old], - }; - } - const expRemoved = Object.assign({}, c.removed); - for (let name in expRemoved) { - expRemoved[name] = commitMap[expRemoved[name]]; - } - - assert.deepEqual(changes.added, expAdded, "added"); - assert.deepEqual(changes.changed, expChanged, "changed"); - assert.deepEqual(changes.removed, expRemoved, "removed"); - assert.equal(changes.modulesFileChanged, c.modules, "modules"); + Object.keys(changes).forEach(name => { + const change = changes[name]; + assert.instanceOf(change, SubmoduleChange); + const oldSha = change.oldSha && commitMap[change.oldSha]; + const newSha = change.newSha && commitMap[change.newSha]; + changes[name] = new SubmoduleChange(oldSha, newSha); + }); + assert.deepEqual(changes, c.result); })); }); }); From bf28849a65b6210a0869099323f16570f1ccdb13 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 24 Oct 2017 11:11:55 -0400 Subject: [PATCH 004/402] Fix close when closed submodule is in path. --- node/lib/util/close_util.js | 4 ++-- node/test/util/close_util.js | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/node/lib/util/close_util.js b/node/lib/util/close_util.js index 2152b4551..9a0a84f84 100644 --- a/node/lib/util/close_util.js +++ b/node/lib/util/close_util.js @@ -72,10 +72,10 @@ exports.close = co.wrap(function *(repo, cwd, paths, force) { paths); const closers = subsToClose.map(co.wrap(function *(name) { const sub = subStats[name]; - const subWorkdir = sub.workdir; - if (null === subWorkdir) { + if (undefined === sub || null === sub.workdir) { return; // RETURN } + const subWorkdir = sub.workdir; const subRepo = subWorkdir.status; if (!force) { // Determine if there are any uncommited changes: diff --git a/node/test/util/close_util.js b/node/test/util/close_util.js index 300910b23..3b2921d0f 100644 --- a/node/test/util/close_util.js +++ b/node/test/util/close_util.js @@ -53,6 +53,10 @@ describe("close_util", function () { paths: ["s"], expected: "a=B|x=U", }, + "with path and closed sub": { + state: "a=B|x=U", + paths: ["."], + }, "simple with resolved paths": { state: "a=B|x=U:Os", paths: ["s"], From ba1f0ef26366f84a2dfa2adf26b69874d57cf8eb Mon Sep 17 00:00:00 2001 From: Shihui Xiang Date: Tue, 7 Nov 2017 12:29:34 -0500 Subject: [PATCH 005/402] gitmeta add-submodules should sort .gitmodels --- node/lib/util/add_submodule.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/node/lib/util/add_submodule.js b/node/lib/util/add_submodule.js index 2e6ec9454..ac9e47a02 100644 --- a/node/lib/util/add_submodule.js +++ b/node/lib/util/add_submodule.js @@ -73,6 +73,12 @@ exports.addSubmodule = co.wrap(function *(repo, url, filename, importArg) { const index = yield repo.index(); yield index.addByPath(SubmoduleConfigUtil.modulesFileName); yield index.write(); + + // sort all the urls since new modules get added + const urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + const newConf = SubmoduleConfigUtil.writeConfigText(urls); + yield fs.writeFile(modulesPath, newConf); + const metaUrl = yield GitUtil.getOriginUrl(repo); const templatePath = yield SubmoduleConfigUtil.getTemplatePath(repo); const subRepo = yield SubmoduleConfigUtil.initSubmoduleAndRepo( From 5d00d71f4cfb9c09c63d0ba6edd908e513ca4cab Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 9 Nov 2017 18:03:48 -0500 Subject: [PATCH 006/402] Fix sorting of in add-submodule We were sorting and writing after we updated the index. --- node/lib/util/add_submodule.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/node/lib/util/add_submodule.js b/node/lib/util/add_submodule.js index ac9e47a02..dffe69530 100644 --- a/node/lib/util/add_submodule.js +++ b/node/lib/util/add_submodule.js @@ -65,20 +65,15 @@ exports.addSubmodule = co.wrap(function *(repo, url, filename, importArg) { } const modulesPath = path.join(repo.workdir(), SubmoduleConfigUtil.modulesFileName); - fs.appendFileSync(modulesPath, `\ -[submodule "${filename}"] - path = ${filename} - url = ${url} -`); const index = yield repo.index(); - yield index.addByPath(SubmoduleConfigUtil.modulesFileName); - yield index.write(); - - // sort all the urls since new modules get added const urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + urls[filename] = url; const newConf = SubmoduleConfigUtil.writeConfigText(urls); yield fs.writeFile(modulesPath, newConf); + yield index.addByPath(SubmoduleConfigUtil.modulesFileName); + yield index.write(); + const metaUrl = yield GitUtil.getOriginUrl(repo); const templatePath = yield SubmoduleConfigUtil.getTemplatePath(repo); const subRepo = yield SubmoduleConfigUtil.initSubmoduleAndRepo( From 5bbab20e90bf365189b6637d54a65f0dbcad92cc Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 10 Nov 2017 09:08:43 -0500 Subject: [PATCH 007/402] Pass through 'log' to git --- node/lib/cmd/forward.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node/lib/cmd/forward.js b/node/lib/cmd/forward.js index 7c2e9fdf9..5d9b90bab 100644 --- a/node/lib/cmd/forward.js +++ b/node/lib/cmd/forward.js @@ -74,6 +74,7 @@ ${helpText} See 'git ${name} --help' for more information.`, exports.forwardedCommands = new Set([ "branch", "fetch", + "log", "remote", "tag", ]); From b8bbee8a1a412a978e7343e54cd71042bd8d3fbe Mon Sep 17 00:00:00 2001 From: Shihui Xiang Date: Fri, 10 Nov 2017 11:10:43 -0500 Subject: [PATCH 008/402] Fix the add-submodule info message --- node/lib/cmd/add_submodule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/add_submodule.js b/node/lib/cmd/add_submodule.js index e9c97e117..50e0028a4 100644 --- a/node/lib/cmd/add_submodule.js +++ b/node/lib/cmd/add_submodule.js @@ -121,7 +121,7 @@ The path ${colors.red(args.path)} already exists.`); if (null === importArg) { console.log(`\ Added new sub-repo ${colors.blue(args.path)}. It is currently empty. Please -stage changes and/or make a commit before finishing with 'git meta commit'; +stage changes under sub-repo before finishing with 'git meta commit'; you will not be able to use 'git meta commit' until you do so.`); } }); From 4013870d0aec298df1d16235f021f494deb6abdf Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sat, 11 Nov 2017 12:52:39 -0500 Subject: [PATCH 009/402] Rewrote merge It had many problems, all of which should be fixed now: 1. Didn't support editors 2. Opened all submodules referenced on either side even if not necessary 3. Did not close opened submodules even if no commit was generated 4. Yielded on all entries, causing potential memory problems 5. Had convoluted and completely unnecessary/wrong logic mapping requests for FFWD/NOFFWD into the submodules. 6. Didn't handle conflicts in the .gitmodules file the way rebase did. 7. no --continue or --abort --- node/lib/cmd/merge.js | 71 ++- node/lib/util/commit.js | 35 +- node/lib/util/merge.js | 349 ++----------- node/lib/util/merge_file_util.js | 111 +++++ node/lib/util/merge_util.js | 623 ++++++++++++++++++++++++ node/lib/util/print_status_util.js | 21 +- node/lib/util/read_repo_ast_util.js | 9 + node/lib/util/rebase_util.js | 59 +-- node/lib/util/repo_ast.js | 24 +- node/lib/util/repo_ast_util.js | 34 ++ node/lib/util/repo_status.js | 21 +- node/lib/util/shorthand_parser_util.js | 42 +- node/lib/util/status_util.js | 29 ++ node/lib/util/submodule_config_util.js | 1 + node/lib/util/submodule_util.js | 50 ++ node/lib/util/test_util.js | 2 +- node/lib/util/write_repo_ast_util.js | 14 + node/test/util/merge.js | 202 +------- node/test/util/merge_file_util.js | 86 ++++ node/test/util/merge_util.js | 446 +++++++++++++++++ node/test/util/print_status_util.js | 19 +- node/test/util/read_repo_ast_util.js | 54 +- node/test/util/repo_ast.js | 49 +- node/test/util/repo_ast_util.js | 112 +++++ node/test/util/repo_status.js | 8 + node/test/util/shorthand_parser_util.js | 74 +++ node/test/util/status_util.js | 11 + node/test/util/submodule_util.js | 63 +++ node/test/util/write_repo_ast_util.js | 1 + 29 files changed, 2017 insertions(+), 603 deletions(-) create mode 100644 node/lib/util/merge_file_util.js create mode 100644 node/lib/util/merge_util.js create mode 100644 node/test/util/merge_file_util.js create mode 100644 node/test/util/merge_util.js diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index d17779b9d..a79d20321 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -58,7 +58,7 @@ exports.configureParser = function (parser) { parser.addArgument(["-m", "--message"], { type: "string", help: "commit message", - required: true, + required: false, }); parser.addArgument(["--ff"], { help: "allow fast-forward merges; this is the default", @@ -77,7 +77,19 @@ exports.configureParser = function (parser) { }); parser.addArgument(["commit"], { type: "string", - help: "the commitish to merge" + help: "the commitish to merge", + defaultValue: null, + nargs: "?", + }); + parser.addArgument(["--continue"], { + action: "storeConst", + constant: true, + help: "continue an in-progress merge", + }); + parser.addArgument(["--abort"], { + action: "storeConst", + constant: true, + help: "abort an in-progress merge", }); }; @@ -95,41 +107,60 @@ exports.executeableSubcommand = co.wrap(function *(args) { const colors = require("colors"); - const Merge = require("../util/merge"); + const MergeUtil = require("../util/merge_util"); const GitUtil = require("../util/git_util"); - const StatusUtil = require("../util/status_util"); const UserError = require("../util/user_error"); - const MODE = Merge.MODE; + const MODE = MergeUtil.MODE; let mode = MODE.NORMAL; - if (args.ff) { - if (args.ff_only) { - throw new UserError("--ff and --ff-only cannot be used together."); - } - if (args.no_ff) { - throw new UserError("--ff and --no-ff cannot be used together."); - } + if (args.ff + args.continue + args.abort + args.no_ff + args.ff_only > 1) { + throw new UserError( + "Cannot use ff, no-ff, ff-only, abort, or continue together."); } - else if (args.ff_only) { - if (args.no_ff) { - throw new UserError( - "--no-ff and --ff-only cannot be used together."); - } + + if (args.ff_only) { mode = MODE.FF_ONLY; } else if (args.no_ff) { mode = MODE.FORCE_COMMIT; } - const repo = yield GitUtil.getCurrentRepo(); - const status = yield StatusUtil.getRepoStatus(repo); + if (args.continue) { + if (null !== args.commit) { + throw new UserError("Cannot specify a commit with --continue."); + } + yield MergeUtil.continue(repo); + return; // RETURN + } + if (args.abort) { + if (null !== args.commit) { + throw new UserError("Cannot specify a commit with --abort."); + } + yield MergeUtil.abort(repo); + return; // RETURN + } + if (null === args.commit) { + throw new UserError("Commit required."); + } const commitish = yield GitUtil.resolveCommitish(repo, args.commit); if (null === commitish) { throw new UserError(`\ Could not resolve ${colors.red(args.commit)} to a commit.`); } + const editMessage = function () { + const message = `\ +Merge of '${args.commit}' + +# please enter a commit message to explain why this merge is necessary, +# especially if it merges an updated upstream into a topic branch. +# +# lines starting with '#' will be ignored, and an empty message aborts +# the commit. +`; + return GitUtil.editMessage(repo, message); + }; const commit = yield repo.getCommit(commitish.id()); - yield Merge.merge(repo, status, commit, mode, args.message); + yield MergeUtil.merge(repo, commit, mode, args.message, editMessage); }); diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index b352d9d64..df849c9f1 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -61,9 +61,10 @@ const UserError = require("./user_error"); * * @param {String} message */ -function ensureEolOnLastLine(message) { +exports.ensureEolOnLastLine = function (message) { + // TODO: test independently return message.endsWith("\n") ? message : (message + "\n"); -} +}; /** * Return the `NodeGit.Tree` object for the (left) parent of the head commit @@ -229,10 +230,11 @@ const commitRepo = co.wrap(function *(repo, yield index.write(); } if (doCommit) { - return yield repo.createCommitOnHead([], - signature, - signature, - ensureEolOnLastLine(message)); + return yield repo.createCommitOnHead( + [], + signature, + signature, + exports.ensureEolOnLastLine(message)); } return null; }); @@ -611,15 +613,16 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message) { const sig = repo.defaultSignature(); const parents = [headCommit]; - const commitId = yield NodeGit.Commit.create(repo, - 0, - sig, - sig, - 0, - ensureEolOnLastLine(message), - tree, - parents.length, - parents); + const commitId = yield NodeGit.Commit.create( + repo, + 0, + sig, + sig, + 0, + exports.ensureEolOnLastLine(message), + tree, + parents.length, + parents); // Now we need to put the commit on head. We need to unstage the changes // we've just committed, otherwise we see conflicts with the workdir. We @@ -998,7 +1001,7 @@ exports.amendRepo = co.wrap(function *(repo, message) { const index = yield repo.index(); const treeId = yield index.writeTree(); const tree = yield NodeGit.Tree.lookup(repo, treeId); - const termedMessage = ensureEolOnLastLine(message); + const termedMessage = exports.ensureEolOnLastLine(message); const id = yield head.amend("HEAD", null, null, null, termedMessage, tree); return id.tostrS(); }); diff --git a/node/lib/util/merge.js b/node/lib/util/merge.js index d83197f34..9390c18d4 100644 --- a/node/lib/util/merge.js +++ b/node/lib/util/merge.js @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, Two Sigma Open Source + * Copyright (c) 2017, Two Sigma Open Source * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,327 +31,56 @@ "use strict"; const assert = require("chai").assert; -const co = require("co"); -const colors = require("colors"); -const NodeGit = require("nodegit"); - -const GitUtil = require("./git_util"); -const Open = require("./open"); -const RepoStatus = require("./repo_status"); -const SubmoduleConfigUtil = require("./submodule_config_util"); -const SubmoduleUtil = require("./submodule_util"); -const UserError = require("./user_error"); /** - * @enum {MODE} - * Flags to describe what type of merge to do. + * This module defines the `Merge` value-semantic type. */ -const MODE = { - NORMAL : 0, // will do a fast-forward merge when possible - FF_ONLY : 1, // will fail unless fast-forward merge is possible - FORCE_COMMIT: 2, // will generate merge commit even could fast-forward -}; - -exports.MODE = MODE; /** - * Merge the specified `commit` in the specified `metaRepo` having the - * specified `metaRepoStatus`, using the specified `mode` to control whether or - * not a merge commit will be generated. The specified `commitMessage` will be - * recorded as the message for merge commits. Throw a `UserError` exception if - * a fast-forward merge is requested and cannot be completed. - * - * Note that this method will open closed submodules having changes recorded in - * `commit` compared to HEAD. + * @class Merge * - * @async - * @param {NodeGit.Repository} metaRepo - * @param {RepoStatus} metaRepoStatus - * @param {NodeGit.Commit} commit - * @param {MODE} mode - * @param {String} commitMessage - * @return {Object|null} - * @return {String} return.metaCommit - * @return {Object} return.submoduleCommits map from submodule to commit + * This class represents the state of a merge. */ -exports.merge = co.wrap(function *(metaRepo, - metaRepoStatus, - commit, - mode, - commitMessage) { - assert.instanceOf(metaRepo, NodeGit.Repository); - assert.instanceOf(metaRepoStatus, RepoStatus); - assert.isNumber(mode); - assert.instanceOf(commit, NodeGit.Commit); - assert.isString(commitMessage); - - // TODO: See how we do with a variety of edge cases, e.g.: submodules added - // and removed. - // TODO: Deal with conflicts. - - // Basic algorithm: - // - start merge on meta-repo - // - detect changes in sub-repos - // - merge changes in sub-repos - // - if any conflicts in sub-repos, bail - // - finalize commit in meta-repo - // - // The actual problem is complicated by a couple of things: - // - // - oddities with and/or poor support of submodules - // - unlike rebase and cherry-pick, which seem similar on the surface, the - // merge operation doesn't operate directly on the current HEAD, index, - // or working directory: it creates a weird virtual index - // - // I haven't created issues for nodegit or libgit2 yet as I'm not sure how - // many of these problems are real problems or "by design". If this - // project moves out of the prototype phase, we should resolve these - // issues as much of the code below feels like a hackish workaround. - // - // details to follow: - - // If the target commit is an ancestor of the derived commit, then we have - // nothing to do; the target commit is already part of the current history. - - const commitSha = commit.id().tostrS(); - - if (yield GitUtil.isUpToDate(metaRepo, - metaRepoStatus.headCommit, - commitSha)) { - return null; +class Merge { + + /** + * Create a new `Merge` object. + * + * @param {String} message + * @param {String} originalHead + * @param {String} mergeHead + */ + constructor(message, originalHead, mergeHead) { + assert.isString(message); + assert.isString(originalHead); + assert.isString(mergeHead); + + this.d_message = message; + this.d_originalHead = originalHead; + this.d_mergeHead = mergeHead; + Object.freeze(this); } - let canFF = yield NodeGit.Graph.descendantOf(metaRepo, - commitSha, - metaRepoStatus.headCommit); - - if (MODE.FF_ONLY === mode && !canFF) { - throw new UserError(`The meta-repositor cannot be fast-forwarded to \ -${colors.red(commitSha)}.`); + /** + * @property {String} message commit message started with the merge + */ + get message() { + return this.d_message; } - const sig = metaRepo.defaultSignature(); - - // Kick off the merge. It is important to note is that `Merge.commit` does - // not directly modify the working directory or index. The `metaIndex` - // object it returns is magical, virtual, does not operate on HEAD or - // anything, has no effect. - - const head = yield metaRepo.getCommit(metaRepoStatus.headCommit); - - const metaIndex = yield SubmoduleUtil.cacheSubmodules(metaRepo, () => { - return NodeGit.Merge.commits(metaRepo, - head, - commit, - null); - }); - - let errorMessage = ""; - - // `toAdd` will contain a list of paths that need to be added to the final - // index when it's ready. Adding them to the "virtual", `metaIndex` object - // turns out to have no effect. This complication is caused by a a - // combination of merge/index weirdness and submodule weirdness. - - const toAdd = []; - - const subCommits = {}; // Record of merge commits in submodules. - - const subUrls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(metaRepo, - head); - const headTree = yield head.getTree(); - //const subs = metaRepoStatus.submodules; - - const opener = new Open.Opener(metaRepo, null); - const subFetcher = yield opener.fetcher(); - - const mergeEntry = co.wrap(function *(entry) { - const path = entry.path; - const stage = RepoStatus.getStage(entry.flags); - - // If the entry is not on the "other" side of the merge move on. - - if (RepoStatus.STAGE.THEIRS !== stage && - RepoStatus.STAGE.NORMAL !== stage) { - return; // RETURN - } - - // If it's not a submodule move on. - - if (!(path in subUrls)) { - return; // RETURN - } - - // Otherwise, we have a submodule that needs to be merged. - - const subSha = entry.id.tostrS(); - const subCommitId = NodeGit.Oid.fromString(subSha); - const subEntry = yield headTree.entryByPath(path); - const subHeadSha = subEntry.sha(); - const subCommitSha = subCommitId.tostrS(); - - // Exit early without opening if we have the same commit as the one - // we're supposed to merge to. - - if (subCommitSha === subHeadSha) { - return; // RETURN - } - - const subRepo = yield opener.getSubrepo(path); - - // Fetch commit to merge. - - yield subFetcher.fetchSha(subRepo, path, subSha); - - const subCommit = yield subRepo.getCommit(subCommitId); - - // If this submodule is up-to-date with the merge commit, exit. - - if (yield GitUtil.isUpToDate(subRepo, subHeadSha, subCommitSha)) { - console.log(`Submodule ${colors.blue(path)} is up-to-date with \ -commit ${colors.green(subCommitSha)}.`); - return; // RETURN - } - - // If we can fast-forward, we don't need to do a merge. - - const canSubFF = yield NodeGit.Graph.descendantOf(subRepo, - subCommitSha, - subHeadSha); - if (canSubFF && MODE.FORCE_COMMIT !== mode) { - console.log(`Submodule ${colors.blue(path)}: fast-forward to -${colors.green(subCommitSha)}.`); - yield NodeGit.Reset.reset(subRepo, - subCommit, - NodeGit.Reset.TYPE.HARD); - - // We still need to add this submodule's name to the list to add so - // that it will be recorded to the index if the meta-repo ends up - // generating a commit. - - toAdd.push(path); - return; // RETURN - } - else if (MODE.FF_ONLY === mode) { - // If non-ff merge is disallowed, bail. - errorMessage += `Submodule ${colors.red(path)} could not be \ -fast-forwarded.\n`; - return; // RETURN - } - - // We're going to generate a commit. Note that the meta-repo cannot be - // fast-forwarded. - - canFF = false; - - console.log(`Submodule ${colors.blue(path)}: merging commit \ -${colors.green(subCommitSha)}.\n`); - - // Start the merge. - - const subHead = yield subRepo.getCommit(subHeadSha); - let index = yield NodeGit.Merge.commits(subRepo, - subHead, - subCommit, - null); - - // Abort if conflicted. - - if (index.hasConflicts()) { - errorMessage += `Submodule ${colors.red(path)} is conflicted.\n`; - return; // RETURN - } - - // Otherwise, finish off the merge. - - yield index.writeTreeTo(subRepo); - yield NodeGit.Checkout.index(subRepo, index, { - checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, - }); - index = yield subRepo.index(); - const treeId = yield index.writeTreeTo(subRepo); - const mergeCommit = yield subRepo.createCommit("HEAD", - sig, - sig, - commitMessage, - treeId, - [subHead, subCommit]); - subCommits[path] = mergeCommit.tostrS(); - - // And add this sub-repo to the list of sub-repos that need to be added - // to the index later. - - toAdd.push(path); - }); - - // Createa a submodule merger for each submodule in the index. - - const entries = metaIndex.entries(); - yield entries.map(mergeEntry); - - // If one of the submodules could not be merged, exit. - - if ("" !== errorMessage) { - throw new UserError(errorMessage); + /** + * @property {String} originalHead HEAD commit when merge started + */ + get originalHead() { + return this.d_originalHead; } - // If we've made it through the submodules and can still fast-forward, just - // reset the head to the right commit and return. - - if (canFF && MODE.FORCE_COMMIT !== mode) { - console.log( - `Fast-forwarding meta-repo to ${colors.green(commitSha)}.`); - yield NodeGit.Reset.reset(metaRepo, commit, NodeGit.Reset.TYPE.HARD); - return { - metaCommit: commitSha, - submoduleCommits: subCommits, - }; + /** + * @property {String} mergeHead target commit of merge + */ + get mergeHead() { + return this.d_mergeHead; } +} - console.log(`Merging meta-repo commit ${colors.green(commitSha)}.`); - - // This bit gets a little nasty. First, we need to put `metaIndex` into a - // proper state and write it out. - - yield metaIndex.conflictCleanup(); - yield metaIndex.writeTreeTo(metaRepo); - - // Having committed the index with changes, we need to check it out so that - // it's applied to the current index and working directory. Only there - // will we be able to properly reflect the changes to the submodules. We - // need to get to a point where we have a "real" index to work with. - - const checkoutOpts = { - checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE - }; - yield NodeGit.Checkout.index(metaRepo, metaIndex, checkoutOpts); - - // Now that the changes are applied to the current working directory and - // index, we can open the current index and work with it. - - const newIndex = yield metaRepo.index(); - - // We've made changes to (merges into) some of the submodules; now we can - // finally stage them into the index. - - yield toAdd.map(subName => newIndex.addByPath(subName)); - - // And write that index out. - - yield newIndex.write(); - const id = yield newIndex.writeTreeTo(metaRepo); - - // And finally, commit it. - - const metaCommit = yield metaRepo.createCommit("HEAD", - sig, - sig, - commitMessage, - id, - [head, commit]); - - return { - metaCommit: metaCommit.tostrS(), - submoduleCommits: subCommits, - }; -}); +module.exports = Merge; diff --git a/node/lib/util/merge_file_util.js b/node/lib/util/merge_file_util.js new file mode 100644 index 000000000..ac2d9509f --- /dev/null +++ b/node/lib/util/merge_file_util.js @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2017, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const fs = require("fs-promise"); +const path = require("path"); +const rimraf = require("rimraf"); + +const Merge = require("./merge"); + +/** + * This module contains methods for accessing files that pertain to in-progress + * merges. + */ + +const metaMergeDir = "META_MERGE"; +const messageFile = "MSG"; +const originalHeadFile = "ORIG_HEAD"; +const mergeHeadFile = "MERGE_HEAD"; + +/** + * Return the `Merge` object in the specified `gitDir`, if one exists, or null + * if there is no merge in progress. + * + * @param {String} gitDir + * @return {String|null} + */ +exports.readMerge = co.wrap(function *(gitDir) { + assert.isString(gitDir); + let message; + let originalHead; + let mergeHead; + const root = path.join(gitDir, metaMergeDir); + try { + message = yield fs.readFile(path.join(root, messageFile), + "utf8"); + originalHead = yield fs.readFile(path.join(root, originalHeadFile), + "utf8"); + mergeHead = yield fs.readFile(path.join(root, mergeHeadFile), "utf8"); + } + catch (e) { + return null; + } + return new Merge(message, + originalHead.split("\n")[0], + mergeHead.split("\n")[0]); +}); + +/** + * Write the specified `merge` to the specified `gitDir`. The behavior is + * undefined if there is already a merge recorded in `gitDir`. + * + * @param {String} gitDir + * @param {Merge} merge + */ +exports.writeMerge = co.wrap(function *(gitDir, merge) { + assert.isString(gitDir); + assert.instanceOf(merge, Merge); + + const root = path.join(gitDir, metaMergeDir); + yield fs.mkdir(root); + yield fs.writeFile(path.join(root, "MSG"), merge.message); + yield fs.writeFile(path.join(root, "ORIG_HEAD"), + merge.originalHead + "\n"); + yield fs.writeFile(path.join(root, "MERGE_HEAD"), + merge.mergeHead + "\n"); +}); + +/** + * Remove files related to a meta merge. + * + * @param {String} gitDir + */ +exports.cleanMerge = co.wrap(function *(gitDir) { + assert.isString(gitDir); + const root = path.join(gitDir, metaMergeDir); + const promise = new Promise(callback => { + return rimraf(root, {}, callback); + }); + yield promise; +}); diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js new file mode 100644 index 000000000..51641a08c --- /dev/null +++ b/node/lib/util/merge_util.js @@ -0,0 +1,623 @@ +/* + * Copyright (c) 2017, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const ChildProcess = require("child-process-promise"); +const co = require("co"); +const colors = require("colors"); +const NodeGit = require("nodegit"); + +const Checkout = require("./checkout"); +const Commit = require("./commit"); +const DeinitUtil = require("./deinit_util"); +const DoWorkQueue = require("./do_work_queue"); +const GitUtil = require("./git_util"); +const Merge = require("./merge"); +const MergeFileUtil = require("./merge_file_util"); +const Open = require("./open"); +const RepoStatus = require("./repo_status"); +const StatusUtil = require("./status_util"); +const SubmoduleConfigUtil = require("./submodule_config_util"); +const SubmoduleUtil = require("./submodule_util"); +const UserError = require("./user_error"); + +/** + * @enum {MODE} + * Flags to describe what type of merge to do. + */ +const MODE = { + NORMAL : 0, // will do a fast-forward merge when possible + FF_ONLY : 1, // will fail unless fast-forward merge is possible + FORCE_COMMIT: 2, // will generate merge commit even could fast-forward +}; + +exports.MODE = MODE; + +/** + * Perform a fast-forward merge in the specified `repo` to the specified + * `commit`. If `MODE.FORCE_COMMIT === mode`, generate a merge commit. When + * generating a merge commit, use the optionally specified `message`. The + * behavior is undefined unless `commit` is different from but descendant of + * the HEAD commit in `repo`. If a commit is generated, return its sha; + * otherwise, return null. + * + * @param {NodeGit.Repository} repo + * @param {MODE} mode + * @param {NodeGit.Commit} commit + * @param {String|null} message + * @return {String|null} + */ +exports.fastForwardMerge = co.wrap(function *(repo, mode, commit, message) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isNumber(mode); + assert.instanceOf(commit, NodeGit.Commit); + assert.isString(message); + + // Remember the current branch; the checkoutCommit function will move it. + + const branch = yield repo.getCurrentBranch(); + let result = null; + let newHead; + if (MODE.FORCE_COMMIT !== mode) { + // If we're not generating a commit, we just need to checkout the one + // we're fast-forwarding to. + + yield Checkout.checkoutCommit(repo, commit, false); + newHead = commit; + } + else { + // Checkout the commit we're fast-forwarding to. + + const head = yield repo.getHeadCommit(); + yield Checkout.checkoutCommit(repo, commit, false); + + // Then, generate a new commit that has the previous HEAD and commit to + // merge as children. + + const sig = repo.defaultSignature(); + const tree = yield commit.getTree(); + const id = yield NodeGit.Commit.create( + repo, + 0, + sig, + sig, + null, + Commit.ensureEolOnLastLine(message), + tree, + 2, + [head, commit]); + newHead = yield repo.getCommit(id); + result = newHead.id().tostrS(); + + // Move HEAD to point to the new commit. + + yield NodeGit.Reset.reset(repo, + newHead, + NodeGit.Reset.TYPE.HARD, + null, + branch.name()); + } + + // If we were on a branch, make it current again. + + if (branch.isBranch()) { + yield branch.setTarget(newHead, "ffwd merge"); + yield repo.setHead(branch.name()); + } + return result; +}); + +/** + * Merge the specified `commit` in the specified `repo` having the specified + * `status`, using the specified `mode` to control whether or not a merge + * commit will be generated. Return `null` if the repository is up-to-date, or + * an object describing generated commits otherwise. If the optionally + * specified `commitMessage` is provided, use it as the commit message for any + * generated merge commit; otherwise, use the specified `editMessage` promise + * to request a message. Throw a `UserError` exception if a fast-forward merge + * is requested and cannot be completed. Throw a `UserError` if there are + * conflicts, or if local modifications prevent the merge from happening. + * + * @async + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + * @param {MODE} mode + * @param {String|null} commitMessage + * @param {() -> Promise(String)} editMessage + * @return {Object|null} + * @return {String} return.metaCommit + * @return {Object} return.submoduleCommits map from submodule to commit + */ +exports.merge = co.wrap(function *(repo, + commit, + mode, + commitMessage, + editMessage) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isNumber(mode); + assert.instanceOf(commit, NodeGit.Commit); + if (null !== commitMessage) { + assert.isString(commitMessage); + } + assert.isFunction(editMessage); + + const status = yield StatusUtil.getRepoStatus(repo, { + showMetaChanges: true, + }); + + // Cannot merge if there is an existing merge + + if (null !== status.merge) { + throw new UserError(`\ +There is an existing merge in progress. Run 'git meta merge --continue' +to complete it, or 'git meta merge --abort' to abandon it. +`); + } + + // Cannot merge if any staged changes. + + if (!status.isIndexDeepClean()) { + throw new UserError("Cannot merge due to staged changes."); + } + + const result = { + metaCommit: null, + submoduleCommits: {}, + }; + + const commitSha = commit.id().tostrS(); + + const head = yield repo.getHeadCommit(); + + if (head.id().tostrS() === commit.id().tostrS()) { + console.log("Nothing to do."); + return null; // RETURN + } + + const canFF = yield NodeGit.Graph.descendantOf(repo, + commitSha, + head.id().tostrS()); + + let message = ""; + + if (!canFF || MODE.FORCE_COMMIT === mode) { + if (null === commitMessage) { + const raw = yield editMessage(); + message = GitUtil.stripMessage(raw); + if ("" === message) { + console.log("Empty commit message."); + return result; + } + } + else { + message = commitMessage; + } + } + + if (MODE.FF_ONLY === mode && !canFF) { + throw new UserError(`The meta-repository cannot be fast-forwarded to \ +${colors.red(commitSha)}.`); + } + else if (canFF) { + console.log(`Fast-forwarding meta-repo to ${colors.green(commitSha)}.`); + + + const sha = yield exports.fastForwardMerge(repo, + mode, + commit, + message); + result.metaCommit = sha; + return result; + } + + + const sig = repo.defaultSignature(); + + // Kick off the merge. It is important to note is that `Merge.commit` does + // not directly modify the working directory or index. The `index` + // object it returns is magical, virtual, does not operate on HEAD or + // anything, has no effect. + + const mergeIndex = yield SubmoduleUtil.cacheSubmodules(repo, () => { + return NodeGit.Merge.commits(repo, head, commit, null); + }); + + const checkoutOpts = { + checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE + }; + yield SubmoduleUtil.cacheSubmodules(repo, () => { + return NodeGit.Checkout.index(repo, mergeIndex, checkoutOpts); + }); + const index = yield repo.index(); + + let errorMessage = ""; + + const subCommits = result.submoduleCommits; + const subUrls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, + head); + const headTree = yield head.getTree(); + const opener = new Open.Opener(repo, null); + const subFetcher = yield opener.fetcher(); + + let hasModulesFile = false; + + const mergeEntry = co.wrap(function *(entry) { + const path = entry.path; + const stage = RepoStatus.getStage(entry.flags); + + if (path === SubmoduleConfigUtil.modulesFileName) { + hasModulesFile = true; + return; // RETURN + } + + else if (RepoStatus.STAGE.THEIRS === stage && !(path in subUrls)) { + errorMessage += `Conflict in ${colors.red(path)}`; + return; // RETURN + } + + // We don't need to do anything with an entry unless it is a conflicted + // submodule. + + if (RepoStatus.STAGE.THEIRS !== stage || !(path in subUrls)) { + return; // RETURN + } + + // Otherwise, we have a submodule that needs to be merged. + + const subSha = entry.id.tostrS(); + const subCommitId = NodeGit.Oid.fromString(subSha); + const subEntry = yield headTree.entryByPath(path); + const subHeadSha = subEntry.sha(); + const subCommitSha = subCommitId.tostrS(); + const subRepo = yield opener.getSubrepo(path); + + // Fetch commit to merge. + + yield subFetcher.fetchSha(subRepo, path, subSha); + + const subCommit = yield subRepo.getCommit(subCommitId); + + const upToDate = yield NodeGit.Graph.descendantOf(subRepo, + subHeadSha, + subCommitSha); + if (upToDate) { + yield index.addByPath(path); + return; // RETURN + } + + // If we can fast-forward, we don't need to do a merge. + + const canSubFF = yield NodeGit.Graph.descendantOf(subRepo, + subCommitSha, + subHeadSha); + if (canSubFF) { + console.log(`Submodule ${colors.blue(path)}: fast-forward to \ +${colors.green(subCommitSha)}.`); + yield NodeGit.Reset.reset(subRepo, + subCommit, + NodeGit.Reset.TYPE.HARD); + yield index.addByPath(path); + return; // RETURN + } + + // If we already have the commit being merged in, we don't need to do + // anything. + + console.log(`Submodule ${colors.blue(path)}: merging commit \ +${colors.green(subCommitSha)}.`); + + // Start the merge. + + const subHead = yield subRepo.getCommit(subHeadSha); + let subIndex = yield NodeGit.Merge.commits(subRepo, + subHead, + subCommit, + null); + + yield NodeGit.Checkout.index(subRepo, subIndex, { + checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, + }); + + // Abort if conflicted. + + if (subIndex.hasConflicts()) { + const merge = new Merge(message, + subHead.id().tostrS(), + subCommit.id().tostrS()); + yield MergeFileUtil.writeMerge(subRepo.path(), merge); + + errorMessage += `Submodule ${colors.red(path)} is conflicted:\n`; + const entries = subIndex.entries(); + for (let i = 0; i < entries.length; ++i) { + const subEntry = entries[i]; + const subStage = RepoStatus.getStage(subEntry.flags); + if (RepoStatus.STAGE.OURS === subStage) { + errorMessage += `\t${colors.red(subEntry.path)}\n`; + } + } + return; // RETURN + } + + // Otherwise, finish off the merge. + + subIndex = yield subRepo.index(); + const treeId = yield subIndex.writeTreeTo(subRepo); + const mergeCommit = yield subRepo.createCommit("HEAD", + sig, + sig, + message, + treeId, + [subHead, subCommit]); + subCommits[path] = mergeCommit.tostrS(); + + // And add this sub-repo to the list of sub-repos that need to be added + // to the index later. + + yield index.addByPath(path); + }); + + const entries = index.entries(); + yield DoWorkQueue.doInParallel(entries, mergeEntry, 30); + + if (hasModulesFile) { + const good = yield SubmoduleUtil.mergeModulesFile(repo, head, commit); + if (!good) { + errorMessage += `Conflicting submodule additions/removals.`; + } + else { + yield index.addByPath(SubmoduleConfigUtil.modulesFileName); + } + } + + // We must write the index here or the staging we've done erlier will go + // away. + yield index.write(); + + if ("" !== errorMessage) { + // We're about to fail due to conflict. First, record that there is a + // merge in progress so that we can continue or abort it later. + + const merge = new Merge(message, + head.id().tostrS(), + commit.id().tostrS()); + yield MergeFileUtil.writeMerge(repo.path(), merge); + throw new UserError(errorMessage); + } + + // If we've made it here, it means there are no "real" conflicts, but we + // need to clean up the index anyway or it won't let us write a tree. + + yield index.conflictCleanup(); + + console.log(`Merging meta-repo commit ${colors.green(commitSha)}.`); + + const id = yield index.writeTreeTo(repo); + + // And finally, commit it. + + const metaCommit = yield repo.createCommit("HEAD", + sig, + sig, + message, + id, + [head, commit]); + + result.metaCommit = metaCommit.tostrS(); + + // Close subs that were opened if no commits were generated to them. + + const closeSub = co.wrap(function *(path) { + if (!(path in subCommits)) { + console.log(`Closing ${colors.green(path)} -- no commit created.`); + yield DeinitUtil.deinit(repo, path); + } + }); + const opened = Array.from(yield opener.getOpenedSubs()); + DoWorkQueue.doInParallel(opened, closeSub, 30); + return result; +}); + +/** + * Throw a `UserError` if the specified `index` has non-submodule conflicts and + * do nothing otherwise. + * + * @param {NodeGit.Index} index + */ +const checkForConflicts = function (index) { + assert.instanceOf(index, NodeGit.Index); + const entries = index.entries(); + for (let i = 0; i < entries.length; ++i) { + const entry = entries[i]; + const stage = RepoStatus.getStage(entry.flags); + if (RepoStatus.STAGE.OURS === stage && + NodeGit.TreeEntry.FILEMODE.COMMIT !== entry.mode) { + throw new UserError("Meta-repo has conflicts."); + } + } +}; + +/** + * Continue the merge in the specified `repo`. Throw a `UserError` if there is + * no merge in progress in `repo` or if `repo` still has outstanding conflicts. + * Return an object describing generated commits. + * + * @param {NodeGit.Repository} repo + * @return {Object|null} + * @return {String} return.metaCommit + * @return {Object} return.submoduleCommits map from submodule to commit + */ +exports.continue = co.wrap(function *(repo) { + assert.instanceOf(repo, NodeGit.Repository); + + const result = { + metaCommit: null, + submoduleCommits: {}, + }; + const merge = yield MergeFileUtil.readMerge(repo.path()); + if (null === merge) { + throw new UserError("No merge in progress."); + } + + const index = yield repo.index(); + + checkForConflicts(index); + + // We have to do this because there may have been outsanding submodule + // conflicts. We validated in `checkForConflicts` that there are no "real" + // conflicts. + + console.log(`Continuing with merge of ${colors.green(merge.mergeHead)}.`); + + let errorMessage = ""; + + const continueSub = co.wrap(function *(subPath) { + const subRepo = yield SubmoduleUtil.getRepo(repo, subPath); + const subIndex = yield subRepo.index(); + if (subIndex.hasConflicts()) { + errorMessage += + `Submodule ${colors.red(subPath)} has conflicts.\n`; + return; // RETURN + } + const sig = subRepo.defaultSignature(); + const subMerge = yield MergeFileUtil.readMerge(subRepo.path()); + if (null === subMerge) { + // There is no merge in this submodule, but if there are staged + // changes we need to make a commit. + + const status = yield StatusUtil.getRepoStatus(subRepo, { + showMeta: true, + }); + if (0 !== Object.keys(status.staged)) { + const id = yield subRepo.createCommitOnHead([], + sig, + sig, + merge.message); + result.submoduleCommits[subPath] = id.tostrS(); + } + } + else { + // Now, we have a submodule that was in the middle of merging. + // Continue it and then clean up the merge. + + const head = yield subRepo.getHeadCommit(); + const mergeHead = yield subRepo.getCommit(subMerge.mergeHead); + const treeId = yield subIndex.writeTreeTo(subRepo); + const id = yield subRepo.createCommit("HEAD", + sig, + sig, + subMerge.message, + treeId, + [head, mergeHead]); + yield MergeFileUtil.cleanMerge(subRepo.path()); + result.submoduleCommits[subPath] = id.tostrS(); + } + yield index.addByPath(subPath); + }); + const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo); + yield DoWorkQueue.doInParallel(openSubs, continueSub, 30); + + if ("" !== errorMessage) { + throw new UserError(errorMessage); + } + yield index.conflictCleanup(); + yield index.write(); + const treeId = yield index.writeTreeTo(repo); + + const sig = repo.defaultSignature(); + const head = yield repo.getHeadCommit(); + const mergeHead = yield repo.getCommit(merge.mergeHead); + const metaCommit = yield repo.createCommit("HEAD", + sig, + sig, + merge.message, + treeId, + [head, mergeHead]); + console.log( + `Finished with merge commit ${colors.green(metaCommit.tostrS())}`); + yield MergeFileUtil.cleanMerge(repo.path()); + result.metaCommit = metaCommit.tostrS(); + return result; +}); + +const resetMerge = co.wrap(function *(repo) { + // TODO: add this to libgit2 + const execString = `git -C '${repo.workdir()}' reset --merge`; + yield ChildProcess.exec(execString); +}); + +/** + * Abort the merge in progress in the specified `repo`, or throw a `UserError` + * if no merge is in progress. + * + * @param {NodeGit.Repository} repo + */ +exports.abort = co.wrap(function *(repo) { + assert.instanceOf(repo, NodeGit.Repository); + + const merge = yield MergeFileUtil.readMerge(repo.path()); + if (null === merge) { + throw new UserError("No merge in progress."); + } + + const head = yield repo.getHeadCommit(repo); + const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo); + const shas = yield SubmoduleUtil.getSubmoduleShasForCommit(repo, + openSubs, + head); + const index = yield repo.index(); + const abortSub = co.wrap(function *(subName) { + // Our goal here is to do a 'git reset --merge'. Ideally, we'd do a + // soft reset first to put the submodule on the right sha, but you + // can't do a soft reset "in the middle of a merge", so we do an + // initial 'git reset --merge' once, then if we're not on the right sha + // we can do the soft reset -- the 'git reset --merge' cleans up any + // merge conflicts -- then do one final 'git reset --merge'. + + const subRepo = yield SubmoduleUtil.getRepo(repo, subName); + yield resetMerge(subRepo); + const subHead = yield subRepo.getHeadCommit(); + if (subHead.id().tostrS() !== shas[subName]) { + const commit = yield subRepo.getCommit(shas[subName]); + yield NodeGit.Reset.reset(subRepo, + commit, + NodeGit.Reset.TYPE.SOFT); + yield resetMerge(subRepo); + } + yield MergeFileUtil.cleanMerge(subRepo.path()); + yield index.addByPath(subName); + }); + yield DoWorkQueue.doInParallel(openSubs, abortSub, 30); + yield index.conflictCleanup(); + yield index.write(); + yield resetMerge(repo); + yield MergeFileUtil.cleanMerge(repo.path()); +}); diff --git a/node/lib/util/print_status_util.js b/node/lib/util/print_status_util.js index f9f9c3113..d423369d8 100644 --- a/node/lib/util/print_status_util.js +++ b/node/lib/util/print_status_util.js @@ -41,6 +41,7 @@ const colors = require("colors/safe"); const path = require("path"); const GitUtil = require("./git_util"); +const Merge = require("./merge"); const Rebase = require("./rebase"); const RepoStatus = require("./repo_status"); @@ -351,11 +352,25 @@ exports.printRebase = function (rebase) { return `${colors.red("rebase in progress; onto ", shortSha)} You are currently rebasing branch '${rebase.headName}' on '${shortSha}'. (fix conflicts and then run "git meta rebase --continue") - (use "git meta rebase --skip" to skip this patch) (use "git meta rebase --abort" to check out the original branch) `; }; +/** + * Return a message describing the specified `merge`. + * + * @param {Merge} + * @return {String} + */ +function printMerge(merge) { + assert.instanceOf(merge, Merge); + return `\ +A merge is in progress. + (fix conflicts and run "git meta merge --continue") + (use "git meta merge --abort" to abort the merge) +`; +} + /** * Return a message describing the state of the current branch in the specified * `status`. @@ -391,6 +406,10 @@ exports.printRepoStatus = function (status, cwd) { result += exports.printCurrentBranch(status); + if (null !== status.merge) { + result += printMerge(status.merge); + } + let changes = ""; const fileStatuses = exports.accumulateStatus(status); const staged = fileStatuses.staged; diff --git a/node/lib/util/read_repo_ast_util.js b/node/lib/util/read_repo_ast_util.js index c9c214835..23ce300ab 100644 --- a/node/lib/util/read_repo_ast_util.js +++ b/node/lib/util/read_repo_ast_util.js @@ -44,6 +44,7 @@ const path = require("path"); const RepoAST = require("./repo_ast"); const RebaseFileUtil = require("./rebase_file_util"); +const MergeFileUtil = require("./merge_file_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); /** @@ -503,6 +504,13 @@ exports.readRAST = co.wrap(function *(repo) { yield loadCommit(NodeGit.Oid.fromString(rebase.onto)); } + const merge = yield MergeFileUtil.readMerge(repo.path()); + + if (null !== merge) { + yield loadCommit(NodeGit.Oid.fromString(merge.originalHead)); + yield loadCommit(NodeGit.Oid.fromString(merge.mergeHead)); + } + return new RepoAST({ commits: commits, branches: branchTargets, @@ -515,6 +523,7 @@ exports.readRAST = co.wrap(function *(repo) { workdir: workdir, openSubmodules: openSubmodules, rebase: rebase, + merge: merge, bare: bare, }); }); diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index b58bcad53..6e5f77ad7 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -33,7 +33,6 @@ const assert = require("chai").assert; const co = require("co"); const colors = require("colors"); -const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); const rimraf = require("rimraf"); @@ -190,55 +189,6 @@ rebase; rewinding to ${colors.green(ontoCommitId.tostrS())}.`); return yield processSubmoduleRebase(repo, name, rebase, op); }); -/** - * Attempt to handle a conflicted `.gitmodules` file in the specified `repo` - * having the specified `index`, with changes coming from the specified - * `fromCommit` and `ontoCommit` commits. Return true if the conflict was - * resolved and false otherwise. - * - * @param {NodeGit.Repository} repo - * @param {NodeGit.Index} index - * @param {NodeGit.Commit} fromCommit - * @param {NodeGit.Commit} ontoCommit - * @return {Boolean} - */ -const mergeModulesFile = co.wrap(function *(repo, - index, - fromCommit, - ontoCommit) { - // If there is a conflict in the '.gitmodules' file, attempt to resolve it - // by comparing the current change against the original onto commit and the - // merge base between the base and onto commits. - - const Conf = SubmoduleConfigUtil; - const getSubs = Conf.getSubmodulesFromCommit; - const fromNext = yield getSubs(repo, fromCommit); - - const baseId = yield NodeGit.Merge.base(repo, - fromCommit.id(), - ontoCommit.id()); - const mergeBase = yield repo.getCommit(baseId); - const baseSubs = - yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, mergeBase); - - const ontoSubs = yield SubmoduleConfigUtil.getSubmodulesFromCommit( - repo, - ontoCommit); - - const merged = Conf.mergeSubmoduleConfigs(fromNext, ontoSubs, baseSubs); - // If it was resolved, write out and stage the new - // modules state. - - if (null !== merged) { - const newConf = Conf.writeConfigText(merged); - yield fs.writeFile(path.join(repo.workdir(), Conf.modulesFileName), - newConf); - yield index.addByPath(Conf.modulesFileName); - return true; - } - return false; -}); - /** * Process the specified `entry` from the specified `index` for the specified * `metaRepo` during a rebase from the specified `fromCommit` on the specified @@ -292,11 +242,12 @@ const processMetaRebaseEntry = co.wrap(function *(metaRepo, } else { if (SubmoduleConfigUtil.modulesFileName === entry.path) { - const succeeded = yield mergeModulesFile(metaRepo, - index, - fromCommit, - ontoCommit); + const succeeded = yield SubmoduleUtil.mergeModulesFile( + metaRepo, + fromCommit, + ontoCommit); if (succeeded) { + yield index.addByPath(SubmoduleConfigUtil.modulesFileName); break; // BREAK } } diff --git a/node/lib/util/repo_ast.js b/node/lib/util/repo_ast.js index ee0e97fe2..37f5c9434 100644 --- a/node/lib/util/repo_ast.js +++ b/node/lib/util/repo_ast.js @@ -38,6 +38,7 @@ const deeper = require("deeper"); const deepCopy = require("deepcopy"); const Rebase = require("./rebase"); +const Merge = require("./merge"); /** * @class {Branch} @@ -303,7 +304,8 @@ class AST { * @param {Object} [args.workdir] * @param {Object} [args.notes] * @param {Object} [args.openSubmodules] - * @param {Rebase} [args.Rebase] + * @param {Rebase} [args.rebase] + * @param {Merge} [args.merge] */ constructor(args) { if (undefined === args) { @@ -469,6 +471,17 @@ in commit ${id}.`); } } + this.d_merge = null; + if ("merge" in args) { + const merge = args.merge; + if (null !== merge) { + assert.instanceOf(merge, Merge); + assert.isFalse(this.d_bare); + checkAndTraverse(merge.originalHead, "original head of merge"); + checkAndTraverse(merge.mergeHead, "merge head"); + this.d_merge = merge; + } + } // Validate that all commits have been reached. @@ -640,6 +653,13 @@ in commit ${id}.`); return this.d_rebase; } + /** + * @property {Merge} null unless a merge is in progress + */ + get merge() { + return this.d_merge; + } + /** * Accumulate the specified `changes` into the specified `dest` map. A * non-null value in `changes` overrides any existing value in `dest`; a @@ -699,6 +719,7 @@ in commit ${id}.`); openSubmodules: ("openSubmodules" in args) ? args.openSubmodules : this.d_openSubmodules, rebase: ("rebase" in args) ? args.rebase : this.d_rebase, + merge: ("merge" in args) ? args.merge : this.d_merge, bare: ("bare" in args) ? args.bare : this.d_bare, }); } @@ -774,6 +795,7 @@ in commit ${id}.`); AST.Branch = Branch; AST.Commit = Commit; AST.Rebase = Rebase; +AST.Merge = Merge; AST.Remote = Remote; AST.Submodule = Submodule; module.exports = AST; diff --git a/node/lib/util/repo_ast_util.js b/node/lib/util/repo_ast_util.js index 7d7d2449f..d70d34cb2 100644 --- a/node/lib/util/repo_ast_util.js +++ b/node/lib/util/repo_ast_util.js @@ -472,6 +472,32 @@ ${colorExp(expected.rebase.onto)} but got ${colorAct(actual.rebase.onto)}.`); } } + // Check merge + + if (null === actual.merge && null !== expected.merge) { + result.push("Missing merge."); + } + else if (null !== actual.merge && null === expected.merge) { + result.push("Unexpected merge."); + } + else if (null !== actual.merge) { + if (actual.merge.message !== expected.merge.message) { + result.push(`Expected ${colorBad("merge message")} to be \ +${colorExp(expected.merge.message)} but got \ +${colorAct(actual.merge.message)}.`); + } + if (actual.merge.originalHead !== expected.merge.originalHead) { + result.push(`Expected ${colorBad("merge original head")} to be \ +${colorExp(expected.merge.originalHead)} but got \ +${colorAct(actual.merge.originalHead)}.`); + } + if (actual.merge.mergeHead !== expected.merge.mergeHead) { + result.push(`Expected ${colorBad("merge head")} to be \ +${colorExp(expected.merge.mergeHead)} but got \ +${colorAct(actual.merge.mergeHead)}.`); + } + } + return result; } @@ -697,6 +723,13 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { mapCommitId(rebase.onto)); } + let merge = ast.merge; + if (null !== merge) { + merge = new RepoAST.Merge(merge.message, + mapCommitId(merge.originalHead), + mapCommitId(merge.mergeHead)); + } + return ast.copy({ commits: commits, branches: branches, @@ -709,6 +742,7 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { workdir: mapChanges(ast.workdir), openSubmodules: openSubmodules, rebase: rebase, + merge: merge, }); }; diff --git a/node/lib/util/repo_status.js b/node/lib/util/repo_status.js index fe54eae84..c2f74ca90 100644 --- a/node/lib/util/repo_status.js +++ b/node/lib/util/repo_status.js @@ -32,6 +32,7 @@ const assert = require("chai").assert; const Rebase = require("./rebase"); +const Merge = require("./merge"); /** * This modules defines the type `RepoStatus`, used to describe modifications @@ -407,6 +408,7 @@ class RepoStatus { * @param {Object} [args.submodules] map from name to `Submodule` * @param {Object} [args.workdir] map from name to `FILESTATUS` * @param {Rebase} [args.rebase] rebase, if one is in progress + * @param {Merge} [args.merge] merge, if one is in progress */ constructor(args) { if (undefined === args) { @@ -421,6 +423,7 @@ class RepoStatus { this.d_workdir = {}; this.d_submodules = {}; this.d_rebase = null; + this.d_merge = null; if ("currentBranchName" in args) { if (null !== args.currentBranchName) { @@ -463,6 +466,14 @@ class RepoStatus { } this.d_rebase = rebase; } + + if ("merge" in args) { + const merge = args.merge; + if (null !== merge) { + assert.instanceOf(merge, Merge); + } + this.d_merge = merge; + } Object.freeze(this); } @@ -607,12 +618,19 @@ class RepoStatus { } /** - * @property {Rebase} rebase if non-null, state of in-progress rebase + * @property {rebase} rebase if non-null, state of in-progress rebase */ get rebase() { return this.d_rebase; } + /** + * @property {merge} merge if non-null, state of in-progress merge + */ + get merge() { + return this.d_merge; + } + /** * Return a new `RepoStatus` object having the same value as this one, but * with replacing properties defined in the specified `args`. @@ -637,6 +655,7 @@ class RepoStatus { args.submodules: this.d_submodules, workdir: ("workdir" in args) ? args.workdir : this.d_workdir, rebase: ("rebase" in args) ? args.rebase : this.d_rebase, + merge: ("merge" in args) ? args.merge : this.d_merge, }); } } diff --git a/node/lib/util/shorthand_parser_util.js b/node/lib/util/shorthand_parser_util.js index a5e2f2dcd..1efdbe5e6 100644 --- a/node/lib/util/shorthand_parser_util.js +++ b/node/lib/util/shorthand_parser_util.js @@ -57,7 +57,7 @@ const RepoASTUtil = require("../util/repo_ast_util"); * base repo type = 'S' | 'B' | ('C') | 'A' | 'N' * override = | | | | * | | | | - * | + * | | * head = 'H='| nothing means detached * nothing = * commit = + @@ -75,6 +75,7 @@ const RepoASTUtil = require("../util/repo_ast_util"); * [' '=[](',\s*'=[])*] * note = N =message * rebase = E,, + * merge = M,, * index = I [,]* * workdir = W [,]* * open submodule = 'O'[' '('!')*] @@ -171,6 +172,9 @@ const RepoASTUtil = require("../util/repo_ast_util"); * -- file `x` to be `y`. * S:Emaster,1,2 -- rebase in progress started on "master", * original head "1", onto commit "2" + * S:Mmerging commit,1,2 -- merge in progress started with commit + * message "merging commit" + * original head "1", merge head "2" * * Note that the "clone' type may not be used with single-repo ASTs, and the * url must map to the name of another repo. A cloned repository has the @@ -320,6 +324,7 @@ function prepareASTArguments(baseAST, rawRepo) { workdir: baseAST.workdir, openSubmodules: baseAST.openSubmodules, rebase: baseAST.rebase, + merge: baseAST.merge, bare: baseAST.bare, }; @@ -432,6 +437,12 @@ function prepareASTArguments(baseAST, rawRepo) { resultArgs.rebase = rawRepo.rebase; } + // Override merge if provided + + if ("merge" in rawRepo) { + resultArgs.merge = rawRepo.merge; + } + return resultArgs; } @@ -509,6 +520,7 @@ function parseOverrides(shorthand, begin, end, delimiter) { let notes = {}; let openSubmodules = {}; let rebase; + let merge; /** * Parse a set of changes from the specified `begin` character to the @@ -861,6 +873,26 @@ function parseOverrides(shorthand, begin, end, delimiter) { rebase = new RepoAST.Rebase(parts[0], parts[1], parts[2]); } + /** + * Parse the merge definition beginning at the specified `begin` and + * terminating at the specified `end`. + * + * @param {Number} begin + * @param {Number} end + */ + function parseMerge(begin, end) { + if (begin === end) { + merge = null; + return; // RETURN + } + const mergeDef = shorthand.substr(begin, end - begin); + const parts = mergeDef.split(","); + assert.equal(parts.length, + 3, + `Wrong number of merge parts in ${mergeDef}`); + merge = new RepoAST.Merge(parts[0], parts[1], parts[2]); + } + /** * Parse the override beginning at the specified `begin` and finishing at * the specified `end`. @@ -881,6 +913,7 @@ function parseOverrides(shorthand, begin, end, delimiter) { case "H": return parseHeadOverride; case "R": return parseRemote; case "I": return parseIndex; + case "M": return parseMerge; case "N": return parseNote; case "W": return parseWorkdir; case "O": return parseOpenSubmodule; @@ -926,6 +959,9 @@ function parseOverrides(shorthand, begin, end, delimiter) { if (undefined !== rebase) { result.rebase = rebase; } + if (undefined !== merge) { + result.merge = merge; + } return result; } @@ -1181,6 +1217,10 @@ exports.parseMultiRepoShorthand = function (shorthand, existingRepos) { includeCommit(resultArgs.rebase.originalHead); includeCommit(resultArgs.rebase.onto); } + if (resultArgs.merge) { + includeCommit(resultArgs.merge.originalHead); + includeCommit(resultArgs.merge.mergeHead); + } for (let remoteName in resultArgs.remotes) { const remote = resultArgs.remotes[remoteName]; for (let branch in remote.branches) { diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index d23524a43..2a26cd8fc 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -42,6 +42,8 @@ const NodeGit = require("nodegit"); const DiffUtil = require("./diff_util"); const GitUtil = require("./git_util"); +const Merge = require("./merge"); +const MergeFileUtil = require("./merge_file_util"); const Rebase = require("./rebase"); const RebaseFileUtil = require("./rebase_file_util"); const RepoStatus = require("./repo_status"); @@ -113,6 +115,29 @@ function remapRebase(rebase, commitMap) { return new Rebase(rebase.headName, originalHead, onto); } +/** + * Return a new `Merge` object having the same value as the specified `merge` + * but with commit shas being replaced by commits in the specified `commitMap`. + * + * @param {Merge} merg + * @param {Object} commitMap from sha to sha + */ +function remapMerge(merge, commitMap) { + assert.instanceOf(merge, Merge); + assert.isObject(commitMap); + + let originalHead = merge.originalHead; + let mergeHead = merge.mergeHead; + if (originalHead in commitMap) { + originalHead = commitMap[originalHead]; + } + if (mergeHead in commitMap) { + mergeHead = commitMap[mergeHead]; + } + return new Merge(merge.message, originalHead, mergeHead); +} + + /** * Return a new `RepoStatus` object having the same value as the specified * `status` but with all commit shas replaced by commits in the specified @@ -148,6 +173,8 @@ exports.remapRepoStatus = function (status, commitMap, urlMap) { workdir: status.workdir, rebase: status.rebase === null ? null : remapRebase(status.rebase, commitMap), + merge: status.merge === null ? null : remapMerge(status.merge, + commitMap), }); }; @@ -371,6 +398,8 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { args.rebase = rebase; } + args.merge = yield MergeFileUtil.readMerge(repo.path()); + if (options.showMetaChanges && !repo.isBare()) { const head = yield repo.getHeadCommit(); let tree = null; diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index e3b8c85af..4fec32218 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -439,6 +439,7 @@ exports.initSubmoduleAndRepo = co.wrap(function *(repoUrl, * dictionaries, who have the specified `mergeBase` dictionary as their merge * base; or, `null` if there is a conflict between the two that cannot be * resolved. + * TODO: indicate which submodules are in conflict * * @param {Object} lhs * @param {Object} rhs diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 72169ad4e..c5d4b353f 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -657,3 +657,53 @@ exports.cacheSubmodules = co.wrap(function *(repo, operation) { repo.submoduleCacheClear(); return result; }); + +/** + * Attempt to handle a conflicted `.gitmodules` file in the specified `repo` + * with changes from the specified `fromCommit` and `ontoCommit` commits. If + * successful, write the result to the .gitmodules file and return true; + * otherwise, return false. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} fromCommit + * @param {NodeGit.Commit} ontoCommit + * @return {Boolean} + */ +exports.mergeModulesFile = co.wrap(function *(repo, + fromCommit, + ontoCommit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(fromCommit, NodeGit.Commit); + assert.instanceOf(ontoCommit, NodeGit.Commit); + // If there is a conflict in the '.gitmodules' file, attempt to resolve it + // by comparing the current change against the original onto commit and the + // merge base between the base and onto commits. + + const Conf = SubmoduleConfigUtil; + const getSubs = Conf.getSubmodulesFromCommit; + const fromNext = yield getSubs(repo, fromCommit); + + const baseId = yield NodeGit.Merge.base(repo, + fromCommit.id(), + ontoCommit.id()); + const mergeBase = yield repo.getCommit(baseId); + const baseSubs = + yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, mergeBase); + + const ontoSubs = yield SubmoduleConfigUtil.getSubmodulesFromCommit( + repo, + ontoCommit); + + const merged = Conf.mergeSubmoduleConfigs(fromNext, ontoSubs, baseSubs); + // If it was resolved, write out and stage the new + // modules state. + + if (null !== merged) { + const newConf = Conf.writeConfigText(merged); + yield fs.writeFile(path.join(repo.workdir(), Conf.modulesFileName), + newConf); + return true; + } + return false; +}); + diff --git a/node/lib/util/test_util.js b/node/lib/util/test_util.js index 6f40651c7..cdb52540e 100644 --- a/node/lib/util/test_util.js +++ b/node/lib/util/test_util.js @@ -39,7 +39,7 @@ const co = require("co"); const fs = require("fs-promise"); const path = require("path"); const NodeGit = require("nodegit"); -const temp = require("temp").track(); +const temp = require("temp"); /** * Return the path to a newly-created temporary directory. diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index 8f64ce0ed..1fa924d40 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -46,6 +46,7 @@ const NodeGit = require("nodegit"); const path = require("path"); const DoWorkQueue = require("./do_work_queue"); +const MergeFileUtil = require("./merge_file_util"); const RebaseFileUtil = require("./rebase_file_util"); const RepoAST = require("./repo_ast"); const RepoASTUtil = require("./repo_ast_util"); @@ -437,6 +438,19 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { indexHead = rebase.onto; } + // Write out the merge info, if it exists. + + if (null !== ast.merge) { + // Map commits + + const originalSha = commitMap[ast.merge.originalHead]; + const mergeSha = commitMap[ast.merge.mergeHead]; + const merge = new RepoAST.Merge(ast.merge.message, + originalSha, + mergeSha); + yield MergeFileUtil.writeMerge(repo.path(), merge); + } + // Set up the index. We render the current commit and apply the index // on top of it. diff --git a/node/test/util/merge.js b/node/test/util/merge.js index 4dee1280b..e68d16b30 100644 --- a/node/test/util/merge.js +++ b/node/test/util/merge.js @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, Two Sigma Open Source + * Copyright (c) 2017, Two Sigma Open Source * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,198 +31,16 @@ "use strict"; const assert = require("chai").assert; -const co = require("co"); -const Merge = require("../../lib//util/merge"); -const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); -const StatusUtil = require("../../lib/util/status_util"); +const Merge = require("../../lib/util/merge"); -describe("merge", function () { - // Will do merge from repo `x`. A merge commit in the meta-repo will be - // named `x`; any merge commits in the sub-repos will be given the name of - // the sub-repo in which they are made. - // TODO: test for changes to submodule shas, and submodule deletions - - const doMerge = co.wrap(function *(upToDate, - fromCommit, - mode, - repos, - maps) { - const x = repos.x; - const status = yield StatusUtil.getRepoStatus(x); - const commitMap = maps.commitMap; - const reverseCommitMap = maps.reverseCommitMap; - assert.property(reverseCommitMap, fromCommit); - const physicalCommit = reverseCommitMap[fromCommit]; - const commit = yield x.getCommit(physicalCommit); - const result = yield Merge.merge(x, status, commit, mode, "message\n"); - if (upToDate) { - assert.isNull(result); - return; // RETURN - } - assert.isObject(result); - let newCommitMap = {}; - - // If a new commit was generated -- it wasn't a fast-forward commit -- - // record a mapping from the new commit to it's logical name: "x". - - if (!(result.metaCommit in commitMap)) { - newCommitMap[result.metaCommit] = "x"; - } - - // Map the new commits in submodules to the names of the submodules - // where they were made. - - Object.keys(result.submoduleCommits).forEach(name => { - commitMap[result.submoduleCommits[name]] = name; - }); - return { - commitMap: newCommitMap, - }; - }); - - // Test plan: - // - basic merging with meta-repo: normal/ffw/force commit - // - many scenarios with submodules - // - merges with open/closed unaffected submodules - // - where submodules are opened and closed - // - where they can and can't be fast-forwarded - - const MODE = Merge.MODE; - const cases = { - "trivial": { - initial: "x=S", - fromCommit: "1", - expected: null, - }, - "ancestor": { - initial: "x=S:C2-1;Bmaster=2", - fromCommit: "1", - upToDate: true, - expected: null, - }, - "ff merge, not required": { - initial: "x=S:C2-1;Bfoo=2", - fromCommit: "2", - expected: "x=E:Bmaster=2", - }, - "ff merge, required": { - initial: "x=S:C2-1;Bfoo=2", - fromCommit: "2", - mode: MODE.FF_ONLY, - expected: "x=E:Bmaster=2", - }, - "ff merge, but disallowed": { - initial: "x=S:C2-1;Bfoo=2", - fromCommit: "2", - mode: MODE.FORCE_COMMIT, - expected: "x=E:Cx-1,2 2=2;Bmaster=x", - }, - "one merge": { - initial: "x=S:C2-1;C3-1;Bmaster=2;Bfoo=3", - fromCommit: "3", - expected: "x=E:Cx-2,3 3=3;Bmaster=x", - }, - "one merge, forced anyway": { - initial: "x=S:C2-1;C3-1;Bmaster=2;Bfoo=3", - fromCommit: "3", - expected: "x=E:Cx-2,3 3=3;Bmaster=x", - mode: MODE.FORCE_COMMIT, - }, - "one merge, ff requested": { - initial: "x=S:C2-1;C3-1;Bmaster=2;Bfoo=3", - fromCommit: "3", - mode: MODE.FF_ONLY, - fails: true, - }, - "ff merge adding submodule": { - initial: "a=S|x=U:Bfoo=1;*=foo", - fromCommit: "2", - expected: "x=E:Bfoo=2", - }, - "ff merge with submodule change": { - initial: "a=S:C4-1;Bfoo=4|x=U:C5-2 s=Sa:4;Bfoo=5", - fromCommit: "5", - expected: "x=E:Bmaster=5;Os H=4", - }, - "fforwardable but disallowed with submodule change": { - initial: "a=S:C4-1;Bfoo=4|x=U:C5-2 s=Sa:4;Bfoo=5", - fromCommit: "5", - mode: MODE.FORCE_COMMIT, - expected: - "x=E:Bmaster=x;Cx-2,5 s=Sa:s;Os Cs-1,4 4=4!H=s", - }, - "fforwardable merge with non-ffwd submodule change": { - initial: "\ -a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c|\ -x=U:C3-2 s=Sa:b;C4-3 s=Sa:c;Bmaster=3;Bfoo=4", - fromCommit: "4", - expected: - "x=E:Cx-3,4 s=Sa:s;Os Cs-b,c c=c!H=s;Bmaster=x", - }, - "fforwardable merge with non-ffwd submodule change, ff requested": { - initial: "\ -a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c|\ -x=U:C3-2 s=Sa:b;C4-3 s=Sa:c;Bmaster=3;Bfoo=4", - fromCommit: "4", - mode: MODE.FF_ONLY, - expected: "x=E:Os H=b", - fails: true, - }, - "non-ffmerge with non-ffwd submodule change": { - initial: "\ -a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c|\ -x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4", - fromCommit: "4", - expected: - "x=E:Cx-3,4 s=Sa:s;Os Cs-b,c c=c!H=s;Bmaster=x", - }, - "non-ffmerge with non-ffwd submodule change, sub open": { - initial: "\ -a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c|\ -x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os", - fromCommit: "4", - expected: - "x=E:Cx-3,4 s=Sa:s;Os Cs-b,c c=c!H=s;Bmaster=x", - }, - "submodule commit is up-to-date": { - initial: "\ -a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c|\ -x=U:C3-2 s=Sa:c;C4-2 s=Sa:b,x=y;Bmaster=3;Bfoo=4", - fromCommit: "4", - expected: "x=E:Cx-3,4 x=y;Os H=c;Bmaster=x", - }, - "submodule commit is same": { - initial: "\ -a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c|\ -x=U:C3-2 s=Sa:c;C4-2 s=Sa:c,x=y;Bmaster=3;Bfoo=4", - fromCommit: "4", - expected: "x=E:Cx-3,4 x=y;Bmaster=x", - }, - "otherwise ffwardable change to meta with two subs; one can't ffwd": { - initial: "\ -a=Aa:Cb-a;Cc-a;Cd-c;Bx=d;By=b|\ -x=U:C3-2 s=Sa:b,t=Sa:c;C4-3 s=Sa:c,t=Sa:d;Bmaster=3;Bfoo=4", - fromCommit: "4", - expected: "\ -x=E:Cx-3,4 s=Sa:s,t=Sa:d;Bmaster=x;\ -Os Cs-b,c c=c!H=s;\ -Ot H=d", - }, - }; - Object.keys(cases).forEach(caseName => { - const c = cases[caseName]; - it(caseName, co.wrap(function *() { - const expected = c.expected; - function manipulator(repos, maps) { - const upToDate = null === expected; - const mode = !("mode" in c) ? MODE.NORMAL : c.mode; - return doMerge(upToDate, c.fromCommit, mode, repos, maps); - } - yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, - c.expected || {}, - manipulator, - c.fails); - })); +describe("Merge", function () { + it("breath", function () { + const m = new Merge("foo", "bar", "baz"); + assert.instanceOf(m, Merge); + assert.isFrozen(m); + assert.equal(m.message, "foo"); + assert.equal(m.originalHead, "bar"); + assert.equal(m.mergeHead, "baz"); }); }); diff --git a/node/test/util/merge_file_util.js b/node/test/util/merge_file_util.js new file mode 100644 index 000000000..01eea9fc0 --- /dev/null +++ b/node/test/util/merge_file_util.js @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2017, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const fs = require("fs-promise"); +const path = require("path"); + +const MergeFileUtil = require("../../lib//util/merge_file_util"); +const Merge = require("../../lib//util/merge"); +const TestUtil = require("../../lib/util/test_util"); + +describe("MergeFileUtil", function () { + describe("readMerge", function () { + it("found", co.wrap(function *() { + const tempDir = yield TestUtil.makeTempDir(); + const gitDir = path.join(tempDir, "META_MERGE"); + yield fs.mkdir(gitDir); + yield fs.writeFile(path.join(gitDir, "MSG"), "hello world\n"); + yield fs.writeFile(path.join(gitDir, "ORIG_HEAD"), "123\n"); + yield fs.writeFile(path.join(gitDir, "MERGE_HEAD"), "456\n"); + const result = yield MergeFileUtil.readMerge(tempDir); + assert.deepEqual(result, new Merge("hello world\n", "123", "456")); + })); + it("not found", co.wrap(function *() { + const tempDir = yield TestUtil.makeTempDir(); + const gitDir = path.join(tempDir, "META_MERGE"); + yield fs.mkdir(gitDir); + yield fs.writeFile(path.join(gitDir, "MSG"), "hello world\n"); + yield fs.writeFile(path.join(gitDir, "ORIG_HEAD"), "123\n"); + const result = yield MergeFileUtil.readMerge(tempDir); + assert.isNull(result); + })); + }); + it("writeMerge", co.wrap(function *() { + const tempDir = yield TestUtil.makeTempDir(); + const merge = new Merge("foo\n", "bar", "baz"); + yield MergeFileUtil.writeMerge(tempDir, merge); + const root = path.join(tempDir, "META_MERGE"); + const message = yield fs.readFile(path.join(root, "MSG"), "utf8"); + assert.equal(message, "foo\n"); + const originalHead = yield fs.readFile(path.join(root, "ORIG_HEAD"), + "utf8"); + assert.equal(originalHead, "bar\n"); + const mergeHead = yield fs.readFile(path.join(root, "MERGE_HEAD"), + "utf8"); + assert.equal(mergeHead, "baz\n"); + })); + it("cleanMerge", co.wrap(function *() { + const tempDir = yield TestUtil.makeTempDir(); + yield MergeFileUtil.writeMerge(tempDir, + new Merge("hey", "there", "all")); + yield MergeFileUtil.cleanMerge(tempDir); + const result = yield MergeFileUtil.readMerge(tempDir); + assert.isNull(result); + })); +}); diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js new file mode 100644 index 000000000..b53bf0ff2 --- /dev/null +++ b/node/test/util/merge_util.js @@ -0,0 +1,446 @@ +/* + * Copyright (c) 2017, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); + +const MergeUtil = require("../../lib//util/merge_util"); +const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); + +/** + * Return the commit map required by 'RepoASTTestUtil.testMultiRepoManipulator' + * from the specified 'result' returned by the 'merge' and 'continue' function, + * using the specified 'maps' provided to the manipulators. + */ +function mapReturnedCommits(result, maps) { + assert.isObject(result); + let newCommitMap = {}; + + // If a new commit was generated -- it wasn't a fast-forward commit -- + // record a mapping from the new commit to it's logical name: "x". + + const commitMap = maps.commitMap; + if (!(result.metaCommit in commitMap)) { + newCommitMap[result.metaCommit] = "x"; + } + + // Map the new commits in submodules to the names of the submodules where + // they were made. + + Object.keys(result.submoduleCommits).forEach(name => { + commitMap[result.submoduleCommits[name]] = name; + }); + return { + commitMap: newCommitMap, + }; +} + +describe("MergeUtil", function () { + describe("fastForwardMerge", function () { + const MODE = MergeUtil.MODE; + const cases = { + "simple": { + initial: "x=S:C2-1;Bfoo=2", + commit: "2", + mode: MODE.NORMAL, + expected: "x=E:Bmaster=2", + }, + "simple, FF_ONLY": { + initial: "x=S:C2-1;Bfoo=2", + commit: "2", + mode: MODE.FF_ONLY, + expected: "x=E:Bmaster=2", + }, + "simple detached": { + initial: "x=S:C2-1;Bfoo=2;*=", + commit: "2", + mode: MODE.NORMAL, + expected: "x=E:H=2", + }, + "with submodule": { + initial: "a=B:Ca-1;Ba=a|x=U:C3-2 s=Sa:a;Bfoo=3", + commit: "3", + mode: MODE.NORMAL, + expected: "x=E:Bmaster=3", + }, + "with open submodule": { + initial: "a=B:Ca-1;Ba=a|x=U:C3-2 s=Sa:a;Bfoo=3;Os", + commit: "3", + mode: MODE.NORMAL, + expected: "x=E:Bmaster=3;Os H=a", + }, + "with open submodule and change": { + initial: ` +a=B:Ca-1;Ba=a| +x=U:C3-2 s=Sa:a;Bfoo=3;Os W README.md=3`, + commit: "3", + mode: MODE.NORMAL, + expected: "x=E:Bmaster=3;Os H=a!W README.md=3", + }, + "with open submodule and conflict": { + initial: ` +a=B:Ca-1;Ba=a| +x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, + commit: "3", + mode: MODE.NORMAL, + fails: true, + }, + "force commit": { + initial: "x=S:C2-1;Bfoo=2", + commit: "2", + mode: MODE.FORCE_COMMIT, + expected: "x=E:Chahaha\n#x-1,2 2=2;Bmaster=x", + message: "hahaha", + }, + "force commit, detached": { + initial: "x=S:C2-1;Bfoo=2;*=", + commit: "2", + mode: MODE.FORCE_COMMIT, + expected: "x=E:Chahaha\n#x-1,2 2=2;H=x", + message: "hahaha", + }, + "ff merge adding submodule": { + initial: "a=S|x=U:Bfoo=1;*=foo", + commit: "2", + expected: "x=E:Bfoo=2", + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + const ffwd = co.wrap(function *(repos, maps) { + const x = repos.x; + const reverseCommitMap = maps.reverseCommitMap; + assert.property(reverseCommitMap, c.commit); + const physicalCommit = reverseCommitMap[c.commit]; + const commit = yield x.getCommit(physicalCommit); + const message = c.message || "message\n"; + const mode = c.mode || MODE.NORMAL; + const result = yield MergeUtil.fastForwardMerge(x, + mode, + commit, + message); + let newCommitMap = {}; + if (null !== result) { + assert.isString(result); + // If a new commit was generated, map it to "x". + + newCommitMap[result] = "x"; + } + return { + commitMap: newCommitMap, + }; + }); + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + c.expected, + ffwd, + c.fails); + })); + }); + }); + + describe("merge", function () { + // Will do merge from repo `x`. A merge commit in the meta-repo will + // be named `x`; any merge commits in the sub-repos will be given the + // name of the sub-repo in which they are made. TODO: test for changes + // to submodule shas, and submodule deletions + + // Test plan: + // - basic merging with meta-repo: normal/ffw/force commit; note that + // fast-forward merges are tested in the driver for + // 'fastForwardMerge', so we just need to validate that it works once + // here + // - many scenarios with submodules + // - merges with open/closed unaffected submodules + // - where submodules are opened and closed + // - where they can and can't be fast-forwarded + + const MODE = MergeUtil.MODE; + const cases = { + "trivial -- nothing to do": { + initial: "x=S", + fromCommit: "1", + expected: null, + }, + "staged change": { + initial: "x=S:I foo=bar", + fromCommit: "1", + fails: true, + }, + "submodule commit": { + initial: "a=B|x=U:Os Cs-1!H=s", + fromCommit: "1", + fails: true, + }, + "already a merge in progress": { + initial: "x=S:Mhia,1,1", + fromCommit: "1", + fails: true, + }, + "one merge": { + initial: "x=S:C2-1;C3-1;Bmaster=2;Bfoo=3", + fromCommit: "3", + expected: "x=E:Cx-2,3 3=3;Bmaster=x", + }, + "one merge with editor": { + initial: "x=S:C2-1;C3-1;Bmaster=2;Bfoo=3", + fromCommit: "3", + editMessage: () => Promise.resolve("foo\nbar\n# baz\n"), + expected: "x=E:Cfoo\nbar\n#x-2,3 3=3;Bmaster=x", + message: null, + }, + "one merge with empty message": { + initial: "x=S:C2-1;C3-1;Bmaster=2;Bfoo=3", + fromCommit: "3", + editMessage: () => Promise.resolve(""), + message: null, + }, + "non-ffmerge with ffwd submodule change": { + initial: ` +a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + expected: "x=E:Cx-3,4 s=Sa:c;Os H=c;Bmaster=x", + }, + "non-ffmerge with ffwd submodule change on lhs": { + initial: ` +a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| +x=U:C3-2 s=Sa:b;C4-2;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 4=4;Bmaster=x", + }, + "non-ffmerge with ffwd submodule change, auto-close": { + initial: ` +a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 s=Sa:c;Bmaster=x", + }, + "non-ffmerge with ffwd submodule change, doesn't auto-close": { + initial: ` +a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + expected: "x=E:Cx-3,4 s=Sa:c;Bmaster=x;Os", + }, + "non-ffmerge with non-ffwd submodule change": { + initial: ` +a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 s=Sa:s;Os Cs-b,c c=c!H=s;Bmaster=x", + }, + "non-ffmerge with non-ffwd submodule change, sub already open": { + initial: ` +a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + expected: "x=E:Cx-3,4 s=Sa:s;Os Cs-b,c c=c!H=s;Bmaster=x", + }, + "submodule commit is up-to-date": { + initial:` +a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:c;C4-2 s=Sa:b,x=y;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + expected: "x=E:Cx-3,4 x=y;Os H=c;Bmaster=x", + }, + "submodule commit is up-to-date, was not open": { + initial:` +a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:c;C4-2 s=Sa:b,x=y;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 x=y;Bmaster=x", + }, + "submodule commit is same": { + initial: ` +a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:c;C4-2 s=Sa:c,x=y;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 x=y;Bmaster=x", + }, + "added in merge": { + initial: ` +a=B| +x=S:C2-1;C3-1 t=Sa:1;Bmaster=2;Bfoo=3`, + fromCommit: "3", + expected: "x=E:Cx-2,3 t=Sa:1;Bmaster=x", + }, + "added on both sides": { + initial: ` +a=B| +x=S:C2-1 s=Sa:1;C3-1 t=Sa:1;Bmaster=2;Bfoo=3`, + fromCommit: "3", + expected: "x=E:Cx-2,3 t=Sa:1;Bmaster=x", + }, + "conflicted add": { + initial: ` +a=B|b=B| +x=S:C2-1 s=Sa:1;C3-1 s=Sb:1;Bmaster=2;Bfoo=3`, + fromCommit: "3", + expected: "x=E:Mmessage\n,2,3", + fails: true, + }, + "conflict in meta": { + initial: "x=S:C2-1 foo=bar;C3-1 foo=baz;Bmaster=2;Bfoo=3", + fromCommit: "3", + expected: "x=E:Mmessage\n,2,3", + fails: true, + }, + "conflict in submodule": { + initial: ` +a=B:Ca-1 README.md=8;Cb-1 README.md=9;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Mmessage\n,3,4;Os Mmessage\n,a,b", + fails: true, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const expected = c.expected; + + const doMerge = co.wrap(function *(repos, maps) { + const upToDate = null === expected; + const mode = !("mode" in c) ? MODE.NORMAL : c.mode; + const x = repos.x; + const reverseCommitMap = maps.reverseCommitMap; + assert.property(reverseCommitMap, c.fromCommit); + const physicalCommit = reverseCommitMap[c.fromCommit]; + const commit = yield x.getCommit(physicalCommit); + let message = c.message; + if (undefined === message) { + message = "message\n"; + } + const defaultEditor = function () {}; + const editMessage = c.editMessage || defaultEditor; + const result = yield MergeUtil.merge(x, + commit, + mode, + message, + editMessage); + if (upToDate) { + assert.isNull(result); + return; // RETURN + } + return mapReturnedCommits(result, maps); + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + expected || {}, + doMerge, + c.fails); + })); + }); + }); + describe("continue", function () { + // TODO: test abort from conflicts. Need conflict support to make this + // work. + + const cases = { + "no merge": { + initial: "x=S", + fails: true, + }, + "continue in meta": { + initial: "x=S:C2-1;C3-1;Bmaster=2;I baz=bam;Mhi\n,2,3;Bfoo=3", + expected: "x=E:Chi\n#x-2,3 baz=bam;Bmaster=x;M;I baz=~", + }, + "cheap continue in meta": { + initial: "x=S:C2;Mhi\n,1,2;B2=2", + expected: "x=E:Chi\n#x-1,2 ;Bmaster=x;M", + }, + "continue with extra in non-continue sub": { + initial: ` +a=B| +x=U:C3-1;Mhi\n,2,3;B3=3;Os I README.md=8`, + expected: ` +x=E:Chi\n#x-2,3 s=Sa:s;Bmaster=x;M;Os Chi\n#s-1 README.md=8!H=s`, + }, + "continue in a sub": { + initial: ` +a=B:Ca;Ba=a| +x=U:C3-1;Mhi\n,2,3;B3=3;Os I README.md=8!Myo\n,1,a!Ba=a`, + expected: ` +x=E:Chi\n#x-2,3 s=Sa:s;Bmaster=x;M;Os Cyo\n#s-1,a README.md=8!H=s!Ba=a`, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + const doContinue = co.wrap(function *(repos, maps) { + const repo = repos.x; + const result = yield MergeUtil.continue(repo); + return mapReturnedCommits(result, maps); + }); + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + c.expected, + doContinue, + c.fails); + })); + }); + }); + describe("abort", function() { + const cases = { + "no merge": { + initial: "x=S", + fails: true, + }, + "noop": { + initial: "x=S:Mfoo,1,1", + expected: "x=E:M", + }, + "noop with sub": { + initial: "a=B|x=U:Mfoo,1,1;Os Mfoo,1,1", + expected: "x=E:M;Os M", + }, + "moved back a sub": { + initial: ` +a=B| +x=U:Mx,1,1;Os Cs-1!H=s!Bs=s`, + expected: `x=E:M;Os H=1!Cs-1!Bs=s`, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + const doAbort = co.wrap(function *(repos) { + const repo = repos.x; + yield MergeUtil.abort(repo); + }); + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + c.expected, + doAbort, + c.fails); + })); + }); + }); +}); diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js index 7448db689..703622ceb 100644 --- a/node/test/util/print_status_util.js +++ b/node/test/util/print_status_util.js @@ -34,6 +34,7 @@ const assert = require("chai").assert; const colors = require("colors"); const Rebase = require("../../lib/util/rebase"); +const Merge = require("../../lib/util/merge"); const RepoStatus = require("../../lib/util/repo_status"); const PrintStatusUtil = require("../../lib/util/print_status_util"); @@ -687,7 +688,23 @@ describe("PrintStatusUtil", function () { input: new RepoStatus({ currentBranchName: "master", }), - regex: /On branch.*master.*\n.*nothing to commit.*/, + exact: `\ +On branch ${colors.green("master")}. +nothing to commit, working tree clean +`, + }, + "merge": { + input: new RepoStatus({ + currentBranchName: "master", + merge: new Merge("hello", "1", "1"), + }), + exact: `\ +On branch ${colors.green("master")}. +A merge is in progress. + (fix conflicts and run "git meta merge --continue") + (use "git meta merge --abort" to abort the merge) +nothing to commit, working tree clean +`, }, "rebase": { input: new RepoStatus({ diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index af84dc922..f71b962a6 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -37,6 +37,8 @@ const NodeGit = require("nodegit"); const path = require("path"); const DeinitUtil = require("../../lib/util/deinit_util"); +const Merge = require("../../lib/util/merge"); +const MergeFileUtil = require("../../lib/util/merge_file_util"); const Rebase = require("../../lib/util/rebase"); const RepoAST = require("../../lib/util/repo_ast"); const ReadRepoASTUtil = require("../../lib/util/read_repo_ast_util"); @@ -1433,15 +1435,61 @@ describe("readRAST", function () { yield NodeGit.Rebase.init(r, current, onto, null, null); - // Remove the branches, making the commits reachable only from the - // rebase. - const ast = yield ReadRepoASTUtil.readRAST(r); const rebase = ast.rebase; assert.equal(rebase.originalHead, thirdCommit.id().tostrS()); assert.equal(rebase.onto, secondCommit.id().tostrS()); })); + it("merge", co.wrap(function *() { + // Start out with a base repo having two branches, "master", and "foo", + // foo having one commit on top of master. + + const start = yield repoWithCommit(); + const r = start.repo; + + // Switch to master + + yield r.checkoutBranch("master"); + + const head = yield r.getHeadCommit(); + const sha = head.id().tostrS(); + + const merge = new Merge("hello", sha, sha); + + const original = yield ReadRepoASTUtil.readRAST(r); + const expected = original.copy({ + merge: merge, + }); + + yield MergeFileUtil.writeMerge(r.path(), merge); + + const actual = yield ReadRepoASTUtil.readRAST(r); + + RepoASTUtil.assertEqualASTs(actual, expected); + })); + + it("merge - unreachable", co.wrap(function *() { + const r = yield TestUtil.createSimpleRepository(); + r.detachHead(); + const secondCommit = yield TestUtil.generateCommit(r); + const thirdCommit = yield TestUtil.generateCommit(r); + + // Then begin a merge. + + const merge = new Merge("hello world", + secondCommit.id().tostrS(), + thirdCommit.id().tostrS()); + yield MergeFileUtil.writeMerge(r.path(), merge); + + // Remove the branches, making the commits reachable only from the + // rebase. + + const ast = yield ReadRepoASTUtil.readRAST(r); + const actualMerge = ast.merge; + assert.deepEqual(actualMerge, merge); + })); + it("add subs again", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); let expected = yield astFromSimpleRepo(repo); diff --git a/node/test/util/repo_ast.js b/node/test/util/repo_ast.js index 1792e177b..cf71f28f6 100644 --- a/node/test/util/repo_ast.js +++ b/node/test/util/repo_ast.js @@ -165,6 +165,7 @@ describe("RepoAST", function () { const Commit = RepoAST.Commit; const Rebase = RepoAST.Rebase; + const Merge = RepoAST.Merge; const Remote = RepoAST.Remote; const c1 = new Commit(); @@ -192,8 +193,8 @@ describe("RepoAST", function () { eworkdir: ("workdir" in expected) ? expected.workdir : {}, eopenSubmodules: ("openSubmodules" in expected) ? expected.openSubmodules : {}, - erebase: ("rebase" in expected) ? - expected.rebase : null, + erebase: ("rebase" in expected) ? expected.rebase : null, + emerge: ("merge" in expected) ? expected.merge : null, fails : fails, }; } @@ -207,6 +208,7 @@ describe("RepoAST", function () { head: null, currentBranchName: null, rebase: null, + merge: null, bare: false, }, undefined, @@ -230,6 +232,12 @@ describe("RepoAST", function () { commits: {"1": c1 }, head: "1", }, undefined, true), + "bad bare with merge": m({ + bare: true, + merge: new Merge("foo", "1", "1"), + commits: {"1": c1 }, + head: "1", + }, undefined, true), "branchCommit": m({ commits: {"1":c1, "2": cWithPar}, branches: {"master": new RepoAST.Branch("2", null) }, @@ -488,6 +496,37 @@ describe("RepoAST", function () { "bad rebase": m({ rebase: new Rebase("fff", "1", "1"), }, undefined, true), + "with merge": m({ + commits: { + "1": new Commit(), + }, + head: "1", + merge: new Merge("fff", "1", "1"), + }, { + commits: { + "1": new Commit(), + }, + head: "1", + merge: new Merge("fff", "1", "1"), + }), + "with merge specific commits": m({ + commits: { + "1": new Commit(), + "2": new Commit(), + }, + head: "1", + merge: new Merge("fff", "2", "2"), + }, { + commits: { + "1": new Commit(), + "2": new Commit(), + }, + head: "1", + merge: new Merge("fff", "2", "2"), + }), + "bad merge": m({ + merge: new Merge("fff", "1", "1"), + }, undefined, true), }; Object.keys(cases).forEach(caseName => { it(caseName, function () { @@ -510,6 +549,7 @@ describe("RepoAST", function () { assert.deepEqual(obj.workdir, c.eworkdir); assert.deepEqual(obj.openSubmodules, c.eopenSubmodules); assert.deepEqual(obj.rebase, c.erebase); + assert.deepEqual(obj.merge, c.emerge); assert.equal(obj.bare, c.ebare); if (c.input) { @@ -648,6 +688,7 @@ describe("RepoAST", function () { describe("AST.copy", function () { const Rebase = RepoAST.Rebase; + const Merge = RepoAST.Merge; const base = new RepoAST({ commits: { "1": new RepoAST.Commit()}, branches: { "master": new RepoAST.Branch("1", null) }, @@ -657,6 +698,7 @@ describe("RepoAST", function () { index: { foo: "bar" }, workdir: { foo: "bar" }, rebase: new Rebase("hello", "1", "1"), + merge: new Merge("hello", "1", "1"), bare: false, }); const newArgs = { @@ -669,6 +711,7 @@ describe("RepoAST", function () { index: { foo: "bar" }, workdir: { foo: "bar" }, rebase: new Rebase("hello world", "2", "2"), + merge: new Merge("hello world", "2", "2"), bare: false, }; const cases = { @@ -686,6 +729,7 @@ describe("RepoAST", function () { index: {}, workdir: {}, rebase: null, + merge: null, }, e: new RepoAST({ commits: { "1": new RepoAST.Commit()}, @@ -711,6 +755,7 @@ describe("RepoAST", function () { assert.deepEqual(obj.workdir, c.e.workdir); assert.deepEqual(obj.openSubmodules, c.e.openSubmodules); assert.deepEqual(obj.rebase, c.e.rebase); + assert.deepEqual(obj.merge, c.e.merge); assert.equal(obj.bare, c.e.bare); }); }); diff --git a/node/test/util/repo_ast_util.js b/node/test/util/repo_ast_util.js index ec244d9e5..be31a743b 100644 --- a/node/test/util/repo_ast_util.js +++ b/node/test/util/repo_ast_util.js @@ -133,6 +133,7 @@ describe("RepoAstUtil", function () { const AST = RepoAST; const Commit = AST.Commit; const Rebase = AST.Rebase; + const Merge = AST.Merge; const Remote = AST.Remote; const Submodule = AST.Submodule; @@ -165,6 +166,7 @@ describe("RepoAstUtil", function () { workdir: { foo: "bar" }, openSubmodules: { y: anAST }, rebase: new Rebase("foo", "1", "1"), + merge: new Merge("foo", "1", "1"), bare: false, }), expected: new AST({ @@ -178,6 +180,7 @@ describe("RepoAstUtil", function () { workdir: { foo: "bar" }, openSubmodules: { y: anAST }, rebase: new Rebase("foo", "1", "1"), + merge: new Merge("foo", "1", "1"), bare: false, }), }, @@ -497,6 +500,73 @@ describe("RepoAstUtil", function () { }), fails: true, }, + "missing merge": { + actual: new AST({ + commits: { "1": aCommit}, + head: "1", + merge: new Merge("foo", "1", "1"), + }), + expected: new AST({ + commits: { "1": aCommit}, + head: "1", + }), + fails: true, + }, + "unexpected merge": { + actual: new AST({ + commits: { "1": aCommit}, + head: "1", + }), + expected: new AST({ + commits: { "1": aCommit}, + head: "1", + merge: new Merge("foo", "1", "1"), + }), + fails: true, + }, + "wrong merge message": { + actual: new AST({ + commits: { "1": aCommit}, + head: "1", + merge: new Merge("foo", "1", "1"), + }), + expected: new AST({ + commits: { "1": aCommit}, + head: "1", + merge: new Merge("foo bar", "1", "1"), + }), + fails: true, + }, + "wrong merge original commit": { + actual: new AST({ + commits: { "1": aCommit, "2": aCommit}, + head: "1", + branches: { master: new RepoAST.Branch("2", null), }, + merge: new Merge("foo", "2", "1"), + }), + expected: new AST({ + commits: { "1": aCommit, "2": aCommit}, + head: "1", + branches: { master: new RepoAST.Branch("2", null), }, + merge: new Merge("foo", "1", "1"), + }), + fails: true, + }, + "wrong merge head": { + actual: new AST({ + commits: { "1": aCommit, "2": aCommit}, + head: "1", + branches: { master: new RepoAST.Branch("2", null), }, + merge: new Merge("foo", "1", "2"), + }), + expected: new AST({ + commits: { "1": aCommit, "2": aCommit}, + head: "1", + branches: { master: new RepoAST.Branch("2", null), }, + merge: new Merge("foo", "1", "1"), + }), + fails: true, + }, }; Object.keys(cases).forEach((caseName) => { const c = cases[caseName]; @@ -516,6 +586,7 @@ describe("RepoAstUtil", function () { describe("mapCommitsAndUrls", function () { const Commit = RepoAST.Commit; const Rebase = RepoAST.Rebase; + const Merge = RepoAST.Merge; const c1 = new Commit({ message: "foo" }); const cases = { "trivial": { i: new RepoAST(), m: {}, e: new RepoAST() }, @@ -872,6 +943,32 @@ describe("RepoAstUtil", function () { rebase: new Rebase("foo", "1", "1"), }), }, + "merge": { + i: new RepoAST({ + commits: { "1": c1 }, + head: "1", + merge: new Merge("foo", "1", "1"), + }), + m: { "1": "2"}, + e: new RepoAST({ + commits: { "2": c1 }, + head: "2", + merge: new Merge("foo", "2", "2"), + }), + }, + "merge unmapped": { + i: new RepoAST({ + commits: { "1": c1 }, + head: "1", + merge: new Merge("foo", "1", "1"), + }), + m: {}, + e: new RepoAST({ + commits: { "1": c1 }, + head: "1", + merge: new Merge("foo", "1", "1"), + }), + }, }; Object.keys(cases).forEach(caseName => { it(caseName, function () { @@ -1047,6 +1144,21 @@ describe("RepoAstUtil", function () { }, }), }, + "no clone merge": { + original: new AST({ + commits: { "1": c1}, + head: "1", + merge: new RepoAST.Merge("foo", "1", "1"), + }), + url: "foo", + expected: new AST({ + commits: { "1": c1 }, + head: "1", + remotes: { + origin: new Remote("foo", {}), + }, + }), + }, }; Object.keys(cases).forEach((caseName) => { const c = cases[caseName]; diff --git a/node/test/util/repo_status.js b/node/test/util/repo_status.js index 440e337c0..012900248 100644 --- a/node/test/util/repo_status.js +++ b/node/test/util/repo_status.js @@ -32,6 +32,7 @@ const assert = require("chai").assert; +const Merge = require("../../lib/util/merge"); const Rebase = require("../../lib/util/rebase"); const RepoStatus = require("../../lib/util/repo_status"); @@ -475,6 +476,7 @@ describe("RepoStatus", function () { workdir: {}, submodules: {}, rebase: null, + merge: null, }; return Object.assign(result, args); } @@ -500,6 +502,7 @@ describe("RepoStatus", function () { }), }, rebase: new Rebase("foo", "1", "2"), + merge: new Merge("baz", "2", "1"), }, e: m({ currentBranchName: "foo", @@ -513,6 +516,7 @@ describe("RepoStatus", function () { }), }, rebase: new Rebase("foo", "1", "2"), + merge: new Merge("baz", "2", "1"), }), } }; @@ -528,6 +532,7 @@ describe("RepoStatus", function () { assert.deepEqual(result.workdir, c.e.workdir); assert.deepEqual(result.submodules, c.e.submodules); assert.deepEqual(result.rebase, c.e.rebase); + assert.deepEqual(result.merge, c.e.merge); }); }); @@ -957,6 +962,7 @@ describe("RepoStatus", function () { }, workdir: { x: FILESTATUS.MODIFIED }, rebase: new Rebase("2", "4", "b"), + merge: new Merge("hah", "1", "1"), }); const anotherStat = new RepoStatus({ currentBranchName: "fo", @@ -967,6 +973,7 @@ describe("RepoStatus", function () { }, workdir: { x: FILESTATUS.ADDED }, rebase: new Rebase("a", "4", "b"), + merge: new Merge("a", "2", "2"), }); it("simple, no args", function () { const newStat = stat.copy(); @@ -984,6 +991,7 @@ describe("RepoStatus", function () { submodules: anotherStat.submodules, workdir: anotherStat.workdir, rebase: anotherStat.rebase, + merge: anotherStat.merge, }); assert.deepEqual(newStat, anotherStat); }); diff --git a/node/test/util/shorthand_parser_util.js b/node/test/util/shorthand_parser_util.js index 66d1d70f6..e74275338 100644 --- a/node/test/util/shorthand_parser_util.js +++ b/node/test/util/shorthand_parser_util.js @@ -599,6 +599,20 @@ describe("ShorthandParserUtil", function () { rebase: null, }), }, + "merge": { + i: "S:Mhi,1,2", + e: m({ + type: "S", + merge: new RepoAST.Merge("hi", "1", "2"), + }), + }, + "merge null": { + i: "S:M", + e: m({ + type: "S", + merge: null, + }), + }, "new submodule": { i: "S:I x=Sfoo:;Ox", e: m({ @@ -635,6 +649,7 @@ describe("ShorthandParserUtil", function () { assert.equal(r.currentBranchName, e.currentBranchName); assert.deepEqual(r.openSubmodules, e.openSubmodules); assert.deepEqual(r.rebase, e.rebase); + assert.deepEqual(r.merge, e.merge); }); }); }); @@ -806,6 +821,12 @@ describe("ShorthandParserUtil", function () { rebase: new RepoAST.Rebase("foo", "1", "1"), }), }, + "merge": { + i: "S:Mfoo,1,1", + e: S.copy({ + merge: new RepoAST.Merge("foo", "1", "1"), + }), + }, }; Object.keys(cases).forEach(caseName => { it(caseName, function () { @@ -1275,6 +1296,59 @@ x=S:Efoo,8,9`, }), } }, + "missing commits in merge": { + i: ` +a=B:C8-1;C9-1;Bmaster=8;Bfoo=9| +x=S:Mfoo,8,9`, + e: { + a: B.copy({ + commits: { + "1": new Commit({ + changes: { + "README.md": "hello world" + }, + message: "the first commit", + }), + "8": new Commit({ + parents: ["1"], + changes: { "8": "8" }, + message: "message\n", + }), + "9": new Commit({ + parents: ["1"], + changes: { "9": "9" }, + message: "message\n", + }), + }, + branches: { + master: new RepoAST.Branch("8", null), + foo: new RepoAST.Branch("9", null), + }, + head: "8", + }), + x: S.copy({ + commits: { + "1": new Commit({ + changes: { + "README.md": "hello world" + }, + message: "the first commit", + }), + "8": new Commit({ + parents: ["1"], + changes: { "8": "8" }, + message: "message\n", + }), + "9": new Commit({ + parents: ["1"], + changes: { "9": "9" }, + message: "message\n", + }), + }, + merge: new RepoAST.Merge("foo", "8", "9"), + }), + } + }, "new open sub": { i: "a=B|x=S:I s=Sa:;Os", e: { diff --git a/node/test/util/status_util.js b/node/test/util/status_util.js index a316d3d1c..b837d10fc 100644 --- a/node/test/util/status_util.js +++ b/node/test/util/status_util.js @@ -34,6 +34,7 @@ const assert = require("chai").assert; const co = require("co"); const path = require("path"); +const Merge = require("../../lib/util/merge"); const Rebase = require("../../lib/util/rebase"); const RepoAST = require("../../lib/util/repo_ast"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); @@ -105,6 +106,7 @@ describe("StatusUtil", function () { staged: { x: RepoStatus.FILESTATUS.ADDED }, workdir: { y: RepoStatus.FILESTATUS.ADDED }, rebase: new Rebase("foo", "1", "1"), + merge: new Merge("foo", "1", "1"), }), commitMap: { "1": "3"}, urlMap: {}, @@ -114,6 +116,7 @@ describe("StatusUtil", function () { staged: { x: RepoStatus.FILESTATUS.ADDED }, workdir: { y: RepoStatus.FILESTATUS.ADDED }, rebase: new Rebase("foo", "3", "3"), + merge: new Merge("foo", "3", "3"), }), }, "with a sub": { @@ -501,6 +504,14 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, rebase: new Rebase("master", "2", "3"), }), }, + "merge": { + state: "x=S:C2-1;C3-1;Bfoo=3;Bmaster=2;Mhi\n,2,3", + expected: new RepoStatus({ + headCommit: "2", + currentBranchName: "master", + merge: new Merge("hi\n", "2", "3"), + }), + }, "staged change": { state: "x=S:I README.md=whoohoo", options: { diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index 45c5e77e2..0bc0a52db 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -32,6 +32,7 @@ const assert = require("chai").assert; const co = require("co"); +const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); @@ -773,4 +774,66 @@ describe("SubmoduleUtil", function () { assert(false, "should have thrown"); })); }); + describe("mergeModulesFile", function () { + // This is largely tested in + // `SubmoduleConfigUtil.mergeSubmoduleConfigs`. + const cases = { + "trivial": { + input: "S:Cx-1;Bx=x;Cy-1;By=y", + expected: {}, + result: true, + }, + "one left": { + input: "S:Cx-1 s=Sa:1;Bx=x;Cy-1;By=y", + expected: { + s: "a", + }, + result: true, + }, + "one right": { + input: "S:Cx-1;Bx=x;Cy-1 s=Sa:1;By=y", + expected: { + s: "a", + }, + result: true, + }, + "one each": { + input: "S:Cx-1 s=Sa:1;Cy-1 t=Sa:1;By=y;Bx=x", + expected: { + s: "a", + t: "a", + }, + result: true, + }, + "conflict": { + input: ` +S:Cx-1 s=Sa:1;Cy-1 s=Sb:1;By=y;Bx=x;Cz-1 z=Sq:1;Bmaster=z`, + expected: { + z: "q", + }, + result: false, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(c.input); + const repo = written.repo; + const fromSha = written.oldCommitMap.x; + const ontoSha = written.oldCommitMap.y; + const from = yield repo.getCommit(fromSha); + const onto = yield repo.getCommit(ontoSha); + const result = yield SubmoduleUtil.mergeModulesFile(repo, + from, + onto); + assert.equal(result, c.result); + const configPath = path.join( + repo.workdir(), + SubmoduleConfigUtil.modulesFileName); + const data = yield fs.readFile(configPath, "utf8"); + const onDisk = SubmoduleConfigUtil.parseSubmoduleConfig(data); + assert.deepEqual(c.expected, onDisk); + })); + }); + }); }); diff --git a/node/test/util/write_repo_ast_util.js b/node/test/util/write_repo_ast_util.js index 8203587e8..7f4b02d47 100644 --- a/node/test/util/write_repo_ast_util.js +++ b/node/test/util/write_repo_ast_util.js @@ -342,6 +342,7 @@ S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q", expected: "\ S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q;H=3", }, + "with in-progress merge": "S:Mhello,1,1", "headless": { input: new RepoAST(), expected: new RepoAST(), From f9322c6c2517fb5bbbb732dc50504160d5121c38 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Mon, 13 Nov 2017 14:25:51 -0500 Subject: [PATCH 010/402] Fix problem where -a missed staged new files It, ironically, considered them to be untracked files. --- node/lib/util/commit.js | 1 + node/test/util/commit.js | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index b352d9d64..4113a296c 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -1574,6 +1574,7 @@ exports.getCommitStatus = co.wrap(function *(repo, cwd, options) { const workdirStatus = yield StatusUtil.getRepoStatus(repo, { showMetaChanges: options.showMetaChanges, ignoreIndex: true, + showAllUntracked: true, }); return exports.calculateAllRepoStatus(baseStatus, workdirStatus); } diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 9ca8903ab..89ab0733f 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -716,6 +716,28 @@ Untracked files: }, }), }, + "sub staged in new dir with all": { + state: "a=B|x=U:C3-2;Bmaster=3;Os I a/b/c=b", + options: { + all: true, + }, + expected: new RepoStatus({ + currentBranchName: "master", + headCommit: "3", + submodules: { + s: new Submodule({ + commit: new Submodule.Commit("1", "a"), + index: new Submodule.Index("1", "a", SAME), + workdir: new Submodule.Workdir(new RepoStatus({ + headCommit: "1", + staged: { + "a/b/c": FILESTATUS.ADDED, + }, + }), SAME), + }), + }, + }), + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From 8aca177ed66815531858b66b4d9375daba8e352a Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 14 Nov 2017 10:23:16 -0500 Subject: [PATCH 011/402] forward show --- node/lib/cmd/forward.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node/lib/cmd/forward.js b/node/lib/cmd/forward.js index 5d9b90bab..5a54df496 100644 --- a/node/lib/cmd/forward.js +++ b/node/lib/cmd/forward.js @@ -76,6 +76,7 @@ exports.forwardedCommands = new Set([ "fetch", "log", "remote", + "show", "tag", ]); From 8d7639c2581bff06b8d54527986bbb5e4acb4419 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 14 Nov 2017 10:41:39 -0500 Subject: [PATCH 012/402] Ignore misconfigured submodules --- node/lib/util/status_util.js | 12 ++++++++++-- node/test/util/status_util.js | 10 +++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 2a26cd8fc..72f11805c 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -232,7 +232,7 @@ exports.getRelation = co.wrap(function *(repo, from, to) { * optionally specified `repo`, and workdir `status` if open, the optionally * specified `indexUrl` and `indexSha` if the submodule exists in the index, * and the optionally specified `commitUrl` and `commitSha` if it exists in the - * HEAD commit. + * HEAD commit. Return `undefined` if the submodule is misconfigured. * @async * @private * @param {NodeGit.Repository|null} repo @@ -249,6 +249,11 @@ exports.getSubmoduleStatus = co.wrap(function *(repo, commitUrl, indexSha, commitSha) { + // If there is no index URL or commit URL, this submodule cannot be deleted + // or added. + if (null === commitUrl && null === indexUrl) { + return undefined; + } const Submodule = RepoStatus.Submodule; const COMMIT_RELATION = Submodule.COMMIT_RELATION; @@ -518,7 +523,10 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { // And copy them into the arguments. subsToList.forEach((name, i) => { - args.submodules[name] = subStats[i]; + const subStat = subStats[i]; + if (undefined !== subStat) { + args.submodules[name] = subStats[i]; + } }); } diff --git a/node/test/util/status_util.js b/node/test/util/status_util.js index b837d10fc..e1107cee9 100644 --- a/node/test/util/status_util.js +++ b/node/test/util/status_util.js @@ -421,7 +421,6 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, }), }, }; - Object.keys(cases).forEach(caseName => { const c = cases[caseName]; it(caseName, co.wrap(function *() { @@ -470,6 +469,15 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, assert.deepEqual(mappedResult, c.expected); })); }); + it("misconfigured", co.wrap(function *() { + const result = yield StatusUtil.getSubmoduleStatus(null, + null, + null, + null, + null, + null); + assert.isUndefined(result); + })); }); describe("getRepoStatus", function () { From a161ff79fd4797f46fb19695b0c8ab7bf3d373f7 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 15 Nov 2017 10:23:00 -0500 Subject: [PATCH 013/402] Stop listing misconfigured submodules as open. If a submodule is in `.git/config` but not in `.gitmodules`, don't list it as being open. This will prevent us from trying to perform operations on the submodules that will fail in unusual ways. --- node/lib/util/submodule_config_util.js | 25 +++++++++++++++++++++ node/lib/util/submodule_util.js | 8 ++++++- node/test/util/submodule_config_util.js | 30 +++++++++++++++++++++++++ node/test/util/submodule_util.js | 11 +++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index 4fec32218..defab64aa 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -257,6 +257,31 @@ exports.getSubmodulesFromIndex = co.wrap(function *(repo, index) { return exports.parseSubmoduleConfig(data); }); +/** + * Return a map from submodule name to url in the specified `repo`. + * + * @private + * @async + * @param {NodeGit.Repository} repo + * @return {Object} map from name to url + */ +exports.getSubmodulesFromWorkdir = function (repo) { + assert.instanceOf(repo, NodeGit.Repository); + + const modulesPath = path.join(repo.workdir(), exports.modulesFileName); + let data; + try { + data = fs.readFileSync(modulesPath, "utf8"); + } + catch (e) { + // File doesn't exist, no submodules configured. + } + if (undefined === data) { + return {}; + } + return exports.parseSubmoduleConfig(data); +}; + /** * Return the path to the config file for the specified `repo`. * diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index c5d4b353f..3243d2595 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -260,13 +260,19 @@ exports.listOpenSubmodules = co.wrap(function *(repo) { // the `.git/config` file without actually opening it, meaning that the // `.git/config` file cannot be used as the single source of truth and we // must verify with `isVisble`, which looks for a repositories `.git` file. + // Also, we need to make sure that the submodule is included in the + // `.gitmodules` file. If a user abandons a submodule while adding it, it + // may have a lingering reference in `.git/config` even though it's been + // removed from `.gitmodules`. + const configuredSubmodules = + SubmoduleConfigUtil.getSubmodulesFromWorkdir(repo); const openInConfig = SubmoduleConfigUtil.parseOpenSubmodules(text); const visCheckers = openInConfig.map(sub => exports.isVisible(repo, sub)); const visFlags = yield visCheckers; let result = []; openInConfig.forEach((name, i) => { - if (visFlags[i]) { + if ((name in configuredSubmodules) && visFlags[i]) { result.push(name); } }); diff --git a/node/test/util/submodule_config_util.js b/node/test/util/submodule_config_util.js index e71d6a48e..0433cdc60 100644 --- a/node/test/util/submodule_config_util.js +++ b/node/test/util/submodule_config_util.js @@ -406,6 +406,36 @@ describe("SubmoduleConfigUtil", function () { })); }); + describe("getSubmodulesFromWorkdir", function () { + // We know that the actual parsing is done by `parseSubmoduleConfig`; + // we just need to check that the parsing happens and that it works in + // the case where there is no `.gitmodules` file. + + it("no gitmodules", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const result = SubmoduleConfigUtil.getSubmodulesFromWorkdir(repo); + assert.deepEqual(result, {}); + })); + + it("with gitmodules", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const modulesPath = path.join(repo.workdir(), + SubmoduleConfigUtil.modulesFileName); + + yield fs.writeFile(modulesPath, `\ +[submodule "x/y"] + path = x/y +[submodule "x/y"] + url = /foo/bar/baz +` + ); + const result = SubmoduleConfigUtil.getSubmodulesFromWorkdir(repo); + assert.deepEqual(result, { + "x/y": "/foo/bar/baz", + }); + })); + }); + describe("getConfigPath", function () { it("breathing", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index 0bc0a52db..11c4ab978 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -338,6 +338,17 @@ describe("SubmoduleUtil", function () { const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo); assert.deepEqual(openSubs, []); })); + + it("missing from .gitmodules", co.wrap(function *() { + const input = "a=B|x=U:Os"; + const written = yield RepoASTTestUtil.createMultiRepos(input); + const x = written.repos.x; + const modulesPath = path.join(x.workdir(), + SubmoduleConfigUtil.modulesFileName); + yield fs.unlink(modulesPath); + const result = yield SubmoduleUtil.listOpenSubmodules(x); + assert.deepEqual(result, []); + })); }); describe("getSubmoduleRepos", function () { From 3fcda21c71e1820bc9e4c5d9ffa86fa587c3e75b Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 26 Nov 2017 12:14:19 -0500 Subject: [PATCH 014/402] Make "--soft" reset work correctly. And generally improve reset. Previously, we weren't doing resets in submodules that were closed, making for an effective "--hard" reset in unopened submodules. Addresses: https://github.com/twosigma/git-meta/issues/298 --- node/lib/util/reset.js | 63 +++++++++++++++++++++++++++++++++++------ node/test/util/reset.js | 15 +++++++++- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index 120b43235..0ee46cc88 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -35,6 +35,7 @@ const co = require("co"); const NodeGit = require("nodegit"); const GitUtil = require("./git_util"); +const Open = require("./open"); const StatusUtil = require("./status_util"); const SubmoduleFetcher = require("./submodule_fetcher"); const SubmoduleUtil = require("./submodule_util"); @@ -79,6 +80,21 @@ exports.reset = co.wrap(function *(repo, commit, type) { assert.instanceOf(commit, NodeGit.Commit); assert.isString(type); + const head = yield repo.getHeadCommit(); + const headTree = yield head.getTree(); + const commitTree = yield commit.getTree(); + const diff = yield NodeGit.Diff.treeToTree(repo, + commitTree, + headTree, + null); + const changedSubs = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff); + + // Prep the opener to open submodules on HEAD; otherwise, our resets will + // be noops. + + const opener = new Open.Opener(repo, null); + const fetcher = yield opener.fetcher(); + const resetType = getType(type); // First, reset the meta-repo. @@ -87,24 +103,53 @@ exports.reset = co.wrap(function *(repo, commit, type) { return NodeGit.Reset.reset(repo, commit, resetType); }); - // Then, all open subs. + // Make a list of submodules to reset, including all that have been changed + // between HEAD and 'commit', and all that are open. - const openNames = yield SubmoduleUtil.listOpenSubmodules(repo); + const openSubsSet = yield opener.getOpenSubs(); + const pathsToResetSet = new Set(openSubsSet); + Object.keys(changedSubs).forEach(path => pathsToResetSet.add(path)); + const pathsToReset = Array.from(pathsToResetSet); + const shas = yield SubmoduleUtil.getSubmoduleShasForCommit(repo, + pathsToReset, + commit); const index = yield repo.index(); - const shas = yield SubmoduleUtil.getCurrentSubmoduleShas(index, openNames); - const fetcher = new SubmoduleFetcher(repo, commit); - - yield openNames.map(co.wrap(function *(name, index) { - const sha = shas[index]; - const subRepo = yield SubmoduleUtil.getRepo(repo, name); + yield pathsToReset.map(co.wrap(function *(name) { + const change = changedSubs[name]; + if (undefined !== change && + (null === change.oldSha || null === change.newSha)) { + // If the submodule has been added or removed since 'commit', + // there's nothing to do. + return; // RETURN + } + const sha = shas[name]; + + // When doing a hard reset, we don't need to open closed submodules + // because we would be throwing away the changes anyway. + + if (TYPE.HARD === type && !openSubsSet.has(name)) { + return; // RETURN + } - // Fetch the sha in case we don't already have it. + // Open the submodule and fetch the sha of the commit to which we're + // resetting in case we don't have it. + const subRepo = yield opener.getSubrepo(name); yield fetcher.fetchSha(subRepo, name, sha); const subCommit = yield subRepo.getCommit(sha); yield NodeGit.Reset.reset(subRepo, subCommit, resetType); + + // Set the index to have the commit to which we just set the submodule; + // otherwise, Git will see a staged change and worktree modifications + // for the submodule. + + yield index.addByPath(name); })); + + // Write the index in case we've had to stage submodule changes. + + yield index.write(); }); /** diff --git a/node/test/util/reset.js b/node/test/util/reset.js index 2b9d85707..001321cb0 100644 --- a/node/test/util/reset.js +++ b/node/test/util/reset.js @@ -85,7 +85,7 @@ describe("reset", function () { type: TYPE.HARD, expected: "x=E:Bmaster=4" }, - "changed sub-repo not open": { + "hard changed sub-repo not open": { initial: "a=B:Ca-1 y=x;Bfoo=a|x=U:C4-2 s=Sa:a;Bfoo=4", to: "4", type: TYPE.HARD, @@ -110,6 +110,19 @@ a=B:Ca-1 y=x;Bfoo=a|x=U:C3-2 t=Sa:1;C4-3 s=Sa:a,t=Sa:a;Bfoo=4;Bmaster=3;Os;Ot`, type: TYPE.HARD, expected: "x=E:Bmaster=4;Os;Ot" }, + "soft in sub": { + initial: "a=B:Ca-1;Bmaster=a|x=U:C3-2 s=Sa:a;Bmaster=3;Bf=3", + to: "2", + type: TYPE.SOFT, + expected: "x=E:Os H=1!I a=a;Bmaster=2", + }, + "soft in sub, already open": { + initial: ` +a=B:Ca-1;Bmaster=a|x=U:C3-2 s=Sa:a;Bmaster=3;Bf=3;Os`, + to: "2", + type: TYPE.SOFT, + expected: "x=E:Os H=1!I a=a;Bmaster=2", + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From 3922625d81438c983a474f2813cf914b74b9f4da Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 26 Nov 2017 13:03:43 -0500 Subject: [PATCH 015/402] Add optional message to stash Addresses https://github.com/twosigma/git-meta/issues/423 --- node/lib/cmd/stash.js | 26 ++++++++++++++++++++++++-- node/lib/util/stash_util.js | 10 ++++++++-- node/test/util/stash_util.js | 15 +++++++++++++-- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/node/lib/cmd/stash.js b/node/lib/cmd/stash.js index 8550957fc..3294e7bf4 100644 --- a/node/lib/cmd/stash.js +++ b/node/lib/cmd/stash.js @@ -54,6 +54,13 @@ and unstaged commits to the sub-repos.`; exports.configureParser = function (parser) { + parser.addArgument(["-m", "--message"], { + type: "string", + defaultValue: null, + required: false, + help: "description; if not provided one will be generated", + }); + parser.addArgument("type", { help: ` 'save' to save a stash, 'pop' to restore, 'list' to show stashes, 'drop' to \ @@ -116,13 +123,22 @@ const doSave = co.wrap(function *(args) { console.warn("Nothing to stash."); return; // RETURN } - yield StashUtil.save(repo, status, includeUntracked || false); + yield StashUtil.save(repo, + status, + includeUntracked || false, + args.message); console.log("Saved working directory and index state."); }); -const doList = co.wrap(function *() { +const doList = co.wrap(function *(args) { const GitUtil = require("../../lib/util/git_util"); const StashUtil = require("../../lib/util/stash_util"); + const UserError = require("../../lib/util/user_error"); + + if (null !== args.message) { + throw new UserError("-m not compatible with list"); + } + const repo = yield GitUtil.getCurrentRepo(); const list = yield StashUtil.list(repo); process.stdout.write(list); @@ -131,6 +147,12 @@ const doList = co.wrap(function *() { const doDrop = co.wrap(function *(args) { const GitUtil = require("../../lib/util/git_util"); const StashUtil = require("../../lib/util/stash_util"); + const UserError = require("../../lib/util/user_error"); + + if (null !== args.message) { + throw new UserError("-m not compatible with list"); + } + const repo = yield GitUtil.getCurrentRepo(); const index = (null === args.stash) ? 0 : args.stash; yield StashUtil.removeStash(repo, index); diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 2a6ac062c..9c48996b9 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -118,12 +118,16 @@ WIP on ${branchDesc}: ${GitUtil.shortSha(head.id().tostrS())} ${message}`; * @param {NodeGit.Repository} repo * @param {RepoStatus} status * @param {Boolean} includeUntracked + * @param {String|null} message * @return {Object} submodule name to stashed commit */ -exports.save = co.wrap(function *(repo, status, includeUntracked) { +exports.save = co.wrap(function *(repo, status, includeUntracked, message) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(status, RepoStatus); assert.isBoolean(includeUntracked); + if (null !== message) { + assert.isString(message); + } const subResults = {}; // name to sha const subChanges = {}; // name to TreeUtil.Change @@ -194,7 +198,9 @@ exports.save = co.wrap(function *(repo, status, includeUntracked) { // Update the stash ref and the ref log - const message = yield exports.makeLogMessage(repo); + if (null === message) { + message = yield exports.makeLogMessage(repo); + } yield NodeGit.Reference.create(repo, metaStashRef, stashId, diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index c1d69f7ea..cbef62b33 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -206,6 +206,12 @@ x=E:Ci#i foo=bar,1=1;Cw#w foo=bar,1=1;Bi=i;Bw=w`, state: "x=S:C2-1 README.md;Bmaster=2", expected: `x=E:Cstash#s-2, ;Fmeta-stash=s`, }, + "with message": { + state: "x=S:C2-1 README.md;Bmaster=2", + expected: `x=E:Cstash#s-2, ;Fmeta-stash=s`, + message: "hello world", + expectedMessage: "hello world", + }, "closed sub": { state: "a=B|x=S:C2-1 README.md,s=Sa:1;Bmaster=2", expected: `x=E:Cstash#s-2, ;Fmeta-stash=s`, @@ -281,13 +287,18 @@ x=E:Fmeta-stash=s; const includeUntracked = c.includeUntracked || false; const stasher = co.wrap(function *(repos) { const repo = repos.x; - const expMessage = yield StashUtil.makeLogMessage(repo); + const stashMessage = c.message || null; + let expMessage = c.expectedMessage; + if (undefined === expMessage) { + expMessage = yield StashUtil.makeLogMessage(repo); + } const status = yield StatusUtil.getRepoStatus(repo, { showMetaChanges: false, }); const result = yield StashUtil.save(repo, status, - includeUntracked); + includeUntracked, + stashMessage); const commitMap = {}; const stashId = yield NodeGit.Reference.lookup( repo, From 264f4ccd5014a9fb7fcca75eafff7e5d92dcb4b0 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 26 Nov 2017 21:52:12 -0500 Subject: [PATCH 016/402] Removed failing "bare" case for getRepoStatus No longer works since we're looking at the worktree .gitmodules file. Note that `git status` will not run in a bare repo, so I think this is reasonable. --- node/test/util/status_util.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/node/test/util/status_util.js b/node/test/util/status_util.js index e1107cee9..d45712f9f 100644 --- a/node/test/util/status_util.js +++ b/node/test/util/status_util.js @@ -494,13 +494,6 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, headCommit: "1", }), }, - "bare": { - state: "x=B", - expected: new RepoStatus({ - currentBranchName: "master", - headCommit: "1", - }), - }, "empty": { state: { x: new RepoAST()}, expected: new RepoStatus(), From cb35290d93cb972399802ec98d3061849f81f557 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 26 Nov 2017 16:28:13 -0500 Subject: [PATCH 017/402] Remember to track temp files --- node/lib/util/test_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/test_util.js b/node/lib/util/test_util.js index cdb52540e..6f40651c7 100644 --- a/node/lib/util/test_util.js +++ b/node/lib/util/test_util.js @@ -39,7 +39,7 @@ const co = require("co"); const fs = require("fs-promise"); const path = require("path"); const NodeGit = require("nodegit"); -const temp = require("temp"); +const temp = require("temp").track(); /** * Return the path to a newly-created temporary directory. From 0f49f5140800980cbed73c23828c81dfc7b014ef Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 26 Nov 2017 21:07:24 -0500 Subject: [PATCH 018/402] Removed flawed assertion --- node/lib/util/repo_ast.js | 1 - 1 file changed, 1 deletion(-) diff --git a/node/lib/util/repo_ast.js b/node/lib/util/repo_ast.js index 37f5c9434..790dcc05f 100644 --- a/node/lib/util/repo_ast.js +++ b/node/lib/util/repo_ast.js @@ -530,7 +530,6 @@ in commit ${id}.`); const workdir = args.workdir; assert.isObject(workdir); for (let path in workdir) { - assert.isNotNull(this.d_head); assert.isFalse(this.d_bare); const change = workdir[path]; if (null !== change) { From 882780384a9613d11b22c590d8c876cac0ba2344 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 26 Nov 2017 15:05:12 -0500 Subject: [PATCH 019/402] Enhanced 'listWorkdirChanges' Now handles: * deleted submodules and submodules with index-only changes * updating gitmodules files * new submodules --- node/lib/util/tree_util.js | 37 ++++++++++++++--- node/test/util/tree_util.js | 82 +++++++++++++++++++++++++++++++++++-- 2 files changed, 110 insertions(+), 9 deletions(-) diff --git a/node/lib/util/tree_util.js b/node/lib/util/tree_util.js index 03b95fcab..2d3353071 100644 --- a/node/lib/util/tree_util.js +++ b/node/lib/util/tree_util.js @@ -35,7 +35,8 @@ const co = require("co"); const NodeGit = require("nodegit"); const path = require("path"); -const RepoStatus = require("./repo_status"); +const RepoStatus = require("./repo_status"); +const SubmoduleConfigUtil = require("./submodule_config_util"); /** * Return a nested tree mapping the flat structure in the specified `flatTree`, @@ -232,6 +233,7 @@ exports.listWorkdirChanges = function (repo, status, includeUnstaged) { assert.instanceOf(status, RepoStatus); assert.isBoolean(includeUnstaged); + let touchedModules = false; const FILESTATUS = RepoStatus.FILESTATUS; const FILEMODE = NodeGit.TreeEntry.FILEMODE; @@ -262,15 +264,38 @@ exports.listWorkdirChanges = function (repo, status, includeUnstaged) { // commits. const submodules = status.submodules; + const SAME = RepoStatus.Submodule.COMMIT_RELATION.SAME; for (let name in submodules) { const sub = submodules[name]; const wd = sub.workdir; - if (null !== wd && - RepoStatus.Submodule.COMMIT_RELATION.SAME !== wd.relation) { - result[name] = new Change( - NodeGit.Oid.fromString(wd.status.headCommit), - FILEMODE.COMMIT); + let sha = null; + touchedModules = touchedModules || + null === sub.commit || + null === sub.index || + sub.index.url !== sub.commit.url; + if (null !== wd && SAME !== wd.relation) { + sha = wd.status.headCommit; } + else if (null === sub.commit && null !== sub.index) { + sha = sub.index.sha; + } + else if (null === wd && null === sub.index) { + result[name] = null; + } + else if (null === wd && SAME !== sub.index.relation) { + sha = sub.index.sha; + } + if (null !== sha) { + result[name] = new Change(NodeGit.Oid.fromString(sha), + FILEMODE.COMMIT); + + } + } + + if (touchedModules) { + const modulesName = SubmoduleConfigUtil.modulesFileName; + const id = exports.hashFile(repo, modulesName); + result[modulesName] = new Change(id, FILEMODE.BLOB); } return result; diff --git a/node/test/util/tree_util.js b/node/test/util/tree_util.js index 88ae230f6..499db5e8b 100644 --- a/node/test/util/tree_util.js +++ b/node/test/util/tree_util.js @@ -36,9 +36,10 @@ const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); -const RepoStatus = require("../../lib/util/repo_status"); -const TestUtil = require("../../lib/util/test_util"); -const TreeUtil = require("../../lib/util/tree_util"); +const RepoStatus = require("../../lib/util/repo_status"); +const SubmoduleConfigUtil = require("../../lib/util/submodule_config_util"); +const TestUtil = require("../../lib/util/test_util"); +const TreeUtil = require("../../lib/util/tree_util"); describe("TreeUtil", function () { const Change = TreeUtil.Change; @@ -320,6 +321,81 @@ describe("TreeUtil", function () { FILEMODE.COMMIT), }); })); + it("submodule with index change", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const commit = "1111111111111111111111111111111111111111"; + const status = new RepoStatus({ + submodules: { + "sub": new RepoStatus.Submodule({ + commit: new Submodule.Commit("1", "/a"), + index: new Submodule.Index(commit, + "/a", + RELATION.AHEAD), + workdir: null, + }), + }, + }); + const result = TreeUtil.listWorkdirChanges(repo, status, true); + assert.deepEqual(result, { + sub: new Change(NodeGit.Oid.fromString(commit), + FILEMODE.COMMIT), + }); + })); + it("new submodule with commit", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const commit = "1111111111111111111111111111111111111111"; + const modPath = path.join(repo.workdir(), + SubmoduleConfigUtil.modulesFileName); + // It doesn't matter what's in the file, just that the function + // includes its contents. + yield fs.writeFile(modPath, "foo"); + const modId = TreeUtil.hashFile( + repo, + SubmoduleConfigUtil.modulesFileName); + const status = new RepoStatus({ + submodules: { + "sub": new RepoStatus.Submodule({ + commit: null, + index: new Submodule.Index(null, "/a", null), + workdir: new Submodule.Workdir(new RepoStatus({ + headCommit: commit, + }), RELATION.AHEAD), + }), + }, + }); + const result = TreeUtil.listWorkdirChanges(repo, status, true); + assert.deepEqual(result, { + sub: new Change(NodeGit.Oid.fromString(commit), + FILEMODE.COMMIT), + ".gitmodules": new Change(modId, FILEMODE.BLOB), + }); + })); + it("deleted submodule", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const modPath = path.join(repo.workdir(), + SubmoduleConfigUtil.modulesFileName); + // It doesn't matter what's in the file, just that the function + // includes its contents. + yield fs.writeFile(modPath, "foo"); + const modId = TreeUtil.hashFile( + repo, + SubmoduleConfigUtil.modulesFileName); + + const status = new RepoStatus({ + submodules: { + "sub": new RepoStatus.Submodule({ + commit: new Submodule.Commit("1", "/a"), + index: null, + workdir: null, + }), + }, + }); + const result = TreeUtil.listWorkdirChanges(repo, status, true); + assert.deepEqual(result, { + sub: null, + ".gitmodules": new Change(modId, FILEMODE.BLOB), + }); + })); it("untracked and index", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); const status = new RepoStatus({ From c81b5462ac931c564c676872d3c792957488e339 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 26 Nov 2017 21:41:05 -0500 Subject: [PATCH 020/402] Added `commit-shadow` command. For making commits without mutating the index or HEAD of the meta-repo or sub-repos. --- node/lib/cmd/commit-shadow.js | 103 ++++++++++++++++++++++++++ node/lib/git-meta.js | 4 +- node/lib/util/stash_util.js | 132 ++++++++++++++++++++++++++++++++++ node/lib/util/status_util.js | 6 ++ node/test/util/stash_util.js | 120 +++++++++++++++++++++++++++++++ 5 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 node/lib/cmd/commit-shadow.js diff --git a/node/lib/cmd/commit-shadow.js b/node/lib/cmd/commit-shadow.js new file mode 100644 index 000000000..b5d8c3f8f --- /dev/null +++ b/node/lib/cmd/commit-shadow.js @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2017, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const co = require("co"); + +/** + * this module is the entrypoint for the `commit-shadow` command. + */ + +/** + * help text for the `commit-shadow` command + * + * @property {String} + */ +exports.helpText = + `Create a "shadow" commit, leaving index and HEAD unchanged.`; + +/** + * description of the `commit-shadow` comand + * + * @property {String} + */ +exports.description = `Create a "shadow" commit containing all local +modifications (including untracked files if '--include-untracked' is specified) +to all sub-repos and then print the SHA of the created commit. If there are no +local modifications, print the SHA of HEAD. Do not modify the index or update +HEAD to point to the created commit. Note that this command ignores +non-submodule changes to the meta-repo. Note also that this command is meant +for programmatic use and its output format is stable.`; + +/** + * Configure the specified `parser` for the `commit` command. + * + * @param {ArgumentParser} parser + */ +exports.configureParser = function (parser) { + parser.addArgument(["-u", "--include-untracked"], { + required: false, + action: "storeConst", + constant: true, + defaultValue: false, + help: "include untracked files in the shadow commit", + }); + parser.addArgument(["-m", "--message"], { + type: "string", + required: true, + help: "commit message for shadow commits", + }); +}; + +/** + * Exeucte the `commit-shadow` command according to the specified `args`. + * + * @async + * @param {Object} args + * @param {String} args.message + */ +exports.executeableSubcommand = co.wrap(function *(args) { + const GitUtil = require("../util/git_util"); + const StashUtil = require("../util/stash_util"); + + const repo = yield GitUtil.getCurrentRepo(); + const result = yield StashUtil.makeShadowCommit(repo, + args.message, + false, + args.include_untracked ); + if (null === result) { + const head = yield repo.getHeadCommit(); + console.log(head.id().tostrS()); + } + else { + console.log(result.metaCommit); + } +}); diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js index 316df738b..5a6bfb776 100755 --- a/node/lib/git-meta.js +++ b/node/lib/git-meta.js @@ -54,9 +54,10 @@ const push = require("./cmd/push"); const rebase = require("./cmd/rebase"); const reset = require("./cmd/reset"); const root = require("./cmd/root"); -const submodule = require("./cmd/submodule"); +const commitShadow = require("./cmd/commit-shadow"); const stash = require("./cmd/stash"); const status = require("./cmd/status"); +const submodule = require("./cmd/submodule"); const UserError = require("./util/user_error"); const version = require("./cmd/version"); @@ -133,6 +134,7 @@ const commands = { "rebase": rebase, "reset": reset, "root": root, + "commit-shadow": commitShadow, "stash": stash, "submodule": submodule, "status": status, diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 9c48996b9..28c2ce85c 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -457,3 +457,135 @@ exports.list = co.wrap(function *(repo) { } return result; }); + +/** + * Make a shadow commit for the specified `repo` having the specified `status`; + * use the specified commit `message`. Ignored untracked files unless the + * specified `includeUntracked` is true. Return the sha of the created commit. + * Note that this method does not recurse into submodules. + * + * @param {NodeGit.Repository} repo + * @param {RepoStatus} status + * @param {String} message + * @param {Bool} includeUntracked + * @return {String} + */ +const makeShadowCommitForRepo = co.wrap(function *(repo, + status, + message, + includeUntracked) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(status, RepoStatus); + assert.isString(message); + assert.isBoolean(includeUntracked); + + const changes = yield TreeUtil.listWorkdirChanges(repo, + status, + includeUntracked); + const head = yield repo.getHeadCommit(); + let headTree = null; + const parents = []; + if (null !== head) { + parents.push(head); + headTree = yield head.getTree(); + } + const newTree = yield TreeUtil.writeTree(repo, headTree, changes); + const sig = repo.defaultSignature(); + const id = yield NodeGit.Commit.create(repo, + null, + sig, + sig, + null, + message, + newTree, + parents.length, + parents); + return id.tostrS(); +}); + +/** + * Generate a shadow commit in the specified 'repo' with the specified + * 'message' and return an object describing the created commits. Ignore + * untracked files unless the specified 'includeUntracked' is true. If the + * repository is clean, return null. Note that this command does not affect + * the state of 'repo' other than to generate commits. + * + * TODO: Note that we cannot really support `includeMeta === true` due to the + * fact that `getRepoStatus` is broken when `true === ignoreIndex` and there + * are new submodules (see TODO in `StatusUtil.getRepoStatus`). + * + * @param {NodeGit.Repository} repo + * @param {String} message + * @param {Bool} includeMeta + * @param {Bool} includeUntracked + * @return {Object|null} + * @return {String} return.metaCommit + * @return {Object} return.subCommits path to sha of generated subrepo commits + */ +exports.makeShadowCommit = co.wrap(function *(repo, + message, + includeMeta, + includeUntracked) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(message); + assert.isBoolean(includeMeta); + assert.isBoolean(includeUntracked); + + if (!message.endsWith("\n")) { + message += "\n"; + } + + const status = yield StatusUtil.getRepoStatus(repo, { + showMetaChanges: includeMeta, + showAllUntracked: true, + ignoreIndex: true, + }); + if (status.isDeepClean(includeUntracked)) { + return null; // RETURN + } + const subCommits = {}; + const subStats = status.submodules; + const Submodule = RepoStatus.Submodule; + yield Object.keys(subStats).map(co.wrap(function *(name) { + const subStatus = subStats[name]; + const wd = subStatus.workdir; + + // If the submodule is closed or its workdir is clean, we don't need to + // do anything for it. + + if (null === wd || wd.status.isClean(includeUntracked)) { + return; // RETURN + } + const subRepo = yield SubmoduleUtil.getRepo(repo, name); + const subSha = yield makeShadowCommitForRepo(subRepo, + wd.status, + message, + includeUntracked); + const newSubStat = new Submodule({ + commit: subStatus.commit, + index: subStatus.index, + workdir: new Submodule.Workdir(new RepoStatus({ + headCommit: subSha, + }), Submodule.COMMIT_RELATION.AHEAD), + }); + + // Update the status for this submodule so that it will be written + // correctly. + + subStats[name] = newSubStat; + subCommits[name] = subSha; + })); + + // Update the submodules in the status object to reflect newly-generated + // commits. + + const newStatus = status.copy({ submodules: subStats }); + const metaCommit = yield makeShadowCommitForRepo(repo, + newStatus, + message, + includeUntracked); + return { + metaCommit: metaCommit, + subCommits: subCommits, + }; +}); diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 72f11805c..9b3edf11f 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -327,6 +327,12 @@ exports.getSubmoduleStatus = co.wrap(function *(repo, * than against the index. If the specified `options.cwd` is provided, resolve * paths in the context of that directory. * + * TODO: Note that this function is broken when + * `true === ignoreIndex && true === showMetaChanges` and + * there are new submodules. It erroneously reports that the path with the new + * submodule is an untracked file. We need to put some logic in that + * recognizes these paths as being inside a submodule and filters them out. + * * @async * @param {NodeGit.Repository} repo * @param {Object} [options] diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index cbef62b33..15a6dcfe1 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -698,4 +698,124 @@ meta-stash@{1}: log of 1 })); }); }); + + describe("shadow", function () { + const cases = { + "clean": { + state: "x=S", + includeMeta: true, + }, + "a new file": { + state: "x=S:W foo/bar=2", + expected: "x=E:Cm-1 foo/bar=2;Bm=m", + includeMeta: true, + }, + "a new file, untracked not included": { + state: "x=S:W foo/bar=2", + includeMeta: true, + includeUntracked: false, + }, + "with a message": { + state: "x=S:W foo/bar=2", + expected: "x=E:Cfoo\n#m-1 foo/bar=2;Bm=m", + message: "foo", + includeMeta: true, + }, + "deleted file": { + state: "x=S:W README.md", + expected: "x=E:Cm-1 README.md;Bm=m", + includeMeta: true, + }, + "change in index": { + state: "x=S:I README.md=3", + expected: "x=E:Cm-1 README.md=3;Bm=m", + includeMeta: true, + }, + "new file in index": { + state: "x=S:I foo/bar=8", + expected: "x=E:Cm-1 foo/bar=8;Bm=m", + includeMeta: true, + }, + "unchanged submodule": { + state: "a=B|x=U:W README.md=2;Os", + expected: "x=E:Cm-2 README.md=2;Bm=m", + includeMeta: true, + }, + "new commit in unopened submodule": { + state: "a=B:Ca-1;Ba=a|x=U:I s=Sa:a", + expected: "x=E:Cm-2 s=Sa:a;Bm=m", + includeMeta: true, + }, + "new file in open submodule": { + state: "a=B|x=U:Os W x/y/z=3", + includeMeta: true, + expected: ` +x=E:Cm-2 s=Sa:s;Bm=m;Os W x/y/z=3!Cs-1 x/y/z=3!Bs=s`, + }, + "new file in open submodule, untracked not included": { + state: "a=B|x=U:Os W x/y/z=3", + includeMeta: true, + includeUntracked: false, + }, + "new submodule with a commit": { + state: "a=B|x=S:I s=Sa:;Os Cq-1!H=q", + includeMeta: false, + expected: ` +x=E:Cm-1 s=Sa:q;Bm=m` + }, + "new submodule with a new file": { + state: "a=B|x=S:I s=Sa:;Os W foo=bar", + includeMeta: false, + expected: ` +x=E:Cm-1 s=Sa:s;Bm=m;Os Cs foo=bar!Bs=s!W foo=bar` + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + const shadower = co.wrap(function *(repos) { + // If a meta commit was made, map it to "m" and create a branch + // named "m" pointing to it. For each submodule commit made, + // map the commit to one with that submodule's name, and make a + // branch in the submodule with that name. + + const repo = repos.x; + const message = c.message || "message\n"; + const meta = c.includeMeta; + const includeUntracked = + undefined === c.includeUntracked || c.includeUntracked; + const result = yield StashUtil.makeShadowCommit( + repo, + message, + meta, + includeUntracked); + const commitMap = {}; + if (null !== result) { + const metaSha = result.metaCommit; + const commit = yield repo.getCommit(metaSha); + commitMap[metaSha] = "m"; + yield NodeGit.Branch.create(repo, "m", commit, 1); + for (let path in result.subCommits) { + const subSha = result.subCommits[path]; + const subRepo = yield SubmoduleUtil.getRepo(repo, + path); + const subCommit = yield subRepo.getCommit(subSha); + commitMap[subSha] = path; + yield NodeGit.Branch.create(subRepo, + path, + subCommit, + 1); + } + } + return { + commitMap: commitMap, + }; + }); + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.state, + c.expected, + shadower, + c.fails); + })); + }); + }); }); From a8dac75f0b656cee0684127b6fb277634a4fb3d3 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 28 Nov 2017 10:57:05 -0500 Subject: [PATCH 021/402] Fixed merge when new in target, unchanged HEAD If the submodule was open, we were not setting its HEAD to the new commit. The old commit was being added back to the index and saved. --- node/lib/util/merge_util.js | 63 +++++++++++++++++++++++++++--------- node/test/util/merge_util.js | 17 ++++++++++ 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 51641a08c..374b063b7 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -136,6 +136,29 @@ exports.fastForwardMerge = co.wrap(function *(repo, mode, commit, message) { return result; }); +/** + * Perform a fast-forward in the submodule having the specified path. Use the + * specified 'opener' to get the submodule repo, if it's open, and move it to + * the specied 'sha'; stage the change in the specified 'index'. + */ +const doFastForward = co.wrap(function *(opener, index, path, toSha) { + assert.instanceOf(opener, Open.Opener); + assert.instanceOf(index, NodeGit.Index); + assert.isString(path); + assert.isString(toSha); + console.log(`Submodule ${colors.blue(path)}: fast-forward to \ +${colors.green(toSha)}.`); + const open = yield opener.isOpen(path); + if (open) { + const repo = yield opener.getSubrepo(path); + const fetcher = yield opener.fetcher(); + yield fetcher.fetchSha(repo, path, toSha); + const commit = yield repo.getCommit(toSha); + yield NodeGit.Reset.reset(repo, commit, NodeGit.Reset.TYPE.HARD); + yield index.addByPath(path); + } +}); + /** * Merge the specified `commit` in the specified `repo` having the specified * `status`, using the specified `mode` to control whether or not a merge @@ -278,7 +301,6 @@ ${colors.red(commitSha)}.`); hasModulesFile = true; return; // RETURN } - else if (RepoStatus.STAGE.THEIRS === stage && !(path in subUrls)) { errorMessage += `Conflict in ${colors.red(path)}`; return; // RETURN @@ -287,17 +309,32 @@ ${colors.red(commitSha)}.`); // We don't need to do anything with an entry unless it is a conflicted // submodule. - if (RepoStatus.STAGE.THEIRS !== stage || !(path in subUrls)) { + if (!(path in subUrls)) { return; // RETURN } - // Otherwise, we have a submodule that needs to be merged. - const subSha = entry.id.tostrS(); const subCommitId = NodeGit.Oid.fromString(subSha); const subEntry = yield headTree.entryByPath(path); const subHeadSha = subEntry.sha(); - const subCommitSha = subCommitId.tostrS(); + + // If the submodule has a "normal" stage, that means it can be + // trivially fast-forwarded if there is a change. + + if (RepoStatus.STAGE.NORMAL === stage) { + if (subSha !== subHeadSha) { + yield doFastForward(opener, index, path, subSha); + } + return; // RETURN + } + + // Otherwise, if there is a conflict in the submodule, we'll handle it + // during the THEIRS entry. + + else if (RepoStatus.STAGE.THEIRS !== stage) { + return; // RETURN + } + const subRepo = yield opener.getSubrepo(path); // Fetch commit to merge. @@ -308,24 +345,18 @@ ${colors.red(commitSha)}.`); const upToDate = yield NodeGit.Graph.descendantOf(subRepo, subHeadSha, - subCommitSha); + subSha); if (upToDate) { + console.log("We're up-to-date with", path); yield index.addByPath(path); return; // RETURN } - // If we can fast-forward, we don't need to do a merge. - const canSubFF = yield NodeGit.Graph.descendantOf(subRepo, - subCommitSha, + subSha, subHeadSha); if (canSubFF) { - console.log(`Submodule ${colors.blue(path)}: fast-forward to \ -${colors.green(subCommitSha)}.`); - yield NodeGit.Reset.reset(subRepo, - subCommit, - NodeGit.Reset.TYPE.HARD); - yield index.addByPath(path); + yield doFastForward(opener, index, path, subSha); return; // RETURN } @@ -333,7 +364,7 @@ ${colors.green(subCommitSha)}.`); // anything. console.log(`Submodule ${colors.blue(path)}: merging commit \ -${colors.green(subCommitSha)}.`); +${colors.green(subSha)}.`); // Start the merge. diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index b53bf0ff2..3788edd35 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -323,6 +323,23 @@ x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, expected: "x=E:Mmessage\n,3,4;Os Mmessage\n,a,b", fails: true, }, + "new commit in sub in target branch but not in HEAD branch": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 t=Sa:1;C4-3 s=Sa:a;C5-3 t=Sa:b;Bmaster=4;Bfoo=5;Os;Ot`, + fromCommit: "5", + expected: ` +x=E:Cx-4,5 t=Sa:b;Bmaster=x;Ot H=b;Os` + }, + "new commit in sub in target branch but not in HEAD branch, closed" + : { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 t=Sa:1;C4-3 s=Sa:a;C5-3 t=Sa:b;Bmaster=4;Bfoo=5`, + fromCommit: "5", + expected: ` +x=E:Cx-4,5 t=Sa:b;Bmaster=x` + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From 68bc83691f25d8b7eb5d5c1cabd83502c909cfbe Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 28 Nov 2017 17:09:20 -0500 Subject: [PATCH 022/402] Cache dependency check in find-meta search Speeds search throough 200k commits from 180s to 30s in a test repo. Addresses https://github.com/twosigma/git-meta/issues/429 --- node/lib/util/log_util.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/node/lib/util/log_util.js b/node/lib/util/log_util.js index 2ab6185de..b27123730 100644 --- a/node/lib/util/log_util.js +++ b/node/lib/util/log_util.js @@ -81,6 +81,7 @@ exports.findMetaCommit = co.wrap(function *(repo, let toCheck = [metaCommit]; // commits left to check const checked = new Set(); // SHAs checked const existsInSha = new Map(); // repo sha to bool if target included + const isDescended = new Map(); // cache of descendant check const doesExistInCommit = co.wrap(function *(commit) { const sha = commit.id().tostrS(); @@ -108,15 +109,25 @@ exports.findMetaCommit = co.wrap(function *(repo, result = true; } else { - // Ensure that the commit we're checking against is present. - - yield subFetcher.fetchSha(subRepo, - submoduleName, - subShaForCommit); - result = (yield NodeGit.Graph.descendantOf( + // Check to see if the commit we're looking for is descended + // from the current commit. First, look in the cache. + + if (isDescended.has(subShaForCommit)) { + result = isDescended.get(subShaForCommit); + } + else { + // Ensure that the commit we're checking against is + // present; we can't do a descendant check otherwise. + + yield subFetcher.fetchSha(subRepo, + submoduleName, + subShaForCommit); + result = (yield NodeGit.Graph.descendantOf( subRepo, NodeGit.Oid.fromString(subShaForCommit), subCommit.id())) !== 0; + isDescended.set(subShaForCommit, result); + } } } existsInSha.set(sha, result); From 0ecc6764152cb6ccc401354f24b85e3e06ed3778 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 29 Nov 2017 16:59:06 -0500 Subject: [PATCH 023/402] Load status only for submodules being closed. --- node/lib/util/close_util.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/node/lib/util/close_util.js b/node/lib/util/close_util.js index 9a0a84f84..9a81d8080 100644 --- a/node/lib/util/close_util.js +++ b/node/lib/util/close_util.js @@ -59,17 +59,19 @@ exports.close = co.wrap(function *(repo, cwd, paths, force) { assert.isArray(paths); assert.isBoolean(force); - const repoStatus = yield StatusUtil.getRepoStatus(repo); - const subStats = repoStatus.submodules; - let errorMessage = ""; - const workdir = repo.workdir(); const subs = yield SubmoduleUtil.getSubmoduleNames(repo); - const subsToClose = yield SubmoduleUtil.resolveSubmoduleNames(workdir, cwd, subs, paths); + + const repoStatus = yield StatusUtil.getRepoStatus(repo, { + paths: subsToClose, + }); + const subStats = repoStatus.submodules; + let errorMessage = ""; + const closers = subsToClose.map(co.wrap(function *(name) { const sub = subStats[name]; if (undefined === sub || null === sub.workdir) { From 0a3ffbbb52847116a2ddbcecf75293c36ada7445 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 30 Nov 2017 10:09:41 -0500 Subject: [PATCH 024/402] better error message for failed checkout --- node/lib/util/checkout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index a263d87a9..0d9f7c854 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -411,7 +411,7 @@ A branch named ${colors.red(name)} already exists.`); if (null === result.commit) { throw new UserError( - `Could not resolve ${colors.red(committish)}.`); + `Could not resolve ${colors.red(committish)} as a branch or commit.`); } } else { From cd7e00e962e4cfb40f9f85de1d7be5f2ea902845 Mon Sep 17 00:00:00 2001 From: Shihui Xiang Date: Fri, 1 Dec 2017 14:06:47 -0500 Subject: [PATCH 025/402] 402:improve text in '-i' commit mode --- node/lib/util/commit.js | 3 ++- node/test/util/commit.js | 24 ++++++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 565696e98..567b3c4b4 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -1660,7 +1660,8 @@ exports.formatSplitCommitEditorPrompt = function (status, } result += `\ -# <*> enter meta-repo message above this line; delete to commit only submodules +# <*> enter meta-repo message above this line; delete this line to \ +commit only submodules `; if (metaCommitData) { diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 89ab0733f..3b1979767 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -2515,7 +2515,8 @@ x=S:C2-1 q/r/s=Sa:1;Bmaster=2;Oq/r/s H=a`, }), expected: `\ -# <*> enter meta-repo message above this line; delete to commit only submodules +# <*> enter meta-repo message above this line; delete this line to commit \ +only submodules # On branch master. # # Please enter the commit message(s) for your changes. The message for a @@ -2534,7 +2535,8 @@ x=S:C2-1 q/r/s=Sa:1;Bmaster=2;Oq/r/s H=a`, metaCommitData: new Commit.CommitMetaData(sig, "hiya"), expected: `\ hiya -# <*> enter meta-repo message above this line; delete to commit only submodules +# <*> enter meta-repo message above this line; delete this line to commit \ +only submodules # Date: 12/31/1969, 23:00:03 -100 # # On branch master. @@ -2557,7 +2559,8 @@ hiya }), expected: `\ -# <*> enter meta-repo message above this line; delete to commit only submodules +# <*> enter meta-repo message above this line; delete this line to commit \ +only submodules # On branch foo. # Changes to be committed: # \tnew file: baz @@ -2585,7 +2588,8 @@ hiya }), expected: `\ -# <*> enter meta-repo message above this line; delete to commit only submodules +# <*> enter meta-repo message above this line; delete this line to commit \ +only submodules # On branch foo. # Changes to be committed: # \tmodified: bar (submodule, new commits) @@ -2622,7 +2626,8 @@ hiya }), expected: `\ -# <*> enter meta-repo message above this line; delete to commit only submodules +# <*> enter meta-repo message above this line; delete this line to commit \ +only submodules # On branch foo. # # Please enter the commit message(s) for your changes. The message for a @@ -2657,7 +2662,8 @@ hiya }), expected: `\ -# <*> enter meta-repo message above this line; delete to commit only submodules +# <*> enter meta-repo message above this line; delete this line to commit \ +only submodules # On branch foo. # ----------------------------------------------------------------------------- @@ -2701,7 +2707,8 @@ committing 'bar' }, expected: `\ -# <*> enter meta-repo message above this line; delete to commit only submodules +# <*> enter meta-repo message above this line; delete this line to commit \ +only submodules # On branch foo. # ----------------------------------------------------------------------------- yoyoyo @@ -2750,7 +2757,8 @@ committing 'bar' }, expected: `\ yoyoyo -# <*> enter meta-repo message above this line; delete to commit only submodules +# <*> enter meta-repo message above this line; delete this line to commit \ +only submodules # Date: 12/31/1969, 23:00:03 -100 # # On branch foo. From d4640d509e341bdaae308d5bac4db23e7898d8fe Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Mon, 4 Dec 2017 12:13:41 -0500 Subject: [PATCH 026/402] Fixed problem with cleaning up conflicts. I'm not sure why this bug was not replicated in my tests, but it appears that calling `Index.conflictCleanup` *after* writing the index can cause it to be corrupted, preventing the commit from being written and leaving the repository in a bad state (`git status` complains that there are multiple stage entries). To fix, I've put conflict removals for each path that has been addressed, rather than relying on the bulk cleanup (except in the case of `abort`). --- node/lib/util/merge_util.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 374b063b7..71d4c5508 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -157,8 +157,10 @@ ${colors.green(toSha)}.`); yield NodeGit.Reset.reset(repo, commit, NodeGit.Reset.TYPE.HARD); yield index.addByPath(path); } + yield index.conflictRemove(path); }); + /** * Merge the specified `commit` in the specified `repo` having the specified * `status`, using the specified `mode` to control whether or not a merge @@ -349,6 +351,7 @@ ${colors.red(commitSha)}.`); if (upToDate) { console.log("We're up-to-date with", path); yield index.addByPath(path); + yield index.conflictRemove(path); return; // RETURN } @@ -410,10 +413,10 @@ ${colors.green(subSha)}.`); [subHead, subCommit]); subCommits[path] = mergeCommit.tostrS(); - // And add this sub-repo to the list of sub-repos that need to be added - // to the index later. + // Clean up the conflict for this submodule and stage our change. yield index.addByPath(path); + yield index.conflictRemove(path); }); const entries = index.entries(); @@ -426,6 +429,7 @@ ${colors.green(subSha)}.`); } else { yield index.addByPath(SubmoduleConfigUtil.modulesFileName); + yield index.conflictRemove(SubmoduleConfigUtil.modulesFileName); } } @@ -444,11 +448,6 @@ ${colors.green(subSha)}.`); throw new UserError(errorMessage); } - // If we've made it here, it means there are no "real" conflicts, but we - // need to clean up the index anyway or it won't let us write a tree. - - yield index.conflictCleanup(); - console.log(`Merging meta-repo commit ${colors.green(commitSha)}.`); const id = yield index.writeTreeTo(repo); @@ -572,15 +571,16 @@ exports.continue = co.wrap(function *(repo) { result.submoduleCommits[subPath] = id.tostrS(); } yield index.addByPath(subPath); + yield index.conflictRemove(subPath); }); const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo); yield DoWorkQueue.doInParallel(openSubs, continueSub, 30); + yield index.write(); + if ("" !== errorMessage) { throw new UserError(errorMessage); } - yield index.conflictCleanup(); - yield index.write(); const treeId = yield index.writeTreeTo(repo); const sig = repo.defaultSignature(); From 99df240c0ebf1850e37525daac9d85502fd42d38 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 5 Dec 2017 09:41:35 -0500 Subject: [PATCH 027/402] Forward rev-parse https://github.com/twosigma/git-meta/issues/417 --- node/lib/cmd/forward.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node/lib/cmd/forward.js b/node/lib/cmd/forward.js index 5a54df496..46fe3fd97 100644 --- a/node/lib/cmd/forward.js +++ b/node/lib/cmd/forward.js @@ -76,6 +76,7 @@ exports.forwardedCommands = new Set([ "fetch", "log", "remote", + "rev-parse", "show", "tag", ]); From b096ef1558eb17b15c4658a61548acc1fb0b3e57 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Wed, 13 Dec 2017 12:27:59 -0500 Subject: [PATCH 028/402] Silence submodule push if successful. --- node/lib/util/git_util.js | 14 ++++++++------ node/lib/util/push.js | 2 ++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index d92daaffc..ac2ca0cc3 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -306,7 +306,7 @@ exports.getCurrentRepo = function () { * `target` branch in the specified `remote` repository. Return null if the * push succeeded and string containing an error message if the push failed. * Attempt to allow a non-ffwd push if the specified `force` is `true`. - * Silence console output if the specified `quiet` is provided and is true. + * If `quiet` is true, and push was successful, silence console output. * * @async * @param {NodeGit.Repository} repo @@ -344,11 +344,13 @@ exports.push = co.wrap(function *(repo, remote, source, target, force, quiet) { git -C '${repo.workdir()}' push ${forceStr} ${remote} ${source}:${target}`; try { const result = yield ChildProcess.exec(execString); - if (result.stdout && !quiet) { - console.log(result.stdout); - } - if (result.stderr && !quiet) { - console.error(result.stderr); + if (result.error || !quiet) { + if (result.stdout) { + console.log(result.stdout); + } + if (result.stderr) { + console.error(result.stderr); + } } return null; } diff --git a/node/lib/util/push.js b/node/lib/util/push.js index b4c438386..8344c977a 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -119,11 +119,13 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { // Always force push synthetic refs. It should not be necessary, but // if something does go wrong forcing will allow us to auto-correct. + // If they succeed, no need to print the output inside the submodules. const pushResult = yield GitUtil.push(subRepo, subUrl, sha, syntheticName, + true, true); if (null !== pushResult) { errorMessage += From 85c3c601a98b3444c8ff0222a8794c14cc9a9021 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Wed, 13 Dec 2017 12:35:53 -0500 Subject: [PATCH 029/402] Fix whitespace and typo. --- node/lib/util/git_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index d92daaffc..17fd5c7b3 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -826,7 +826,7 @@ exports.isBlank = function (line) { /** * Return the text contained in the specified array of `lines` after removing * all comment (i.e., those whose first non-whitespace character is a '#') and - * leading and trailing blank (i.e., those containing only whitespce) lines. + * leading and trailing blank (i.e., those containing only whitespace) lines. * * @param {String[]} lines * @return {String} From 6d3e483e70bc62a45cdf9209863a1c30a1b582e0 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 14 Dec 2017 17:48:50 -0500 Subject: [PATCH 030/402] make synthetic referential integrity check work on shas not refs --- node/lib/util/shorthand_parser_util.js | 3 +- node/lib/util/synthetic_branch_util.js | 12 ++--- node/test/util/synthetic-branch.js | 66 +++++++++++++++----------- 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/node/lib/util/shorthand_parser_util.js b/node/lib/util/shorthand_parser_util.js index 1efdbe5e6..594d91ca6 100644 --- a/node/lib/util/shorthand_parser_util.js +++ b/node/lib/util/shorthand_parser_util.js @@ -1136,10 +1136,11 @@ exports.parseMultiRepoShorthand = function (shorthand, existingRepos) { function addCommit(id, commit) { if (id in commits) { const oldCommit = commits[id]; + const msg = `different submodule commit for meta commit ${id}`; RepoASTUtil.assertEqualCommits( commit, oldCommit, - `diffferent duplicate for commit ${id}`); + msg); } else { commits[id] = commit; diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 3cab0b346..c90c0d61f 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -99,7 +99,7 @@ function *urlToLocalPath(repo, url) { } /** - * Check that a synthetic branch exists for a given submodule + * Check that a commit exists exists for a given submodule * at a given commit. * @async * @param {NodeGit.Repostory} repo The meta repository @@ -114,14 +114,14 @@ function* checkSubmodule(repo, metaCommit, submoduleEntry, url) { const localPath = yield *urlToLocalPath(repo, url); const submoduleRepo = yield NodeGit.Repository.open(localPath); const submoduleCommitId = submoduleEntry.id(); - const branch = exports.getSyntheticBranchForCommit(submoduleCommitId); try { const subrepoCommit = - yield submoduleRepo.getReferenceCommit(branch); - return subrepoCommit.id().equal(submoduleEntry.id()); + yield NodeGit.Object.lookup(submoduleRepo, submoduleCommitId, + NodeGit.Object.TYPE.COMMIT); + return subrepoCommit !== null; } catch (e) { - console.error("Could not look up ", branch, " in ", localPath, - ": ", e); + console.error("Could not look up ", submoduleCommitId, " in ", + localPath, ": ", e); return false; } } diff --git a/node/test/util/synthetic-branch.js b/node/test/util/synthetic-branch.js index c22ccda28..0f2dff25c 100644 --- a/node/test/util/synthetic-branch.js +++ b/node/test/util/synthetic-branch.js @@ -59,10 +59,6 @@ describe("synthetic-branch", function () { value. */ } const headId = head.id().toString(); - SyntheticBranch.getSyntheticBranchForCommit = function(commit) { - /*jshint unused:false*/ - return "refs/heads/metaTEST"; - }; const pass = yield SyntheticBranch.checkUpdate(x, old, headId, {}); if (!pass) { throw new UserError("fail"); @@ -100,67 +96,79 @@ describe("synthetic-branch", function () { "N refs/notes/git-meta/subrepo-check 4=ok|" + "z=S:C5-1;Bmaster=5", }, + //in these tests, we point meta's y to a commit which doesn't exist + //inside y's repo -- to do this, we use an otherwise unused submodule + //called 'u'. "with a submodule but no synthetic branch": { - input: "x=S:C2-1;C3-2 y=S/y:4;Bmaster=3|y=S:C4-1;Bmaster=4", - expected: "x=S:C2-1;C3-2 y=S/y:4;Bmaster=3|" + - "y=S:C4-1;Bmaster=4", + input: "x=S:C2-1;C3-2 y=S/y:7;Bmaster=3|y=S:C4-1;Bmaster=4" + + "|u=S:C7-1;Bmaster=7", + expected: "x=S:C2-1;C3-2 y=S/y:7;Bmaster=3|y=S:C4-1;Bmaster=4" + + "|u=S:C7-1;Bmaster=7", fails: true }, "with a submodule in a subdir but no synthetic branch": { - input: "x=S:C2-1;C3-2 y/z=S/z:4;Bmaster=3|z=S:C4-1;Bmaster=4", - expected: "x=S:C2-1;C3-2 y/z=S/z:4;Bmaster=3|" + - "z=S:C4-1;Bmaster=4", + input: "x=S:C2-1;C3-2 y/z=S/z:7;Bmaster=3|z=S:C4-1;Bmaster=4" + + "|u=S:C7-1;Bmaster=7", + expected: "x=S:C2-1;C3-2 y/z=S/z:7;Bmaster=3|z=S:C4-1;Bmaster=4" + + "|u=S:C7-1;Bmaster=7", fails: true }, "with a submodule in a subdir, bad parent commit": { - input: "x=S:C2-1;C3-2 y/z=S/z:5;C4-3;Bmaster=4|z=S:C5-1;Bmaster=5", - expected: "x=S:C2-1;C3-2 y/z=S/z:5;C4-3;Bmaster=4|" + - "z=S:C5-1;Bmaster=5", + input: "x=S:C2-1;C3-2 y/z=S/z:7;C4-3;Bmaster=4" + + "|z=S:C5-1;Bmaster=5" + + "|u=S:C7-1;Bmaster=7", + expected: "x=S:C2-1;C3-2 y/z=S/z:7;C4-3;Bmaster=4" + + "|z=S:C5-1;Bmaster=5" + + "|u=S:C7-1;Bmaster=7", fails: true }, "with a submodule in a subdir, bad merge commit": { - input: "x=S:C2-1;C3-2 y/z=S/z:5;C4-3,1;Bmaster=4|" + - "z=S:C5-1;Bmaster=5", - expected: "x=S:C2-1;C3-2 y/z=S/z:5;C4-3,1;Bmaster=4|" + - "z=S:C5-1;Bmaster=5", + input: "x=S:C2-1;C3-2 y/z=S/z:7;C4-3,1;Bmaster=4" + + "|z=S:C5-1;Bmaster=5" + + "|u=S:C7-1;Bmaster=7", + expected: "x=S:C2-1;C3-2 y/z=S/z:7;C4-3,1;Bmaster=4" + + "|z=S:C5-1;Bmaster=5" + + "|u=S:C7-1;Bmaster=7", fails: true }, "with a submodule, at meta commit": { input: "x=S:C2-1;C3-2 y=S/y:4;Bmaster=3|" + - "y=S:C4-1;Bmaster=4;BmetaTEST=4", + "y=S:C4-1;Bmaster=4", expected: "x=S:C2-1;C3-2 y=S/y:4;Bmaster=3;" + "N refs/notes/git-meta/subrepo-check 3=ok|" + - "y=S:C4-1;Bmaster=4;BmetaTEST=4", + "y=S:C4-1;Bmaster=4", }, "with a change to a submodule which was deleted on master": { - input: "x=S:C2-1 s=S/s:1;C3-2 s;Bmaster=3;C4-2 s=S/s:8" + - ";BmetaTEST=4" + + input: "x=S:C2-1 s=S/s:9;C3-2 s;Bmaster=3;C4-2 s=S/s:8" + + ";Btombstone=4" + + "|u=S:C9-1;Bmaster=9" + "|s=S:C8-7;C7-1;Bmaster=8", - expected: "x=S:C2-1 s=S/s:1;C3-2 s;Bmaster=3;C4-2 s=S/s:8" + - ";BmetaTEST=4" + + expected: "x=S:C2-1 s=S/s:9;C3-2 s;Bmaster=3;C4-2 s=S/s:8" + + ";Btombstone=4" + + "|u=S:C9-1;Bmaster=9" + "|s=S:C8-7;C7-1;Bmaster=8", fails: true }, "with a submodule in a subdir, at meta commit": { input: "x=S:C2-1;C3-2 y/z=S/z:4;Bmaster=3|" + - "z=S:C4-1;Bmaster=4;BmetaTEST=4", + "z=S:C4-1;Bmaster=4", expected: "x=S:C2-1;C3-2 y/z=S/z:4;Bmaster=3;" + "N refs/notes/git-meta/subrepo-check 3=ok|" + - "z=S:C4-1;Bmaster=4;BmetaTEST=4", + "z=S:C4-1;Bmaster=4", }, "with a submodule in a subdir, from earlier meta-commit": { input: "x=S:C2-1 y/z=S/z:4;C3-2;Bmaster=3|" + - "z=S:C4-1;Bmaster=4;BmetaTEST=4", + "z=S:C4-1;Bmaster=4", expected: "x=S:C2-1 y/z=S/z:4;C3-2;Bmaster=3;" + "N refs/notes/git-meta/subrepo-check 3=ok|" + - "z=S:C4-1;Bmaster=4;BmetaTEST=4", + "z=S:C4-1;Bmaster=4", }, "with a submodule in a subdir, irrelevant change": { input: "x=S:C2-1 y/z=S/z:4;C3-2 y/foo=bar;Bmaster=3|" + - "z=S:C4-1;Bmaster=4;BmetaTEST=4", + "z=S:C4-1;Bmaster=4", expected: "x=S:C2-1 y/z=S/z:4;C3-2 y/foo=bar;Bmaster=3;" + "N refs/notes/git-meta/subrepo-check 3=ok|" + - "z=S:C4-1;Bmaster=4;BmetaTEST=4", + "z=S:C4-1;Bmaster=4", }, }; From a45c918c714a9788f9dffea41c9f49558fea24d8 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 28 Dec 2017 09:48:07 -0500 Subject: [PATCH 031/402] Fix/improve error with branch importing submodule There was a bug in that it showed 'undefined' instead of the branch name; also, will now suggest using the '-b [BRANCH]' option if the branch is bad. addresses https://github.com/twosigma/git-meta/issues/443 --- node/lib/util/add_submodule.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node/lib/util/add_submodule.js b/node/lib/util/add_submodule.js index dffe69530..e4a543e25 100644 --- a/node/lib/util/add_submodule.js +++ b/node/lib/util/add_submodule.js @@ -93,7 +93,8 @@ exports.addSubmodule = co.wrap(function *(repo, url, filename, importArg) { importArg.branch); if (null === remoteBranch) { throw new UserError(` -The requested branch: ${colors.red(importArg.ranch)} does not exist.`); +The requested branch: ${colors.red(importArg.branch)} does not exist; \ +try '-b [BRANCH]' to specify a different branch.`); } const commit = yield subRepo.getCommit(remoteBranch.target()); yield GitUtil.setHeadHard(subRepo, commit); From 6dc0e3cc1a0cc92fc82fb58835ee2f34a8985b77 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 26 Dec 2017 14:00:52 -0500 Subject: [PATCH 032/402] Forward all non-meta commands to Git. Instead of forwarding just a whitelist, forward everything not provided by git-meta and not in a blacklist. addresses https://github.com/twosigma/git-meta/issues/451 --- node/lib/cmd/forward.js | 13 ------------ node/lib/git-meta.js | 44 +++++++++++++++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/node/lib/cmd/forward.js b/node/lib/cmd/forward.js index 46fe3fd97..d6c92ba72 100644 --- a/node/lib/cmd/forward.js +++ b/node/lib/cmd/forward.js @@ -68,19 +68,6 @@ ${helpText} See 'git ${name} --help' for more information.`, }; }; -/** - * @property {Set} set of commands to forward - */ -exports.forwardedCommands = new Set([ - "branch", - "fetch", - "log", - "remote", - "rev-parse", - "show", - "tag", -]); - /** * Forward the specified `args` to the Git command having the specified `name`. * diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js index 5a6bfb776..a2e62ecad 100755 --- a/node/lib/git-meta.js +++ b/node/lib/git-meta.js @@ -109,7 +109,9 @@ commands will generally perform that same operation, but across a *meta* repository and the *sub* repositories that are locally *opened*. These commands work on any Git repository (even one without configured submodules); we do not provide duplicate commands for Git functionality that does not need -to be applied across sub-modules such as 'clone' and 'init'.`; +to be applied across sub-modules such as 'clone' and 'init'. Note that +git-meta will forward any subcommand that it does not implement to Git, +as if run with 'git -C $(git meta root) ...'.`; const parser = new ArgumentParser({ addHelp:true, @@ -141,12 +143,6 @@ const commands = { "version": version, }; -// Configure forwarded commands. - -Array.from(Forward.forwardedCommands).forEach(name => { - commands[name] = Forward.makeModule(name); -}); - // Configure the parser with commands in alphabetical order. Object.keys(commands).sort().forEach(name => { @@ -154,13 +150,45 @@ Object.keys(commands).sort().forEach(name => { configureSubcommand(subParser, name, cmd); }); +const blacklist = new Set([ + "--help", + "--version", + "-h", + "am", + "annotate", + "archimport", + "archive", + "blame", + "clean", + "cvsexportcommit", + "cvsimport", + "cvsserver", + "fast-export", + "fast-import", + "filter-branch", + "grep", + "merge-file", + "merge-index", + "merge-tree", + "mv", + "p4", + "quiltimport", + "revert", + "rm", + "shell", + "stage", + "svn", + "worktree", +]); + // If the first argument matches a forwarded sub-command, handle it manually. // I was not able to get ArgParse to allow unknown flags, e.g. // `git meta branch -r` to be passed to the REMAINDER positional argument on a // sub-parser level. if (2 < process.argv.length && - Forward.forwardedCommands.has(process.argv[2])) { + !blacklist.has(process.argv[2]) && + !(process.argv[2] in commands)) { const name = process.argv[2]; const args = process.argv.slice(3); Forward.execute(name, args).catch(() => { From 677fbcaea2c50733074d47f2c3ee385d789bca0e Mon Sep 17 00:00:00 2001 From: Shihui Xiang Date: Wed, 20 Dec 2017 16:10:06 -0500 Subject: [PATCH 033/402] git-meta add support '-u' flag add unit tests --- node/lib/cmd/add.js | 10 +++++++++- node/lib/util/add.js | 16 +++++++++++++--- node/test/util/add.js | 18 +++++++++++++++++- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/node/lib/cmd/add.js b/node/lib/cmd/add.js index 7aeec8df2..c450d6408 100644 --- a/node/lib/cmd/add.js +++ b/node/lib/cmd/add.js @@ -60,6 +60,14 @@ to submodules with new commits in their working directories. `; exports.configureParser = function (parser) { + parser.addArgument(["-u", "--update"], { + required: false, + action: "storeConst", + constant: true, + help: "Update tracked files.", + defaultValue:false + }); + parser.addArgument(["--meta"], { required: false, action: "storeConst", @@ -94,5 +102,5 @@ exports.executeableSubcommand = co.wrap(function *(args) { const paths = yield args.paths.map(filename => { return GitUtil.resolveRelativePath(workdir, cwd, filename); }); - yield Add.stagePaths(repo, paths, args.meta); + yield Add.stagePaths(repo, paths, args.meta, args.update); }); diff --git a/node/lib/util/add.js b/node/lib/util/add.js index d473dafb4..c5a11650b 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -34,6 +34,7 @@ const assert = require("chai").assert; const co = require("co"); const NodeGit = require("nodegit"); +const RepoStatus = require("./repo_status"); const StatusUtil = require("./status_util"); const SubmoduleUtil = require("./submodule_util"); @@ -49,10 +50,11 @@ const SubmoduleUtil = require("./submodule_util"); * @param {NodeGit.Repository} repo * @param {String []} paths */ -exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges) { +exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update) { assert.instanceOf(repo, NodeGit.Repository); assert.isArray(paths); assert.isBoolean(stageMetaChanges); + assert.isBoolean(update); const repoStatus = yield StatusUtil.getRepoStatus(repo, { showMetaChanges: stageMetaChanges, @@ -69,8 +71,16 @@ exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges) { const subRepo = yield SubmoduleUtil.getRepo(repo, name); const workdir = subStat.workdir.status.workdir; const index = yield subRepo.index(); - yield Object.keys(workdir).map( - filename => index.addByPath(filename)); + yield Object.keys(workdir).map(filename => { + // if -u flag is provided, update tracked files only. + if (update) { + if (RepoStatus.FILESTATUS.ADDED !== workdir[filename]) { + return index.addByPath(filename); + } + } else { + return index.addByPath(filename); + } + }); yield index.write(); } })); diff --git a/node/test/util/add.js b/node/test/util/add.js index 89b2a0ff1..ff2cb3964 100644 --- a/node/test/util/add.js +++ b/node/test/util/add.js @@ -129,17 +129,33 @@ a=B|x=S:C2-1 a/b=Sa:1;Oa/b W x/y/z=a,x/r/z=b;Bmaster=2`, paths: [], stageMeta: false, }, + "add only tracked files": { + initial: "a=B|x=U:Os W README.md=foo,newFile=a", + paths: ["s"], + expected: "x=E:Os I README.md=foo!W newFile=a", + update: true, + }, + "add all(tracked and not tracked) files": { + initial: "a=B|x=U:Os W README.md=foo,newFile=a", + paths: ["s"], + expected: "x=E:Os I README.md=foo,newFile=a", + update: false, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; it(caseName, co.wrap(function *() { let stageMeta = c.stageMeta; + let update = c.update; if (undefined === stageMeta) { stageMeta = true; } + if (undefined === update) { + update = false; + } const doAdd = co.wrap(function *(repos) { const repo = repos.x; - yield Add.stagePaths(repo, c.paths, stageMeta); + yield Add.stagePaths(repo, c.paths, stageMeta, update); }); yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, c.expected, From b7083aef0bfcefcf38c958d43d662fea08dcc056 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 2 Feb 2018 09:19:21 -0500 Subject: [PATCH 034/402] Fetch and update at most 30 subs at once. With many (>1,000) submodules open we exhast the JS heap. --- node/lib/util/rebase_util.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index 6e5f77ad7..c8ec60711 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -38,6 +38,7 @@ const path = require("path"); const rimraf = require("rimraf"); const DeinitUtil = require("./deinit_util"); +const DoWorkQueue = require("../util/do_work_queue"); const Open = require("./open"); const GitUtil = require("./git_util"); const RepoStatus = require("./repo_status"); @@ -461,7 +462,7 @@ const driveRebase = co.wrap(function *(metaRepo, const shas = yield SubmoduleUtil.getSubmoduleShasForCommit(metaRepo, openSubs, ontoCommit); - yield openSubs.map(co.wrap(function *(name) { + const fetchOpened = co.wrap(function *(name) { const subRepo = yield opener.getSubrepo(name); const head = yield subRepo.head(); const sha = shas[name]; @@ -469,7 +470,8 @@ const driveRebase = co.wrap(function *(metaRepo, yield fetcher.fetchSha(subRepo, name, sha); yield setHead(subRepo, sha); } - })); + }); + yield DoWorkQueue.doInParallel(openSubs, fetchOpened, 30); } yield callFinish(metaRepo, rebase); From c149d1c7ec02b34a27ce852ec2edabae4ea5db7c Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 2 Feb 2018 14:40:55 -0500 Subject: [PATCH 035/402] allow push of arbitrary commit-ishes instead of just branches --- node/lib/util/push.js | 3 ++- node/lib/util/submodule_util.js | 11 ++++++----- node/test/util/submodule_util.js | 6 +++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 8344c977a..2582aa3d2 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -93,7 +93,8 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { // First, push the submodules. let errorMessage = ""; - const shas = yield SubmoduleUtil.getSubmoduleShasForBranch(repo, source); + const shas = yield SubmoduleUtil.getSubmoduleShasForCommitish(repo, + source); const annotatedCommit = yield GitUtil.resolveCommitish(repo, source); const commit = yield repo.getCommit(annotatedCommit.id()); const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 3243d2595..8937e1cef 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -152,18 +152,19 @@ exports.getSubmoduleShasForCommit = /** * Return a map from submodule name to string representing the expected sha1 - * for its repository in the specified `repo` on the branch having the - * specified `branchName`. + * for its repository in the specified `repo` on the specified `commitish`. * * @async * @param {NodeGit.Repository} repo * @param {String} branchName * @return {Object} */ -exports.getSubmoduleShasForBranch = co.wrap(function *(repo, branchName) { +exports.getSubmoduleShasForCommitish = co.wrap(function *(repo, commitish) { assert.instanceOf(repo, NodeGit.Repository); - assert.isString(branchName); - const commit = yield repo.getBranchCommit(branchName); + assert.isString(commitish); + const annotated = yield NodeGit.AnnotatedCommit.fromRevspec(repo, + commitish); + const commit = yield NodeGit.Commit.lookup(repo, annotated.id()); const submoduleNames = yield exports.getSubmoduleNamesForCommit(repo, commit); diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index 11c4ab978..253142210 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -106,7 +106,7 @@ describe("SubmoduleUtil", function () { }); }); - describe("getSubmoduleNamesForBranch", function () { + describe("getSubmoduleNamesForCommittish", function () { // This method is implemented in terms of `getSubmoduleNamesForCommit`; // we just need to do basic verification. @@ -194,7 +194,7 @@ describe("SubmoduleUtil", function () { }); }); - describe("getSubmoduleShasForBranch", function () { + describe("getSubmoduleShasForCommitish", function () { // The implementation of this method is delegated to // `getSubmoduleShasForCommit`; just exercise basic functionality. @@ -203,7 +203,7 @@ describe("SubmoduleUtil", function () { yield RepoASTTestUtil.createRepo("S:C2-1 x=Sa:1;Bm=2"); const repo = written.repo; const result = - yield SubmoduleUtil.getSubmoduleShasForBranch(repo, "m"); + yield SubmoduleUtil.getSubmoduleShasForCommitish(repo, "m"); assert.equal(written.commitMap[result.x], "1"); })); }); From fde7e4fa00f2d3d5472b06ab0bc91109d4a195d5 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 2 Feb 2018 18:23:55 -0500 Subject: [PATCH 036/402] Throttle number of submodule resets to 30 --- node/lib/util/reset.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index 0ee46cc88..b75c162e2 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -34,6 +34,7 @@ const assert = require("chai").assert; const co = require("co"); const NodeGit = require("nodegit"); +const DoWorkQueue = require("../util/do_work_queue"); const GitUtil = require("./git_util"); const Open = require("./open"); const StatusUtil = require("./status_util"); @@ -114,7 +115,7 @@ exports.reset = co.wrap(function *(repo, commit, type) { pathsToReset, commit); const index = yield repo.index(); - yield pathsToReset.map(co.wrap(function *(name) { + const resetSubmodule = co.wrap(function *(name) { const change = changedSubs[name]; if (undefined !== change && (null === change.oldSha || null === change.newSha)) { @@ -145,8 +146,8 @@ exports.reset = co.wrap(function *(repo, commit, type) { // for the submodule. yield index.addByPath(name); - })); - + }); + yield DoWorkQueue.doInParallel(pathsToReset, resetSubmodule, 30); // Write the index in case we've had to stage submodule changes. yield index.write(); From edc94762fcedb415439e4883ae1f4ab8ab7b2602 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 2 Feb 2018 18:30:30 -0500 Subject: [PATCH 037/402] Throttle fetches during checkout --- node/lib/util/checkout.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 0d9f7c854..14b2abd0e 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -38,6 +38,7 @@ const co = require("co"); const colors = require("colors"); const NodeGit = require("nodegit"); +const DoWorkQueue = require("../util/do_work_queue"); const GitUtil = require("./git_util"); const SubmoduleFetcher = require("./submodule_fetcher"); const RepoStatus = require("./repo_status"); @@ -101,13 +102,14 @@ const loadSubmodulesToCheckout = co.wrap(function *(repo, commit) { const result = {}; const subFetcher = new SubmoduleFetcher(repo, commit); - yield open.map(co.wrap(function *(name) { + const doSub = co.wrap(function *(name) { const subRepo = yield SubmoduleUtil.getRepo(repo, name); const sha = shas[name]; yield subFetcher.fetchSha(subRepo, name, sha); const commit = yield subRepo.getCommit(sha); result[name] = { repo: subRepo, commit: commit }; - })); + }); + yield DoWorkQueue.doInParallel(open, doSub); return result; }); From c257788ce8f86b5f12175032cee8f6113d4577f9 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 2 Feb 2018 19:20:31 -0500 Subject: [PATCH 038/402] Throttle fetches during commit --amend also, removed hard-coded "30"s in other places --- node/lib/cmd/open.js | 2 +- node/lib/util/commit.js | 7 ++++--- node/lib/util/merge_util.js | 8 ++++---- node/lib/util/rebase_util.js | 2 +- node/lib/util/reset.js | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 2ea59b575..d4d897ec2 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -136,7 +136,7 @@ Opening ${colors.blue(name)} on ${colors.green(shas[index])}.`); } console.log(`Finished opening ${colors.blue(name)}.`); }); - yield DoWorkQueue.doInParallel(subsToOpen, opener, 30); + yield DoWorkQueue.doInParallel(subsToOpen, opener); if (failed) { process.exit(1); diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 567b3c4b4..ef7346c9f 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -43,6 +43,7 @@ const colors = require("colors"); const NodeGit = require("nodegit"); const path = require("path"); +const DoWorkQueue = require("../util/do_work_queue"); const DiffUtil = require("./diff_util"); const GitUtil = require("./git_util"); const Open = require("./open"); @@ -888,7 +889,7 @@ exports.getAmendStatus = co.wrap(function *(repo, options) { const subsToInspect = Array.from(new Set( Object.keys(submodules).concat(Object.keys(changes)))); - yield subsToInspect.map(co.wrap(function *(name) { + const inspectSub = co.wrap(function *(name) { const change = changes[name]; let currentSub = submodules[name]; let old = null; @@ -956,8 +957,8 @@ exports.getAmendStatus = co.wrap(function *(repo, options) { if (null !== result.oldCommit) { subsToAmend[name] = result.oldCommit; } - })); - + }); + yield DoWorkQueue.doInParallel(subsToInspect, inspectSub); // Look for subs that were removed in the commit we are amending; reflect // their status. diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 71d4c5508..1ca2486f1 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -420,7 +420,7 @@ ${colors.green(subSha)}.`); }); const entries = index.entries(); - yield DoWorkQueue.doInParallel(entries, mergeEntry, 30); + yield DoWorkQueue.doInParallel(entries, mergeEntry); if (hasModulesFile) { const good = yield SubmoduleUtil.mergeModulesFile(repo, head, commit); @@ -472,7 +472,7 @@ ${colors.green(subSha)}.`); } }); const opened = Array.from(yield opener.getOpenedSubs()); - DoWorkQueue.doInParallel(opened, closeSub, 30); + DoWorkQueue.doInParallel(opened, closeSub); return result; }); @@ -574,7 +574,7 @@ exports.continue = co.wrap(function *(repo) { yield index.conflictRemove(subPath); }); const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo); - yield DoWorkQueue.doInParallel(openSubs, continueSub, 30); + yield DoWorkQueue.doInParallel(openSubs, continueSub); yield index.write(); @@ -646,7 +646,7 @@ exports.abort = co.wrap(function *(repo) { yield MergeFileUtil.cleanMerge(subRepo.path()); yield index.addByPath(subName); }); - yield DoWorkQueue.doInParallel(openSubs, abortSub, 30); + yield DoWorkQueue.doInParallel(openSubs, abortSub); yield index.conflictCleanup(); yield index.write(); yield resetMerge(repo); diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index c8ec60711..2385f4e2e 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -471,7 +471,7 @@ const driveRebase = co.wrap(function *(metaRepo, yield setHead(subRepo, sha); } }); - yield DoWorkQueue.doInParallel(openSubs, fetchOpened, 30); + yield DoWorkQueue.doInParallel(openSubs, fetchOpened); } yield callFinish(metaRepo, rebase); diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index b75c162e2..a6527e8a2 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -147,7 +147,7 @@ exports.reset = co.wrap(function *(repo, commit, type) { yield index.addByPath(name); }); - yield DoWorkQueue.doInParallel(pathsToReset, resetSubmodule, 30); + yield DoWorkQueue.doInParallel(pathsToReset, resetSubmodule); // Write the index in case we've had to stage submodule changes. yield index.write(); From 1ad092b62f32a5c5a4b4d9b9ed82093644362423 Mon Sep 17 00:00:00 2001 From: "David \"novalis\" Turner" Date: Mon, 5 Feb 2018 17:11:53 -0500 Subject: [PATCH 039/402] support push-to-url (#468) --- node/lib/util/git_util.js | 25 +++++++++++++++++++++++++ node/lib/util/push.js | 9 +-------- node/test/util/git_util.js | 12 ++++++++++++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 7ce78105a..551176135 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -911,3 +911,28 @@ exports.configIsTrue = co.wrap(function*(repo, configVar) { return (configured === "true" || configured === "yes" || configured === "on"); }); + +/** + * Returns the URL for the specified remote. If the remote is already + * a URL, it is returned unmodified. + * @async + * @param {NodeGit.Repository} repo + * @param {String} remoteName + * @return String + * @throws if there's no such named remote +*/ +exports.getUrlFromRemoteName = co.wrap(function *(repo, remoteName) { + if (remoteName.startsWith("http:") || remoteName.startsWith("https:") || + remoteName.includes("@")) { + return remoteName; + } else { + let remote; + try { + remote = yield repo.getRemote(remoteName); + } + catch (e) { + throw new UserError(`No remote named ${colors.red(remoteName)}.`); + } + return yield exports.getRemoteUrl(repo, remote); + } +}); diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 2582aa3d2..7fa4fbb06 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -81,14 +81,7 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { assert.isString(target); assert.isBoolean(force); - let remote; - try { - remote = yield repo.getRemote(remoteName); - } - catch (e) { - throw new UserError(`No remote named ${colors.red(remoteName)}.`); - } - const remoteUrl = yield GitUtil.getRemoteUrl(repo, remote); + let remoteUrl = yield GitUtil.getUrlFromRemoteName(repo, remoteName); // First, push the submodules. diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index e265eb860..f07300c1b 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -199,6 +199,18 @@ describe("GitUtil", function () { }); }); + describe("getUrlFromRemoteName", function () { + it("works", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const func = GitUtil.getUrlFromRemoteName; + yield NodeGit.Remote.create(repo, "upstream", + "https://example.com"); + assert.equal("https://example.com", yield func(repo, "upstream")); + assert.equal("https://example.org", + yield func(repo, "https://example.org")); + })); + }); + describe("getOriginUrl", function () { it("url from branch remote", co.wrap(function *() { // TODO: don't have it in my shorthand to associate a branch with From cf4db9eea3f98834c62e146a16cd95a09b2eb715 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 8 Feb 2018 15:16:03 -0500 Subject: [PATCH 040/402] Fixed bug where reverted files were not committed https://github.com/twosigma/git-meta/issues/473 When doing an amend, we generate a special 'RepoStatus' object that describes what the resulting commit will actually look like. This allows us to display the right thing in the messsage prompt, and to know when no change is being made (i.e., it's a reversion). However, we were also staging exactly the files flagged as "staged" in the status object. Since reverted files are missing from this object, they were not being staged, and thus were ignored when they shouldn't have been -- in 'all' mode. I changed the logic to just stage all files when in 'all' mode. --- node/lib/util/commit.js | 5 ++++- node/test/util/commit.js | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index ef7346c9f..81a1ab806 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -1107,7 +1107,10 @@ exports.amendMetaRepo = co.wrap(function *(repo, } const subIndex = yield subRepo.index(); - yield stageFiles(subRepo, staged, subIndex); + if (all) { + yield subIndex.addAll(["*"], -1); + yield subIndex.write(); + } subCommits[subName] = yield exports.amendRepo(subRepo, subMessage); return; // RETURN } diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 3b1979767..aab7e4c03 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -1489,6 +1489,14 @@ a=B:Ca-1;Bx=a|x=U:C3-2 s=Sa:a;Bmaster=3;Os;I foo=moo`, expected: ` x=U:Chi\n#x-2 foo=moo,s=Sa:s;Bmaster=x;Os Cmeh\n#s-1 a=a!H=s`, }, + "one reverted, one not": { + input: ` +a=B:Cb-1 a=1,b=2;Cc-b a=2;Bc=c| +x=U:C3-2 s=Sa:b;C4-3 s=Sa:c;Bmaster=4;Os W a=1,b=1`, + all: true, + expected: ` +x=U:C3-2 s=Sa:b;Cx-3 s=Sa:s;Bmaster=x;Os Cs-b b=1`, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From fbba03f73ec053369b54ab6872ba032e1cf91c30 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 9 Feb 2018 16:57:56 -0500 Subject: [PATCH 041/402] Better error messages for mismatched amend commits Addresses: https://github.com/twosigma/git-meta/issues/461 --- node/lib/util/commit.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 81a1ab806..2abc191cf 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -2050,14 +2050,16 @@ exports.doAmendCommand = co.wrap(function *(repo, }); if (0 !== mismatched.length) { let error = `\ -Cannot amend because the signatures of the affected commits in the -following sub-repos do not match that of the meta-repo: +The last meta-repo commit (message or author) +does not match that of the last commit in the following sub-repos: `; mismatched.forEach(name => { error += ` ${colors.red(name)}\n`; }); error += `\ -You can make this commit using the interactive ('-i') commit option.`; +To prevent errors, you must make this commit using the interactive ('-i') +option, which will allow you to see and edit the commit messages for each +repository independently.`; throw new UserError(error); } From 4d0e2810efe8c2c3074d24ef67be5287913eb475 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 9 Feb 2018 17:26:09 -0500 Subject: [PATCH 042/402] Make add work with deleted paths Addresses: https://github.com/twosigma/git-meta/issues/441 --- node/lib/util/add.js | 4 +++- node/test/util/add.js | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/node/lib/util/add.js b/node/lib/util/add.js index c5a11650b..0374d869a 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -73,7 +73,9 @@ exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update) { const index = yield subRepo.index(); yield Object.keys(workdir).map(filename => { // if -u flag is provided, update tracked files only. - if (update) { + if (RepoStatus.FILESTATUS.REMOVED === workdir[filename]) { + return index.removeByPath(filename); + } else if (update) { if (RepoStatus.FILESTATUS.ADDED !== workdir[filename]) { return index.addByPath(filename); } diff --git a/node/test/util/add.js b/node/test/util/add.js index ff2cb3964..6df2eb6ac 100644 --- a/node/test/util/add.js +++ b/node/test/util/add.js @@ -141,6 +141,11 @@ a=B|x=S:C2-1 a/b=Sa:1;Oa/b W x/y/z=a,x/r/z=b;Bmaster=2`, expected: "x=E:Os I README.md=foo,newFile=a", update: false, }, + "deleted": { + initial: "a=B|x=U:Os W README.md", + expected: "x=U:Os I README.md", + paths: [""], + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From 18490dcd169dfc4f67c1d76f4502fede003e1db4 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 9 Feb 2018 16:21:33 -0500 Subject: [PATCH 043/402] Refuse to generate empty amend commits. Addresses https://github.com/twosigma/git-meta/issues/463 Had to fix a few test drivers that were relying on the ability to generate empty commits. --- node/lib/util/commit.js | 17 +++++++++++++-- node/test/util/commit.js | 45 ++++++++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 81a1ab806..be5e20f48 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -1987,7 +1987,8 @@ exports.doCommitCommand = co.wrap(function *(repo, * commit messages for each changed submodules. Use the specified * `editMessage` function to invoke an editor when needed. If * `null === editMessage`, use the message of the previous commit. The - * behavior is undefined if `null !== message && true === interactive`. + * behavior is undefined if `null !== message && true === interactive`. Do not + * generate a commit if it would be empty. * * @param {NodeGit.Repository} repo * @param {String} cwd @@ -1996,7 +1997,7 @@ exports.doCommitCommand = co.wrap(function *(repo, * @param {Boolean} interactive * @param {(repo, txt) -> Promise(String) | null} editMessage * @return {Object} - * @return {String} return.metaCommit + * @return {String|null} return.metaCommit * @return {Object} return.submoduleCommits map from sub name to commit id */ exports.doAmendCommand = co.wrap(function *(repo, @@ -2084,6 +2085,18 @@ You can make this commit using the interactive ('-i') commit option.`; abortForNoMessage(); } + if (!exports.shouldCommit(status, + null === message, + subMessages || undefined)) { + process.stdout.write(PrintStatusUtil.printRepoStatus(status, relCwd)); + process.stdout.write(` +You asked to amend the most recent commit, but doing so would make +it empty. You can remove the commit entirely with "git meta reset HEAD^".`); + return { + metaCommit: null, + submoduleCommits: {} + }; + } // Finally, perform the operation. return yield exports.amendMetaRepo(repo, diff --git a/node/test/util/commit.js b/node/test/util/commit.js index aab7e4c03..4f7c4b580 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -3059,25 +3059,32 @@ x=U:Chola\n#x-2 s=Sa:s;Bmaster=x;Os Cthere\n#s-1 a=a!H=s`, }, "simple amend": { - initial: "x=S:C2-1 README.md=foo;Bmaster=2", + initial: ` +a=B:Ca-1 README.md=foo;Bmaster=a| +x=U:C3-2 s=Sa:a;Bmaster=3;Os`, message: "foo", - expected: "x=S:Cfoo\n#x-1 README.md=foo;Bmaster=x", + expected: ` +x=U:Cfoo\n#x-2 s=Sa:s;Bmaster=x;Os Cfoo\n#s-1 README.md=foo` }, - "amend with change": { - initial: "x=S:C2-1 README.md=foo;Bmaster=2;I a=b", + "would be empty": { + initial: "x=S:C2-1 foo=bar;C3-2 foo=foo;Bmaster=3;W foo=bar", message: "foo", - expected: "x=S:Cfoo\n#x-1 README.md=foo,a=b;Bmaster=x", }, - "amend with no all": { - initial: "x=S:C2-1;Bmaster=2;W README.md=8", + "amend with change": { + initial: ` +a=B:Ca-1 README.md=foo;Bmaster=a| +x=U:C3-2 s=Sa:a;Bmaster=3;Os I a=b`, message: "foo", - expected: "x=S:Cfoo\n#x-1 2=2;Bmaster=x;W README.md=8", + expected: ` +x=U:Cfoo\n#x-2 s=Sa:s;Bmaster=x;Os Cfoo\n#s-1 README.md=foo,a=b` }, - "amend with all but no meta": { - initial: "x=S:C2-1;Bmaster=2;W README.md=8", + "amend with no all": { + initial: ` +a=B:Ca-1 README.md=foo;Bmaster=a| +x=U:C3-2 s=Sa:a;Bmaster=3;Os W README.md=8`, message: "foo", - all: true, - expected: "x=S:Cfoo\n#x-1 2=2;Bmaster=x;W README.md=8", + expected: ` +x=U:Cfoo\n#x-2 s=Sa:s;Bmaster=x;Os Cfoo\n#s-1 README.md=foo!W README.md=8` }, "mismatch": { initial: ` @@ -3100,9 +3107,12 @@ there x=U:Chola\n#x-2 s=Sa:s;Bmaster=x;Os Cthere\n#s-1 a=a!H=s`, }, "simple amend with editor": { - initial: "x=S:C2-1 README.md=foo;Bmaster=2", + initial: ` +a=B:Ca-1 README.md=foo;Bmaster=a| +x=U:C3-2 s=Sa:a;Bmaster=3;Os`, editor: () => Promise.resolve("heya"), - expected: "x=S:Cheya\n#x-1 README.md=foo;Bmaster=x", + expected: ` +x=U:Cheya\n#x-2 s=Sa:s;Bmaster=x;Os Cheya\n#s-1 README.md=foo` }, "simple amend with editor, no message": { initial: "x=S:C2-1 README.md=foo;Bmaster=2", @@ -3110,8 +3120,11 @@ x=U:Chola\n#x-2 s=Sa:s;Bmaster=x;Os Cthere\n#s-1 a=a!H=s`, fails: true, }, "reuse old message": { - initial: "x=S:Cbar\n#2-1 README.md=foo;I a=b;Bmaster=2", - expected: "x=S:Cbar\n#x-1 README.md=foo, a=b;Bmaster=x", + initial: ` +a=B:Cbar\n#a-1 README.md=foo;Bmaster=a| +x=U:Cbar\n#3-2 s=Sa:a;Bmaster=3;Os I a=b`, + expected: ` +x=U:Cbar\n#x-2 s=Sa:s;Bmaster=x;Os Cbar\n#s-1 README.md=foo, a=b`, editor: null, }, }; From a2c8efb2168aea61ddebe86bb5ecbf3f4cccfe99 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 9 Feb 2018 19:12:42 -0500 Subject: [PATCH 044/402] Print a "." instead of "" for submodule name Addresses: https://github.com/twosigma/git-meta/issues/479 --- node/lib/util/print_status_util.js | 6 +++++- node/test/util/print_status_util.js | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/node/lib/util/print_status_util.js b/node/lib/util/print_status_util.js index d423369d8..912ecf490 100644 --- a/node/lib/util/print_status_util.js +++ b/node/lib/util/print_status_util.js @@ -92,7 +92,11 @@ class StatusDescriptor { result += "type changed: "; break; } - result += path.relative(cwd, this.path); + let filename = path.relative(cwd, this.path); + if ("" === filename) { + filename = "."; + } + result += filename; result = color(result); if ("" !== this.detail) { result += ` (${this.detail})`; diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js index 703622ceb..bca4fd995 100644 --- a/node/test/util/print_status_util.js +++ b/node/test/util/print_status_util.js @@ -102,6 +102,11 @@ describe("PrintStatusUtil", function () { cwd: "q", check: /\.\.\/x/, }, + "same as cwd": { + des: new StatusDescriptor(FILESTATUS.ADDED, "x", "y"), + cwd: "x", + check: / \. /, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From 6737e40b879eef35b338d62e9a4bb04c8345cc95 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 14 Feb 2018 16:58:20 -0500 Subject: [PATCH 045/402] Honor `pushDefault` and `pushRemote` in `push` Addresses https://github.com/twosigma/git-meta/issues/484 --- node/lib/cmd/pull.js | 2 +- node/lib/cmd/push.js | 4 +- node/lib/util/git_util.js | 42 +++++++++++++++- node/lib/util/synthetic_branch_util.js | 20 ++------ node/test/util/git_util.js | 67 +++++++++++++++++++++++++- 5 files changed, 113 insertions(+), 22 deletions(-) diff --git a/node/lib/cmd/pull.js b/node/lib/cmd/pull.js index 1c4f357a1..afb0d5327 100644 --- a/node/lib/cmd/pull.js +++ b/node/lib/cmd/pull.js @@ -100,7 +100,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { // TODO: move the following code into `util/push.js` and add test - const tracking = (yield GitUtil.getTrackingInfo(branch)) || {}; + const tracking = (yield GitUtil.getTrackingInfo(repo, branch)) || {}; // The source branch is (in order of preference): the name passed on the // commandline, the tracking branch name, or the current branch name. diff --git a/node/lib/cmd/push.js b/node/lib/cmd/push.js index 8062411ba..b8f6a0451 100644 --- a/node/lib/cmd/push.js +++ b/node/lib/cmd/push.js @@ -101,7 +101,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { // TODO: this all needs to move into the `util` and get a test driver. const branch = yield repo.getCurrentBranch(); - const tracking = (yield GitUtil.getTrackingInfo(branch)) || {}; + const tracking = (yield GitUtil.getTrackingInfo(repo, branch)) || {}; let strRefspecs = []; if (0 === args.refspec.length) { @@ -115,7 +115,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { // The repo is the value passed by the user, the tracking branch's remote, // or just "origin", in order of preference. - const remoteName = args.repository || tracking.remoteName || "origin"; + const remoteName = args.repository || tracking.pushRemoteName || "origin"; yield strRefspecs.map(co.wrap(function *(strRefspec) { const refspec = GitUtil.parseRefspec(strRefspec); diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 551176135..ad32f6a6f 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -110,16 +110,37 @@ exports.findBranch = co.wrap(function *(repo, branchName) { } }); +/** + * Return the string in the specified `config` for the specified `key`, or null + * if `key` does not exist in `config`. + * + * @param {NodeGit.Config} config + * @param {String} key + * @return {String|null} + */ +exports.getConfigString = co.wrap(function *(config, key) { + try { + return yield config.getStringBuf(key); + } + catch (e) { + // Unfortunately, no other way to handle a missing config entry + } + return null; +}); + /** * Return the tracking information for the specified `branch`, or null if it * has none. * + * @param {NodeGit.Repository} repo * @param {NodeGit.Reference} branch * @return {Object|null} * @return {String|null} return.remoteName + * @return {String|null} return.pushRemoteName * @return {String} return.branchName */ -exports.getTrackingInfo = co.wrap(function *(branch) { +exports.getTrackingInfo = co.wrap(function *(repo, branch) { + assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(branch, NodeGit.Reference); let upstream; try { @@ -131,16 +152,33 @@ exports.getTrackingInfo = co.wrap(function *(branch) { } const name = upstream.shorthand(); const parts = name.split("/"); + const config = yield repo.config(); + + // Try to read a 'pushRemote' for the branch. + + let pushRemote = yield exports.getConfigString( + config, + `branch.${branch.shorthand()}.pushRemote`); + + // If no 'pushRemote', try to read a 'pushDefault' for the repo. + + if (null === pushRemote) { + pushRemote = yield exports.getConfigString(config, + "remote.pushDefault"); + } + if (1 === parts.length) { return { branchName: parts[0], remoteName: null, + pushRemoteName: pushRemote, }; } const remoteName = parts.shift(); return { branchName: parts.join("/"), remoteName: remoteName, + pushRemoteName: pushRemote || remoteName, }; }); @@ -155,7 +193,7 @@ exports.getTrackingInfo = co.wrap(function *(branch) { exports.getRemoteForBranch = co.wrap(function *(repo, branch) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(branch, NodeGit.Reference); - const trackingInfo = yield exports.getTrackingInfo(branch); + const trackingInfo = yield exports.getTrackingInfo(repo, branch); if (null === trackingInfo || null === trackingInfo.remoteName) { return null; } diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index c90c0d61f..801613b12 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -66,24 +66,12 @@ function *urlToLocalPath(repo, url) { assert.isString(url); const config = yield repo.config(); - let subrepoUrlBase = ""; - try { - subrepoUrlBase = - yield config.getStringBuf("gitmeta.subrepourlbase"); - } catch (e) { - //It's OK for this to be missing, but nodegit lacks an - //API that expresses this. - - } + const subrepoUrlBase = + (yield GitUtil.getConfigString(config, "gitmeta.subrepourlbase")) || ""; const subrepoRootPath = yield config.getStringBuf("gitmeta.subreporootpath"); - let subrepoSuffix = ""; - try { - subrepoSuffix = yield config.getStringBuf("gitmeta.subreposuffix"); - } catch (e) { - //It's OK for this to be missing, but nodegit lacks an - //API that expresses this. - } + const subrepoSuffix = + (yield GitUtil.getConfigString(config, "gitmeta.subreposuffix")) || ""; if (!url.startsWith(subrepoUrlBase)) { throw "Your git configuration gitmeta.subrepoUrlBase, '" + subrepoUrlBase + "', must be a prefix of all submodule " + diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index f07300c1b..c0bedfeaa 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -47,6 +47,23 @@ const UserError = require("../../lib/util/user_error"); const WriteRepoASTUtil = require("../../lib/util/write_repo_ast_util"); describe("GitUtil", function () { + describe("getConfigString", function () { + it("exists", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const configPath = path.join(repo.path(), "config"); + yield fs.appendFile(configPath, `\ +[foo] + bar = baz +`); + const config = yield repo.config(); + const goodResult = + yield GitUtil.getConfigString(config, "foo.bar"); + assert.equal(goodResult, "baz"); + const badResult = yield GitUtil.getConfigString(config, "yyy.zzz"); + assert.isNull(badResult); + })); + }); describe("getTrackingInfo", function () { const cases = { "no tracking": { @@ -60,6 +77,7 @@ describe("GitUtil", function () { expected: { remoteName: null, branchName: "master", + pushRemoteName: null, }, }, "with remote": { @@ -67,6 +85,7 @@ describe("GitUtil", function () { branch: "bar", expected: { remoteName: "hoo", + pushRemoteName: "hoo", branchName: "gob", }, }, @@ -75,17 +94,63 @@ describe("GitUtil", function () { branch: "bar", expected: { remoteName: "hoo", + pushRemoteName: "hoo", branchName: "foo/bar", }, }, + "with pushRemote": { + state: "S:Rhoo=/a gob=1;Bbar=1 hoo/gob", + branch: "bar", + pushRemote: "bah", + expected: { + remoteName: "hoo", + pushRemoteName: "bah", + branchName: "gob", + }, + }, + "with pushDefault": { + state: "S:Rhoo=/a gob=1;Bbar=1 hoo/gob", + branch: "bar", + pushDefault: "bah", + expected: { + remoteName: "hoo", + pushRemoteName: "bah", + branchName: "gob", + }, + }, + "with pushRemote and pushDefault": { + state: "S:Rhoo=/a gob=1;Bbar=1 hoo/gob", + branch: "bar", + pushRemote: "hehe", + pushDefault: "bah", + expected: { + remoteName: "hoo", + pushRemoteName: "hehe", + branchName: "gob", + }, + } }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; it(caseName, co.wrap(function *() { const written = yield RepoASTTestUtil.createRepo(c.state); const repo = written.repo; + if (undefined !== c.pushRemote) { + const configPath = path.join(repo.path(), "config"); + yield fs.appendFile(configPath, `\ +[branch "${c.branch}"] + pushRemote = ${c.pushRemote} +`); + } + if (undefined !== c.pushDefault) { + const configPath = path.join(repo.path(), "config"); + yield fs.appendFile(configPath, `\ +[remote] + pushDefault = ${c.pushDefault} +`); + } const branch = yield repo.getBranch(c.branch); - const result = yield GitUtil.getTrackingInfo(branch); + const result = yield GitUtil.getTrackingInfo(repo, branch); assert.deepEqual(result, c.expected); })); }); From 5d65e302c057936296d1d88b9f8a06f6f8dde597 Mon Sep 17 00:00:00 2001 From: "David \"novalis\" Turner" Date: Tue, 20 Feb 2018 21:51:06 -0500 Subject: [PATCH 046/402] fix #486: don't add a suffix where one is already present (#487) --- node/lib/util/synthetic_branch_util.js | 17 ++++++++--- node/test/util/synthetic-branch.js | 40 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 801613b12..58591ef62 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -61,7 +61,13 @@ exports.getSyntheticBranchForCommit = function(commit) { return SYNTHETIC_BRANCH_BASE + commit; }; -function *urlToLocalPath(repo, url) { + /** + * Public for testing. Gets the local path corresponding to + * a submodule's URL. + * @param {NodeGit.Repository} repo + * @param {String} url + */ +exports.urlToLocalPath = function *(repo, url) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(url); @@ -70,7 +76,7 @@ function *urlToLocalPath(repo, url) { (yield GitUtil.getConfigString(config, "gitmeta.subrepourlbase")) || ""; const subrepoRootPath = yield config.getStringBuf("gitmeta.subreporootpath"); - const subrepoSuffix = + let subrepoSuffix = (yield GitUtil.getConfigString(config, "gitmeta.subreposuffix")) || ""; if (!url.startsWith(subrepoUrlBase)) { throw "Your git configuration gitmeta.subrepoUrlBase, '" + @@ -78,13 +84,16 @@ function *urlToLocalPath(repo, url) { "urls. Submodule url '" + url + "' fails."; } const remotePath = url.slice(subrepoUrlBase.length); + if (remotePath.endsWith(subrepoSuffix)) { + subrepoSuffix = ""; + } const localPath = path.join(subrepoRootPath, remotePath + subrepoSuffix); if (localPath[0] === "/") { return localPath; } else { return path.normalize(path.join(repo.path(), localPath)); } -} +}; /** * Check that a commit exists exists for a given submodule @@ -99,7 +108,7 @@ function* checkSubmodule(repo, metaCommit, submoduleEntry, url) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(submoduleEntry, NodeGit.TreeEntry); - const localPath = yield *urlToLocalPath(repo, url); + const localPath = yield *exports.urlToLocalPath(repo, url); const submoduleRepo = yield NodeGit.Repository.open(localPath); const submoduleCommitId = submoduleEntry.id(); try { diff --git a/node/test/util/synthetic-branch.js b/node/test/util/synthetic-branch.js index 0f2dff25c..5a73c412b 100644 --- a/node/test/util/synthetic-branch.js +++ b/node/test/util/synthetic-branch.js @@ -227,6 +227,46 @@ describe("synthetic-branch-submodule-pre-receive", function () { })); }); +describe("urlToLocalPath", function () { + const init = co.wrap(function*() { + const base = yield TestUtil.makeTempDir(); + const dir = yield fsp.realpath(base); + const repo = yield NodeGit.Repository.init(dir, 0); + const config = yield repo.config(); + yield config.setString("gitmeta.subrepourlbase", "http://example.com"); + yield config.setString("gitmeta.subreposuffix", ".GiT"); + yield config.setString("gitmeta.subreporootpath", "/a/b/c"); + return repo; + }); + const cases = { + "works without suffix" : { + input: "http://example.com/foo", + expected: "/a/b/c/foo.GiT" + }, + "works with existing suffix" : { + input: "http://example.com/foo.GiT", + expected: "/a/b/c/foo.GiT" + }, + "works with wrong prefix" : { + input: "http://wrong/foo.GiT", + expected: null + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + try { + const repo = yield init(); + const actual = + yield SyntheticBranch.urlToLocalPath(repo, c.input); + assert(c.expected === actual); + } catch (e) { + assert(c.expected === null); + } + })); + }); +}); + describe("synthetic-branch-meta-pre-receive", function () { it("works", co.wrap(function *() { const base = yield TestUtil.makeTempDir(); From 16ec51643f25973c5449acc6d463a1e2e3c356b5 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Tue, 27 Feb 2018 13:22:57 -0500 Subject: [PATCH 047/402] Shadow commit should set FILEMODE.EXECUTABLE if needed. --- node/lib/util/stash_util.js | 2 +- node/lib/util/tree_util.js | 34 +++++++++++++-------- node/test/util/tree_util.js | 61 +++++++++++++++++++++++++++++++------ 3 files changed, 75 insertions(+), 22 deletions(-) diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 28c2ce85c..d6e950ae2 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -69,7 +69,7 @@ exports.stashRepo = co.wrap(function *(repo, status, includeUntracked) { // Create a tree for the workdir based on the index. const indexTree = yield NodeGit.Tree.lookup(repo, indexId); - const changes = TreeUtil.listWorkdirChanges(repo, + const changes = yield TreeUtil.listWorkdirChanges(repo, status, includeUntracked); const workdirTree = yield TreeUtil.writeTree(repo, indexTree, changes); diff --git a/node/lib/util/tree_util.js b/node/lib/util/tree_util.js index 2d3353071..4e769eba3 100644 --- a/node/lib/util/tree_util.js +++ b/node/lib/util/tree_util.js @@ -32,6 +32,7 @@ const assert = require("chai").assert; const co = require("co"); +const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); @@ -52,8 +53,8 @@ const SubmoduleConfigUtil = require("./submodule_config_util"); exports.buildDirectoryTree = function (flatTree) { let result = {}; - for (let path in flatTree) { - const paths = path.split("/"); + for (let subpath in flatTree) { + const paths = subpath.split("/"); let tree = result; // Navigate/build the tree until there is only one path left in paths, @@ -73,7 +74,7 @@ exports.buildDirectoryTree = function (flatTree) { } const leafPath = paths[paths.length - 1]; assert.notProperty(tree, leafPath, `duplicate entry for ${path}`); - const data = flatTree[path]; + const data = flatTree[subpath]; tree[leafPath] = data; } @@ -228,7 +229,7 @@ exports.hashFile = function (repo, filename) { * @param {Boolean} includeUnstaged * @return {Object} */ -exports.listWorkdirChanges = function (repo, status, includeUnstaged) { +exports.listWorkdirChanges = co.wrap(function *(repo, status, includeUnstaged) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(status, RepoStatus); assert.isBoolean(includeUnstaged); @@ -242,20 +243,29 @@ exports.listWorkdirChanges = function (repo, status, includeUnstaged) { // first, plain files. const workdir = status.workdir; - for (let path in workdir) { - switch (workdir[path]) { + for (let subpath in workdir) { + let filemode = FILEMODE.EXECUTABLE; + const fullpath = path.join(repo.workdir(), subpath); + try { + yield fs.access(fullpath, fs.constants.X_OK); + } catch (e) { + // if unable to execute, use BLOB. + filemode = FILEMODE.BLOB + } + switch (workdir[subpath]) { case FILESTATUS.ADDED: if (includeUnstaged) { - result[path] = new Change(exports.hashFile(repo, path), - FILEMODE.BLOB); + result[subpath] = new Change( + exports.hashFile(repo, subpath), + filemode); } break; case FILESTATUS.MODIFIED: - result[path] = new Change(exports.hashFile(repo, path), - FILEMODE.BLOB); + result[subpath] = new Change(exports.hashFile(repo, subpath), + filemode); break; case FILESTATUS.REMOVED: - result[path] = null; + result[subpath] = null; break; } } @@ -299,4 +309,4 @@ exports.listWorkdirChanges = function (repo, status, includeUnstaged) { } return result; -}; +}); diff --git a/node/test/util/tree_util.js b/node/test/util/tree_util.js index 499db5e8b..388096748 100644 --- a/node/test/util/tree_util.js +++ b/node/test/util/tree_util.js @@ -262,7 +262,8 @@ describe("TreeUtil", function () { const status = new RepoStatus({ workdir: { foo: FILESTATUS.REMOVED }, }); - const result = TreeUtil.listWorkdirChanges(repo, status, false); + const result = yield TreeUtil.listWorkdirChanges(repo, status, + false); assert.deepEqual(result, { foo: null }); })); it("modified", co.wrap(function *() { @@ -274,7 +275,8 @@ describe("TreeUtil", function () { const status = new RepoStatus({ workdir: { foo: FILESTATUS.MODIFIED }, }); - const result = TreeUtil.listWorkdirChanges(repo, status, false); + const result = yield TreeUtil.listWorkdirChanges(repo, status, + false); const db = yield repo.odb(); const BLOB = 3; const id = yield db.write(content, content.length, BLOB); @@ -298,7 +300,8 @@ describe("TreeUtil", function () { }), }, }); - const result = TreeUtil.listWorkdirChanges(repo, status, false); + const result = yield TreeUtil.listWorkdirChanges(repo, status, + false); assert.deepEqual(result, {}); })); it("submodule", co.wrap(function *() { @@ -315,7 +318,8 @@ describe("TreeUtil", function () { }), }, }); - const result = TreeUtil.listWorkdirChanges(repo, status, false); + const result = yield TreeUtil.listWorkdirChanges(repo, status, + false); assert.deepEqual(result, { sub: new Change(NodeGit.Oid.fromString(commit), FILEMODE.COMMIT), @@ -335,7 +339,8 @@ describe("TreeUtil", function () { }), }, }); - const result = TreeUtil.listWorkdirChanges(repo, status, true); + const result = yield TreeUtil.listWorkdirChanges(repo, status, + true); assert.deepEqual(result, { sub: new Change(NodeGit.Oid.fromString(commit), FILEMODE.COMMIT), @@ -363,7 +368,8 @@ describe("TreeUtil", function () { }), }, }); - const result = TreeUtil.listWorkdirChanges(repo, status, true); + const result = yield TreeUtil.listWorkdirChanges(repo, status, + true); assert.deepEqual(result, { sub: new Change(NodeGit.Oid.fromString(commit), FILEMODE.COMMIT), @@ -390,7 +396,8 @@ describe("TreeUtil", function () { }), }, }); - const result = TreeUtil.listWorkdirChanges(repo, status, true); + const result = yield TreeUtil.listWorkdirChanges(repo, status, + true); assert.deepEqual(result, { sub: null, ".gitmodules": new Change(modId, FILEMODE.BLOB), @@ -415,7 +422,8 @@ describe("TreeUtil", function () { }), }, }); - const result = TreeUtil.listWorkdirChanges(repo, status, false); + const result = yield TreeUtil.listWorkdirChanges(repo, status, + false); assert.deepEqual(result, {}); })); it("added, with includeUnstaged", co.wrap(function *() { @@ -427,7 +435,8 @@ describe("TreeUtil", function () { const status = new RepoStatus({ workdir: { foo: FILESTATUS.ADDED, }, }); - const result = TreeUtil.listWorkdirChanges(repo, status, true); + const result = yield TreeUtil.listWorkdirChanges(repo, status, + true); const db = yield repo.odb(); const BLOB = 3; const id = yield db.write(content, content.length, BLOB); @@ -435,5 +444,39 @@ describe("TreeUtil", function () { assert.equal(result.foo.id.tostrS(), id.tostrS()); assert.equal(result.foo.mode, FILEMODE.BLOB); })); + it("executable", co.wrap(function *() { + const content = "abcdefg"; + const repo = yield TestUtil.createSimpleRepository(); + + const filename1 = "foo"; + const filepath1 = path.join(repo.workdir(), filename1); + yield fs.writeFile(filepath1, content, { mode: 0o744 }); + + const filename2 = "bar"; + const filepath2 = path.join(repo.workdir(), filename2); + yield fs.writeFile(filepath2, content, { mode: 0o744 }); + + const status = new RepoStatus({ + workdir: { foo: FILESTATUS.MODIFIED, bar: FILESTATUS.ADDED }, + }); + + const db = yield repo.odb(); + const BLOB = 3; + const id = yield db.write(content, content.length, BLOB); + + // executable ignoring new files + let result = yield TreeUtil.listWorkdirChanges(repo, status, + false); + assert.deepEqual(Object.keys(result), ["foo"]); + assert.equal(result.foo.id.tostrS(), id.tostrS()); + assert.equal(result.foo.mode, FILEMODE.EXECUTABLE); + + // executable including added files + result = yield TreeUtil.listWorkdirChanges(repo, status, + true); + assert.deepEqual(Object.keys(result), ["foo", "bar"]); + assert.equal(result.bar.id.tostrS(), id.tostrS()); + assert.equal(result.bar.mode, FILEMODE.EXECUTABLE); + })); }); }); From 7cf2ec5284677806bc2348a8ab036296acc845fc Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Tue, 27 Feb 2018 15:09:48 -0500 Subject: [PATCH 048/402] Remove empty test. --- node/test/util/stash_util.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index 15a6dcfe1..aec23f59c 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -746,17 +746,12 @@ meta-stash@{1}: log of 1 expected: "x=E:Cm-2 s=Sa:a;Bm=m", includeMeta: true, }, - "new file in open submodule": { + "new file in open submodule, untracked not included": { state: "a=B|x=U:Os W x/y/z=3", includeMeta: true, expected: ` x=E:Cm-2 s=Sa:s;Bm=m;Os W x/y/z=3!Cs-1 x/y/z=3!Bs=s`, }, - "new file in open submodule, untracked not included": { - state: "a=B|x=U:Os W x/y/z=3", - includeMeta: true, - includeUntracked: false, - }, "new submodule with a commit": { state: "a=B|x=S:I s=Sa:;Os Cq-1!H=q", includeMeta: false, From 8bebcee36830b090cf0f9926643c775e8356aff8 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Tue, 27 Feb 2018 15:10:14 -0500 Subject: [PATCH 049/402] Staged, new files were not being included in commit-shadow. --- node/lib/util/stash_util.js | 11 +++++++---- node/test/util/stash_util.js | 7 +++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 28c2ce85c..875c1f3de 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -483,13 +483,16 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, status, includeUntracked); const head = yield repo.getHeadCommit(); - let headTree = null; const parents = []; if (null !== head) { parents.push(head); - headTree = yield head.getTree(); } - const newTree = yield TreeUtil.writeTree(repo, headTree, changes); + + const index = yield repo.index(); + const treeOid = yield index.writeTree(); + const indexTree = yield repo.getTree(treeOid); + + const newTree = yield TreeUtil.writeTree(repo, indexTree, changes); const sig = repo.defaultSignature(); const id = yield NodeGit.Commit.create(repo, null, @@ -538,7 +541,7 @@ exports.makeShadowCommit = co.wrap(function *(repo, const status = yield StatusUtil.getRepoStatus(repo, { showMetaChanges: includeMeta, showAllUntracked: true, - ignoreIndex: true, + ignoreIndex: false, }); if (status.isDeepClean(includeUntracked)) { return null; // RETURN diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index aec23f59c..c7044244a 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -752,6 +752,13 @@ meta-stash@{1}: log of 1 expected: ` x=E:Cm-2 s=Sa:s;Bm=m;Os W x/y/z=3!Cs-1 x/y/z=3!Bs=s`, }, + "new file in open submodule, staged": { + state: "a=B|x=U:Os I x/y/z=3", + expected: ` +x=E:Cm-2 s=Sa:s;Bm=m;Os I x/y/z=3!Cs-1 x/y/z=3!Bs=s`, + includeMeta: true, + includeUntracked: false, + }, "new submodule with a commit": { state: "a=B|x=S:I s=Sa:;Os Cq-1!H=q", includeMeta: false, From 8d0a4d3255cc46ea641f2f218808d3d413463608 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 27 Feb 2018 16:00:25 -0500 Subject: [PATCH 050/402] Stop making empty commits in 'merge --continue' I had two bugs: 1. I was incorrectly checking for staged entries and should have been using the `RepoStatus.isIndexClean` method anyway. 2. After fixing this another test case broke because I was passing the wrong flag to request meta (i.e., non-submodule) changes when getting the status for the submodule. Addresses: https://github.com/twosigma/git-meta/issues/491 --- node/lib/util/merge_util.js | 4 ++-- node/test/util/merge_util.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 1ca2486f1..34f441131 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -544,9 +544,9 @@ exports.continue = co.wrap(function *(repo) { // changes we need to make a commit. const status = yield StatusUtil.getRepoStatus(subRepo, { - showMeta: true, + showMetaChanges: true, }); - if (0 !== Object.keys(status.staged)) { + if (!status.isIndexClean()) { const id = yield subRepo.createCommitOnHead([], sig, sig, diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index 3788edd35..ec07fb585 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -409,6 +409,21 @@ x=U:C3-1;Mhi\n,2,3;B3=3;Os I README.md=8!Myo\n,1,a!Ba=a`, expected: ` x=E:Chi\n#x-2,3 s=Sa:s;Bmaster=x;M;Os Cyo\n#s-1,a README.md=8!H=s!Ba=a`, }, + "continue in one sub, done in another": { + initial: ` +a=B:Ca-1;Cac-1 a=2;Cb-1;Cmab-a,b b=b;Bmab=mab;Bb=b;Ba=a;Bac=ac| +x=S:C2-1 s=Sa:1,t=Sa:1; + C3-2 s=Sa:a,t=Sa:a; + C4-2 s=Sa:ac,t=Sa:b; + Bmaster=3;Bfoo=4; + Mhi\n,3,4; + Os I a=foo!Myou\n,a,ac!Bac=ac; + Ot H=mab`, + expected: ` +x=E:Chi\n#x-3,4 s=Sa:s,t=Sa:mab;Bmaster=x;M; + Os Cyou\n#s-a,ac a=foo!H=s!Bac=ac; + Ot`, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From 1156970871a9fc7547640fb7eef6846cdb0c5f6d Mon Sep 17 00:00:00 2001 From: Shihui Xiang Date: Wed, 28 Feb 2018 12:00:04 -0500 Subject: [PATCH 051/402] #495: add missing hooks in git-meta --- node/lib/cmd/add_submodule.js | 4 +++ node/lib/cmd/checkout.js | 14 +++++++- node/lib/cmd/cherrypick.js | 4 +++ node/lib/cmd/merge.js | 6 ++++ node/lib/cmd/open.js | 6 ++++ node/lib/cmd/rebase.js | 4 +++ node/lib/cmd/reset.js | 4 +++ node/lib/util/close_util.js | 6 ++++ node/lib/util/hook.js | 60 +++++++++++++++++++++++++++++++ node/test/util/hook.js | 67 +++++++++++++++++++++++++++++++++++ 10 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 node/lib/util/hook.js create mode 100644 node/test/util/hook.js diff --git a/node/lib/cmd/add_submodule.js b/node/lib/cmd/add_submodule.js index 50e0028a4..b3921a401 100644 --- a/node/lib/cmd/add_submodule.js +++ b/node/lib/cmd/add_submodule.js @@ -31,6 +31,7 @@ "use strict"; const co = require("co"); +const Hook = require("../util/hook"); /** * This module contains methods for implementing the `new` command. @@ -124,4 +125,7 @@ Added new sub-repo ${colors.blue(args.path)}. It is currently empty. Please stage changes under sub-repo before finishing with 'git meta commit'; you will not be able to use 'git meta commit' until you do so.`); } + + //Run post-add-submodule hook with submodule names which added successfully. + yield Hook.execHook("post-add-submodule", [args.path]); }); diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index 06fcd736d..333f2d4e4 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -30,6 +30,7 @@ */ const co = require("co"); +const Hook = require("../util/hook"); /** * This module contains the entrypoint for the `checkout` command. @@ -88,6 +89,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { const Checkout = require("../util/checkout"); const GitUtil = require("../util/git_util"); let newBranch = null; + let branchCheckout = "1"; const newBranchNameArr = args["new branch name"]; if (newBranchNameArr) { @@ -130,10 +132,20 @@ exports.executeableSubcommand = co.wrap(function *(args) { if (null !== op.commit && null === op.switchBranch) { // In this case, we're not making a branch; just let the user know what // we checked out. - + + branchCheckout = "0"; console.log(`Checked out ${colors.green(args.committish)}.`); } + // Run post-checkout hook. + // Note: The hook is given three parameters: the ref of the previous HEAD, + // the ref of the new HEAD (which may or may not have changed), + // and a flag indicating whether the checkout was a branch checkout (changing branches, flag = "1"), + // or a file checkout (retrieving a file from the index, flag = "0"). + const headId = yield repo.getHeadCommit(); + const oldHead = headId.id().tostrS(); + const newHead = op.commit; + yield Hook.execHook("post-checkout", [oldHead, newHead, branchCheckout]); // If we made a new branch, let the user know about it. const newB = op.newBranch; diff --git a/node/lib/cmd/cherrypick.js b/node/lib/cmd/cherrypick.js index a00ec89bf..016395146 100644 --- a/node/lib/cmd/cherrypick.js +++ b/node/lib/cmd/cherrypick.js @@ -31,6 +31,7 @@ "use strict"; const co = require("co"); +const Hook = require("../util/hook"); /** * This module contains methods for implementing the `cherry-pick` command. @@ -97,4 +98,7 @@ commit.`); yield CherryPick.cherryPick(repo, commit); } } + + // Run post-commit hook as regular git. + yield Hook.execHook("post-commit"); }); diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index a79d20321..35619eb12 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -31,6 +31,7 @@ "use strict"; const co = require("co"); +const Hook = require("../util/hook"); /** * This module contains methods for implementing the `merge` command. @@ -163,4 +164,9 @@ Merge of '${args.commit}' }; const commit = yield repo.getCommit(commitish.id()); yield MergeUtil.merge(repo, commit, mode, args.message, editMessage); + + // Run post-merge hook if merge successfully. + // Fixme: --squash is not supported yet, once supported, need to parse 0/1 + // as arg into the post-merge hook, 1 means it is a squash merge, 0 means not. + yield Hook.execHook("post-merge", ["0"]); }); diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index d4d897ec2..bee290e03 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -35,6 +35,7 @@ */ const co = require("co"); +const Hook = require("../util/hook"); /** * help text for the `open` command @@ -102,6 +103,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { const fetcher = new SubmoduleFetcher(repo, head); let failed = false; + let subsOpenSuccessfully = []; const openSubs = new Set(yield SubmoduleUtil.listOpenSubmodules(repo)); @@ -123,6 +125,7 @@ Opening ${colors.blue(name)} on ${colors.green(shas[index])}.`); try { yield Open.openOnCommit(fetcher, name, shas[index], templatePath); + subsOpenSuccessfully.push(name); } catch (e) { if (e instanceof UserError) { @@ -138,6 +141,9 @@ Opening ${colors.blue(name)} on ${colors.green(shas[index])}.`); }); yield DoWorkQueue.doInParallel(subsToOpen, opener); + // Run post-open-submodule hook with submodules which opened successfully. + yield Hook.execHook("post-open-submodule", subsOpenSuccessfully); + if (failed) { process.exit(1); } diff --git a/node/lib/cmd/rebase.js b/node/lib/cmd/rebase.js index 27d252543..75985b01b 100644 --- a/node/lib/cmd/rebase.js +++ b/node/lib/cmd/rebase.js @@ -30,6 +30,7 @@ */ const co = require("co"); +const Hook = require("../util/hook"); /** * This module contains methods for implementing the `rebase` command. @@ -110,4 +111,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { const commit = yield repo.getCommit(committish.id()); yield RebaseUtil.rebase(repo, commit); } + + // Run post-rewrite hook with "rebase" as args, means rebase command invoked this hook. + yield Hook.execHook("post-rewrite", ["rebase"]); }); diff --git a/node/lib/cmd/reset.js b/node/lib/cmd/reset.js index a60ab0034..9dcefb9c1 100644 --- a/node/lib/cmd/reset.js +++ b/node/lib/cmd/reset.js @@ -32,6 +32,7 @@ const ArgParse = require("argparse"); const co = require("co"); +const Hook = require("../util/hook"); /** * This submodule provides the entrypoint for the `reset` command. @@ -206,4 +207,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { const relCwd = path.relative(repo.workdir(), cwd); const statusText = PrintStatusUtil.printRepoStatus(repoStatus, relCwd); process.stdout.write(statusText); + + // Run post-reset hook. + yield Hook.execHook("post-reset"); }); diff --git a/node/lib/util/close_util.js b/node/lib/util/close_util.js index 9a81d8080..fee1047e8 100644 --- a/node/lib/util/close_util.js +++ b/node/lib/util/close_util.js @@ -36,6 +36,7 @@ const co = require("co"); const colors = require("colors"); const DeinitUtil = require("../util/deinit_util"); +const Hook = require("../util/hook"); const StatusUtil = require("../util/status_util"); const SubmoduleUtil = require("../util/submodule_util"); const UserError = require("../util/user_error"); @@ -71,6 +72,7 @@ exports.close = co.wrap(function *(repo, cwd, paths, force) { }); const subStats = repoStatus.submodules; let errorMessage = ""; + let subsClosedSuccessfully = []; const closers = subsToClose.map(co.wrap(function *(name) { const sub = subStats[name]; @@ -94,8 +96,12 @@ Pass ${colors.magenta("--force")} to close it anyway. } } yield DeinitUtil.deinit(repo, name); + subsClosedSuccessfully.push(name); })); yield closers; + + // Run post-close-submodule hook with submodules which closed successfully. + yield Hook.execHook("post-close-submodule", subsClosedSuccessfully); if ("" !== errorMessage) { throw new UserError(errorMessage); } diff --git a/node/lib/util/hook.js b/node/lib/util/hook.js new file mode 100644 index 000000000..c019e0ca6 --- /dev/null +++ b/node/lib/util/hook.js @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const ChildProcess = require("child-process-promise"); +const co = require("co"); +const path = require("path"); +const GitUtil = require("../util/git_util"); + +/** + * Run git-meta hook with given hook name. + * @async + * @param {String} name + * @param {String[]} args + * @return {String} + */ +exports.execHook = co.wrap(function *(name, args=[]) { + assert.isString(name); + + const hookPath = path.join(GitUtil.getRootGitDirectory(), ".git/hooks"); + const absPath = path.resolve(hookPath, name); + + try { + const result = yield ChildProcess.execFile(absPath, args); + return result.stdout; + } catch (e) { + // ignore the exception + // Fixme: Need to be sync with git. If the hook is not executable, + // should give user hints. + } +}); diff --git a/node/test/util/hook.js b/node/test/util/hook.js new file mode 100644 index 000000000..c067b49a1 --- /dev/null +++ b/node/test/util/hook.js @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const Hook = require("../../lib/util/hook"); +const path = require("path"); +const fs = require("fs-promise"); +const TestUtil = require("../../lib/util/test_util"); + +describe("Hook", function () { + describe("execHook", function () { + // 1. Hook does not exist, no error throws. + it("hook_does_not_exist", co.wrap(function *() { + const hookName = "fake_hook"; + assert.doesNotThrow( + function () { + //Nothing happened, no error throws. + }, + yield Hook.execHook(hookName) + ); + })); + + // 2. Hook exists. + it("hook_exists", co.wrap(function *() { + const hookName = "real_hook"; + const repo = yield TestUtil.createSimpleRepository(); + const workDir = repo.workdir(); + process.chdir(workDir); + const hooksDir = path.join(workDir, ".git/hooks"); + const hookFile = path.join(hooksDir, hookName); + yield fs.writeFile(hookFile, "#!/bin/bash \necho 'it is a test hook'\n"); + yield fs.chmod(hookFile, "755"); + const result = yield Hook.execHook(hookName); + assert.equal(result, "it is a test hook\n"); + })); + }); +}); \ No newline at end of file From c1722d0c3b18f7f869e61f1c8859c1c1f143226d Mon Sep 17 00:00:00 2001 From: Andy Reitz Date: Thu, 1 Mar 2018 17:51:43 -0800 Subject: [PATCH 052/402] Small updates to architecture.md Just found two tiny typos. --- doc/architecture.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/architecture.md b/doc/architecture.md index 71c143e81..bff419889 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -292,13 +292,13 @@ explain our high-level branching and tagging strategy. Then, we define synthetic-meta-refs and discuss how they fit into git-meta workflows. Finally, we present two variations on synthetic-meta-refs to help illustrate how the concept evolved; one of these variations, *mega-refs*, may prove necessary for -old version of Git. +old versions of Git. ## Branches and Tags In git-meta, branches and tags are applied only to meta-repos. Because each commit in a meta-repo unambiguously describes the state of all sub-repos, it is -unnecessary to apply branches and tags to sup-repos. Furthermore, as described +unnecessary to apply branches and tags to sub-repos. Furthermore, as described in the "Design and Evolution" section below, schemes relying on sub-repo branches proved to be impractical. From 89e50f5d0d0a7161f46fa5a8952a00cbb6fd588e Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 6 Mar 2018 16:46:53 -0500 Subject: [PATCH 053/402] Stop `--amend -a` from adding untracked files. --- node/lib/util/commit.js | 20 +++++++++++++++++++- node/test/util/commit.js | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index c742dd445..2eefd0205 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -1108,7 +1108,25 @@ exports.amendMetaRepo = co.wrap(function *(repo, } const subIndex = yield subRepo.index(); if (all) { - yield subIndex.addAll(["*"], -1); + const actualStatus = yield StatusUtil.getRepoStatus(subRepo, { + showMetaChanges: true, + }); + + // TODO: factor this out. We cannot use `repoStatus` to + // determine what to stage as it shows the status vs. HEAD^ + // and so some things that should be changed will not be in it. + // We cannot call `Index.addAll` because it will stage + // untracked files. Therefore, we need to use our normal + // status routine to examime the workdir and stage changed + // files. + + const workdir = actualStatus.workdir; + for (let path in actualStatus.workdir) { + const change = workdir[path]; + if (RepoStatus.FILESTATUS.ADDED !== change) { + yield exports.stageChange(subIndex, path, change); + } + } yield subIndex.write(); } subCommits[subName] = yield exports.amendRepo(subRepo, subMessage); diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 4f7c4b580..1f7a5a1e2 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -3085,6 +3085,24 @@ x=U:C3-2 s=Sa:a;Bmaster=3;Os W README.md=8`, message: "foo", expected: ` x=U:Cfoo\n#x-2 s=Sa:s;Bmaster=x;Os Cfoo\n#s-1 README.md=foo!W README.md=8` + }, + "amend with all": { + initial: ` +a=B:Ca-1 README.md=foo;Bmaster=a| +x=U:C3-2 s=Sa:a;Bmaster=3;Os W README.md=8`, + message: "foo", + all: true, + expected: ` +x=U:Cfoo\n#x-2 s=Sa:s;Bmaster=x;Os Cfoo\n#s-1 README.md=8`, + }, + "amend with all and untracked": { + initial: ` +a=B:Ca-1 README.md=foo;Bmaster=a| +x=U:C3-2 s=Sa:a;Bmaster=3;Os W README.md=8,foo=bar`, + message: "foo", + all: true, + expected: ` +x=U:Cfoo\n#x-2 s=Sa:s;Bmaster=x;Os Cfoo\n#s-1 README.md=8!W foo=bar`, }, "mismatch": { initial: ` From b51f1248b86be148a807004cbcdab0fc307691a2 Mon Sep 17 00:00:00 2001 From: Shihui Xiang Date: Wed, 7 Mar 2018 12:23:15 -0500 Subject: [PATCH 054/402] #501 git meta rebase should invoke the checkout hook --- node/lib/cmd/rebase.js | 4 ---- node/lib/util/rebase_util.js | 7 ++++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/node/lib/cmd/rebase.js b/node/lib/cmd/rebase.js index 75985b01b..27d252543 100644 --- a/node/lib/cmd/rebase.js +++ b/node/lib/cmd/rebase.js @@ -30,7 +30,6 @@ */ const co = require("co"); -const Hook = require("../util/hook"); /** * This module contains methods for implementing the `rebase` command. @@ -111,7 +110,4 @@ exports.executeableSubcommand = co.wrap(function *(args) { const commit = yield repo.getCommit(committish.id()); yield RebaseUtil.rebase(repo, commit); } - - // Run post-rewrite hook with "rebase" as args, means rebase command invoked this hook. - yield Hook.execHook("post-rewrite", ["rebase"]); }); diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index 2385f4e2e..c23f722bc 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -41,6 +41,7 @@ const DeinitUtil = require("./deinit_util"); const DoWorkQueue = require("../util/do_work_queue"); const Open = require("./open"); const GitUtil = require("./git_util"); +const Hook = require("../util/hook"); const RepoStatus = require("./repo_status"); const RebaseFileUtil = require("./rebase_file_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -554,7 +555,11 @@ up-to-date.`); }; })); }); - return yield driveRebase(metaRepo, initialize, fromCommit, commit); + const result = yield driveRebase(metaRepo, initialize, fromCommit, commit); + + // Run post-rewrite hook with "rebase" as args, means rebase command invoked this hook. + yield Hook.execHook("post-rewrite", ["rebase"]); + return result; }); /** From 9439a577559bafa0cc7d061c296ba7d9594f38b9 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 7 Mar 2018 13:22:26 -0500 Subject: [PATCH 055/402] Fix stylistic problems flagged by jshint --- node/lib/cmd/checkout.js | 8 +++++--- node/lib/cmd/merge.js | 3 ++- node/lib/util/rebase_util.js | 4 +++- node/lib/util/tree_util.js | 2 +- node/test/util/hook.js | 5 +++-- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index 333f2d4e4..4c773b681 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -139,9 +139,11 @@ exports.executeableSubcommand = co.wrap(function *(args) { // Run post-checkout hook. // Note: The hook is given three parameters: the ref of the previous HEAD, - // the ref of the new HEAD (which may or may not have changed), - // and a flag indicating whether the checkout was a branch checkout (changing branches, flag = "1"), - // or a file checkout (retrieving a file from the index, flag = "0"). + // the ref of the new HEAD (which may or may not have changed), and a flag + // indicating whether the checkout was a branch checkout (changing + // branches, flag = "1"), or a file checkout (retrieving a file from the + // index, flag = "0"). + const headId = yield repo.getHeadCommit(); const oldHead = headId.id().tostrS(); const newHead = op.commit; diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index 35619eb12..873b0cf1a 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -167,6 +167,7 @@ Merge of '${args.commit}' // Run post-merge hook if merge successfully. // Fixme: --squash is not supported yet, once supported, need to parse 0/1 - // as arg into the post-merge hook, 1 means it is a squash merge, 0 means not. + // as arg into the post-merge hook, 1 means it is a squash merge, 0 means + // not. yield Hook.execHook("post-merge", ["0"]); }); diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index c23f722bc..9567b0368 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -557,7 +557,9 @@ up-to-date.`); }); const result = yield driveRebase(metaRepo, initialize, fromCommit, commit); - // Run post-rewrite hook with "rebase" as args, means rebase command invoked this hook. + // Run post-rewrite hook with "rebase" as args, means rebase command + // invoked this hook. + yield Hook.execHook("post-rewrite", ["rebase"]); return result; }); diff --git a/node/lib/util/tree_util.js b/node/lib/util/tree_util.js index 4e769eba3..916417d7f 100644 --- a/node/lib/util/tree_util.js +++ b/node/lib/util/tree_util.js @@ -250,7 +250,7 @@ exports.listWorkdirChanges = co.wrap(function *(repo, status, includeUnstaged) { yield fs.access(fullpath, fs.constants.X_OK); } catch (e) { // if unable to execute, use BLOB. - filemode = FILEMODE.BLOB + filemode = FILEMODE.BLOB; } switch (workdir[subpath]) { case FILESTATUS.ADDED: diff --git a/node/test/util/hook.js b/node/test/util/hook.js index c067b49a1..461be1dbb 100644 --- a/node/test/util/hook.js +++ b/node/test/util/hook.js @@ -58,10 +58,11 @@ describe("Hook", function () { process.chdir(workDir); const hooksDir = path.join(workDir, ".git/hooks"); const hookFile = path.join(hooksDir, hookName); - yield fs.writeFile(hookFile, "#!/bin/bash \necho 'it is a test hook'\n"); + yield fs.writeFile(hookFile, + "#!/bin/bash \necho 'it is a test hook'\n"); yield fs.chmod(hookFile, "755"); const result = yield Hook.execHook(hookName); assert.equal(result, "it is a test hook\n"); })); }); -}); \ No newline at end of file +}); From dadbf32fb5d176c4c0c991d3a5537cb22f2bf7a9 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 8 Mar 2018 18:02:27 -0500 Subject: [PATCH 056/402] Fix flaky `StashUtil.removeStash` It was missing a yield statement to where I was pointing the stash reference to a new commit. When the yield is added, the test failed because we ended up with a duplicate entry in the reflog. I believe that what was happening when the test succeeded was that the reference itself was updated but that the program exited before the reflog entry was added. After adding the `yield`, I had to add another statement to remove the duplicate entry. I validated that the test no longer flaked by running it in a loop in bash. Previously it would fail every second or third time. --- node/lib/util/stash_util.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 272971018..b5653546f 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -390,11 +390,16 @@ exports.removeStash = co.wrap(function *(repo, index) { if (0 === index) { if (count > 1) { const entry = log.entryByIndex(0); - NodeGit.Reference.create(repo, - metaStashRef, - entry.idNew(), - 1, - "removeStash"); + yield NodeGit.Reference.create(repo, + metaStashRef, + entry.idNew(), + 1, + "removeStash"); + // But then in doing so, we've written a new entry for the ref, + // remove the old one. + + log.drop(1, 1 /* rewrite previous entry */); + log.write(); } else { NodeGit.Reference.remove(repo, metaStashRef); From 145e3631cbce7b103799563e9eb6db6de77c4b00 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 7 Mar 2018 15:52:18 -0500 Subject: [PATCH 057/402] Fix cherry-pick This changes addresses many outstanding cherry-pick problems, including: * `--continue` and `--abort`: https://github.com/twosigma/git-meta/issues/462 * picking meta-commits that introduce multiple submodule commits in one repo: https://github.com/twosigma/git-meta/issues/361 * failures in libgit2 touching deleted submodules: https://github.com/twosigma/git-meta/issues/360 * handling additions: https://github.com/twosigma/git-meta/issues/337 * state cleanup https://github.com/twosigma/git-meta/issues/224 Key improvements: * Track that a cherry-pick is in-progress to support --continue and --abort * Stop using `libgit2` cherry-pick in the meta-repo. This lets us avoid doing the expensive "cacheSubmodules" call in the meta-repo and gives us more control over what happens. We also avoid any potential submodule bugs re. cherry-picking in libgit2 this way. Using libgit2 cherry-pick didn't buy us a whole lot in the meta-repo anyway. * Use 3-way rebase in the submodules instead of cherry-pick so that we can get all changes introduced by commit. * Minor stability improvements to merge and rebase. * More consistent checks that we are in a good state to initiate operations, e.g., that there are no existing conflicts or cherry-picks before starting a merge. * Better handling of conflicts in general * Better testing of rebase. Factored out some parts to use with cherry-pick and wrote separate tests for them. * Performance improvements to operations that were reading the shas of all submodules when they actually needed only a subset. Additionally, this change lays the groundwork to go back and make further improvements to `merge` and `rebase` (e.g., to make `-i`) should we choose to. Derived implementation changes: * Introduced a type: `CherryPick` for recording a cherry-pick * Support for reading and writing cherry-pick state in `cherry_pick_fileUtil.js`. * Test framework support for cherry-picks * Test framework support for conflicts --- node/lib/cmd/cherry_pick.js | 141 ++++ node/lib/cmd/cherrypick.js | 104 --- node/lib/cmd/stash.js | 1 + node/lib/git-meta.js | 2 +- node/lib/util/add_submodule.js | 9 +- node/lib/util/cherry_pick.js | 94 +++ node/lib/util/cherry_pick_file_util.js | 105 +++ node/lib/util/cherry_pick_util.js | 576 +++++++++++++++++ node/lib/util/cherrypick.js | 172 ----- node/lib/util/commit.js | 3 +- node/lib/util/conflict_util.js | 139 ++++ node/lib/util/diff_util.js | 11 +- node/lib/util/merge_file_util.js | 6 +- node/lib/util/merge_util.js | 15 +- node/lib/util/print_status_util.js | 76 ++- node/lib/util/read_repo_ast_util.js | 76 ++- node/lib/util/rebase_util.js | 275 +++++--- node/lib/util/repo_ast.js | 161 ++++- node/lib/util/repo_ast_test_util.js | 47 ++ node/lib/util/repo_ast_util.js | 58 +- node/lib/util/repo_status.js | 125 +++- node/lib/util/reset.js | 3 +- node/lib/util/shorthand_parser_util.js | 127 +++- node/lib/util/stash_util.js | 8 +- node/lib/util/status_util.js | 130 +++- node/lib/util/submodule_config_util.js | 16 + node/lib/util/submodule_util.js | 48 +- node/lib/util/synthetic_branch_util.js | 5 +- node/lib/util/write_repo_ast_util.js | 60 +- node/test/util/add_submodule.js | 79 +-- node/test/util/cherry_pick.js | 71 ++ node/test/util/cherry_pick_file_util.js | 82 +++ node/test/util/cherry_pick_util.js | 828 ++++++++++++++++++++++++ node/test/util/cherrypick.js | 157 ----- node/test/util/commit.js | 4 +- node/test/util/conflict_util.js | 138 ++++ node/test/util/diff_util.js | 7 +- node/test/util/merge_util.js | 20 +- node/test/util/print_status_util.js | 28 +- node/test/util/read_repo_ast_util.js | 195 ++++++ node/test/util/rebase_util.js | 335 +++++++++- node/test/util/repo_ast.js | 136 +++- node/test/util/repo_ast_test_util.js | 51 ++ node/test/util/repo_ast_util.js | 159 ++++- node/test/util/repo_status.js | 81 +++ node/test/util/shorthand_parser_util.js | 95 +++ node/test/util/status_util.js | 127 ++++ node/test/util/submodule_config_util.js | 12 + node/test/util/submodule_util.js | 175 ++++- node/test/util/write_repo_ast_util.js | 5 + 50 files changed, 4629 insertions(+), 749 deletions(-) create mode 100644 node/lib/cmd/cherry_pick.js delete mode 100644 node/lib/cmd/cherrypick.js create mode 100644 node/lib/util/cherry_pick.js create mode 100644 node/lib/util/cherry_pick_file_util.js create mode 100644 node/lib/util/cherry_pick_util.js delete mode 100644 node/lib/util/cherrypick.js create mode 100644 node/lib/util/conflict_util.js create mode 100644 node/test/util/cherry_pick.js create mode 100644 node/test/util/cherry_pick_file_util.js create mode 100644 node/test/util/cherry_pick_util.js delete mode 100644 node/test/util/cherrypick.js create mode 100644 node/test/util/conflict_util.js diff --git a/node/lib/cmd/cherry_pick.js b/node/lib/cmd/cherry_pick.js new file mode 100644 index 000000000..8b18a6634 --- /dev/null +++ b/node/lib/cmd/cherry_pick.js @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const co = require("co"); +const Hook = require("../util/hook"); + +/** + * This module contains methods for implementing the `cherry-pick` command. + */ + +/** + * help text for the `cherry-pick` command + * @property {String} + */ +exports.helpText = `copy one or more existing commits, creating new commits.`; + +/** + * description of the `cherry-pick` command + * @property {String} + */ +exports.description =` +Apply a commit to the HEAD of the current branch. This command will not +execute if any visible repositories have uncommitted modifications. +Cherry-pick looks at the changes introduced by this commit and +applies them to HEAD, rewriting new submodule commits if they cannot be +fast-forwarded.`; + +exports.configureParser = function (parser) { + parser.addArgument(["commit"], { + nargs: "?", + type: "string", + help: "the commit to cherry-pick", + }); + parser.addArgument(["--continue"], { + action: "storeConst", + constant: true, + help: "continue an in-progress cherry-pick", + }); + // TODO: Note that ideally we might do something similar to + // `git reset --merge`, but that would be (a) tricky and (b) it can fail, + // leaving the cherry-pick still in progress. + + parser.addArgument(["--abort"], { + action: "storeConst", + constant: true, + help: `\ +abort the cherry-pick and return to previous state, throwing away all changes`, + }); +}; + +/** + * Execute the `cherry-pick` command according to the specified `args`. + * + * @async + * @param {Object} args + * @param {String[]} args.commit + */ +exports.executeableSubcommand = co.wrap(function *(args) { + const colors = require("colors"); + + const CherryPickUtil = require("../util/cherry_pick_util"); + const GitUtil = require("../util/git_util"); + const UserError = require("../util/user_error"); + + const repo = yield GitUtil.getCurrentRepo(); + + if (args.continue + args.abort > 1) { + throw new UserError("Cannot use continue and abort together."); + } + + if (args.continue) { + if (null !== args.commit) { + throw new UserError("Cannot specify a commit with '--continue'."); + } + const result = yield CherryPickUtil.continue(repo); + if (null !== result.errorMessage) { + throw new UserError(result.errorMessage); + } + return; // RETURN + } + + if (args.abort) { + if (null !== args.commit) { + throw new UserError("Cannot specify a commit with '--abort'."); + } + yield CherryPickUtil.abort(repo); + return; // RETURN + } + + if (null === args.commit) { + throw new UserError("No commit to cherry-pick."); + } + + const commitish = args.commit; + const annotated = yield GitUtil.resolveCommitish(repo, commitish); + if (null === annotated) { + throw new UserError(`\ +Could not resolve ${colors.red(commitish)} to a commit.`); + } + else { + const id = annotated.id(); + console.log(`Cherry-picking commit ${colors.green(id.tostrS())}.`); + const commit = yield repo.getCommit(id); + const result = yield CherryPickUtil.cherryPick(repo, commit); + if (null !== result.errorMessage) { + throw new UserError(result.errorMessage); + } + } + + // Run post-commit hook as regular git. + yield Hook.execHook("post-commit"); +}); diff --git a/node/lib/cmd/cherrypick.js b/node/lib/cmd/cherrypick.js deleted file mode 100644 index 016395146..000000000 --- a/node/lib/cmd/cherrypick.js +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2016, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -const co = require("co"); -const Hook = require("../util/hook"); - -/** - * This module contains methods for implementing the `cherry-pick` command. - */ - -/** - * help text for the `cherry-pick` command - * @property {String} - */ -exports.helpText = `copy one or more existing commits, creating new commits.`; - -/** - * description of the `cherry-pick` command - * @property {String} - */ -exports.description =` -Apply some commits to the HEAD of the current branch. This command will not -execute if any visible repositories, including the meta-repository, have -uncommitted modifications. Each commit must identify a change in the -meta-repository. For each commit specified, cherry-pick the changes identified -in that commit to the meta-repository. If the change indicates new commits -in a sub-repository, cherry-pick those changes in the respective -sub-repository, opening it if necessary. Only after all sub-repository commits -have been "picked" will the commit in the meta-repository be made.`; - -exports.configureParser = function (parser) { - parser.addArgument(["commits"], { - nargs: "+", - type: "string", - help: "the commits to cherry-pick", - }); -}; - -/** - * Execute the `cherry-pick` command according to the specified `args`. - * - * @async - * @param {Object} args - * @param {String[]} args.commits - */ -exports.executeableSubcommand = co.wrap(function *(args) { - // TODO: - // - abort - // - continue - - const colors = require("colors"); - - const CherryPick = require("../util/cherrypick"); - const GitUtil = require("../util/git_util"); - - const repo = yield GitUtil.getCurrentRepo(); - - for (let i = 0; i < args.commits.length; ++i) { - let commitish = args.commits[i]; - let result = yield GitUtil.resolveCommitish(repo, commitish); - if (null === result) { - console.error(`Could not resolve ${colors.red(commitish)} to a \ -commit.`); - process.exit(-1); - } - else { - console.log(`Cherry-picking commit ${colors.green(result.id())}.`); - let commit = yield repo.getCommit(result.id()); - yield CherryPick.cherryPick(repo, commit); - } - } - - // Run post-commit hook as regular git. - yield Hook.execHook("post-commit"); -}); diff --git a/node/lib/cmd/stash.js b/node/lib/cmd/stash.js index 3294e7bf4..2d601f6ee 100644 --- a/node/lib/cmd/stash.js +++ b/node/lib/cmd/stash.js @@ -118,6 +118,7 @@ const doSave = co.wrap(function *(args) { const repo = yield GitUtil.getCurrentRepo(); const status = yield StatusUtil.getRepoStatus(repo); + StatusUtil.ensureReady(status); const includeUntracked = args.include_untracked || false; if (cleanSubs(status, includeUntracked)) { console.warn("Nothing to stash."); diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js index a2e62ecad..e79fec7bb 100755 --- a/node/lib/git-meta.js +++ b/node/lib/git-meta.js @@ -41,7 +41,7 @@ const NodeGit = require("nodegit"); const add = require("./cmd/add"); const addSubmodule = require("./cmd/add_submodule"); const checkout = require("./cmd/checkout"); -const cherryPick = require("./cmd/cherrypick"); +const cherryPick = require("./cmd/cherry_pick"); const close = require("./cmd/close"); const commit = require("./cmd/commit"); const Forward = require("./cmd/forward"); diff --git a/node/lib/util/add_submodule.js b/node/lib/util/add_submodule.js index e4a543e25..afccdfc03 100644 --- a/node/lib/util/add_submodule.js +++ b/node/lib/util/add_submodule.js @@ -33,9 +33,7 @@ const assert = require("chai").assert; const co = require("co"); const colors = require("colors"); -const fs = require("fs-promise"); const NodeGit = require("nodegit"); -const path = require("path"); const GitUtil = require("./git_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -63,15 +61,10 @@ exports.addSubmodule = co.wrap(function *(repo, url, filename, importArg) { assert.isString(importArg.url); assert.isString(importArg.branch); } - const modulesPath = path.join(repo.workdir(), - SubmoduleConfigUtil.modulesFileName); const index = yield repo.index(); const urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); urls[filename] = url; - const newConf = SubmoduleConfigUtil.writeConfigText(urls); - yield fs.writeFile(modulesPath, newConf); - - yield index.addByPath(SubmoduleConfigUtil.modulesFileName); + yield SubmoduleConfigUtil.writeUrls(repo, index, urls); yield index.write(); const metaUrl = yield GitUtil.getOriginUrl(repo); diff --git a/node/lib/util/cherry_pick.js b/node/lib/util/cherry_pick.js new file mode 100644 index 000000000..0e0bcd8f4 --- /dev/null +++ b/node/lib/util/cherry_pick.js @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; + +/** + * This module defines the `CherryPick` value-semantic type. + */ + +/** + * @class CherryPick + * + * This class represents an in-progress cherry-pick. + */ +class CherryPick { + /** + * Create a new `CherryPick` object. + * + * @param {String} originalHead + * @param {String} picked + */ + constructor(originalHead, picked) { + assert.isString(originalHead); + assert.isString(picked); + this.d_originalHead = originalHead; + this.d_picked = picked; + Object.freeze(this); + } + + /** + * @property {String} originalHead head commit when cherry-pick started + */ + get originalHead() { + return this.d_originalHead; + } + + /** + * @property {String} picked sha of commit being cherry-picked + */ + get picked() { + return this.d_picked; + } + + /** + * Return true if the specified `rhs` represents the same value as this + * `CherryPick` object and false otherwise. Two `CherryPick` objects + * represent the same value if they have the same `originalHead` and + * `picked` properties. + * + * @param {CherryPick} rhs + * @return {Bool} + */ + equal(rhs) { + assert.instanceOf(rhs, CherryPick); + return this.d_originalHead === rhs.d_originalHead && + this.d_picked === rhs.d_picked; + } +} + +CherryPick.prototype.toString = function () { + return `CherryPick(originalHead=${this.d_originalHead}, \ +picked=${this.d_picked})`; +}; + +module.exports = CherryPick; diff --git a/node/lib/util/cherry_pick_file_util.js b/node/lib/util/cherry_pick_file_util.js new file mode 100644 index 000000000..f67a59784 --- /dev/null +++ b/node/lib/util/cherry_pick_file_util.js @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const fs = require("fs-promise"); +const path = require("path"); +const rimraf = require("rimraf"); + +const CherryPick = require("./cherry_pick"); + +/** + * This module contains methods for accessing files that pertain to in-progress + * cherry-picks. + */ + +const metaCherryPickDir = "META_CHERRY_PICK"; +const originalHeadFile = "ORIG_HEAD"; +const pickFile = "PICK"; + +/** + * Return the `CherryPick` object in the specified `gitDir`, if one exists, or + * null if there is no cherry-pick in progress. + * + * @param {String} gitDir + * @return {String|null} + */ +exports.readCherryPick = co.wrap(function *(gitDir) { + assert.isString(gitDir); + let originalHead; + let pick; + const root = path.join(gitDir, metaCherryPickDir); + try { + originalHead = yield fs.readFile(path.join(root, originalHeadFile), + "utf8"); + pick = yield fs.readFile(path.join(root, pickFile), "utf8"); + } + catch (e) { + // TODO: Emit diagnostic if directory exists but is malformed. + return null; + } + return new CherryPick(originalHead.split("\n")[0], pick.split("\n")[0]); +}); + +/** + * Write the specified `cherryPick` to the specified `gitDir`. The behavior is + * undefined if there is already a cherry-pick recorded in `gitDir`. + * + * @param {String} gitDir + * @param {CherryPick} cherryPick + */ +exports.writeCherryPick = co.wrap(function *(gitDir, cherryPick) { + assert.isString(gitDir); + assert.instanceOf(cherryPick, CherryPick); + + const root = path.join(gitDir, metaCherryPickDir); + yield fs.mkdir(root); + yield fs.writeFile(path.join(root, originalHeadFile), + cherryPick.originalHead + "\n"); + yield fs.writeFile(path.join(root, pickFile), + cherryPick.picked + "\n"); +}); + +/** + * Remove files related to a meta cherry-pick. + * + * @param {String} gitDir + */ +exports.cleanCherryPick = co.wrap(function *(gitDir) { + assert.isString(gitDir); + const root = path.join(gitDir, metaCherryPickDir); + const promise = new Promise(callback => { + return rimraf(root, {}, callback); + }); + yield promise; +}); diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js new file mode 100644 index 000000000..90f723ffa --- /dev/null +++ b/node/lib/util/cherry_pick_util.js @@ -0,0 +1,576 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const colors = require("colors"); +const mkdirp = require("mkdirp"); +const NodeGit = require("nodegit"); +const path = require("path"); +const rimraf = require("rimraf"); + +const CherryPick = require("./cherry_pick"); +const CherryPickFileUtil = require("./cherry_pick_file_util"); +const ConflictUtil = require("./conflict_util"); +const DeinitUtil = require("./deinit_util"); +const DoWorkQueue = require("./do_work_queue"); +const GitUtil = require("./git_util"); +const Open = require("./open"); +const RebaseUtil = require("./rebase_util"); +const Reset = require("./reset"); +const StatusUtil = require("./status_util"); +const Submodule = require("./submodule"); +const SubmoduleConfigUtil = require("./submodule_config_util"); +const SubmoduleUtil = require("./submodule_util"); +const TreeUtil = require("./tree_util"); +const UserError = require("./user_error"); + +/** + * Change the specified `submodules` in the specified index. If a name maps to + * a `Submodule`, update it in the specified `index` in the specified `repo` + * and if that submodule is open, reset its HEAD, index, and worktree to + * reflect that commit. Otherwise, if it maps to `null`, remove it and deinit + * it if it's open. Obtain submodule repositories from the specified `opener`. + * The behavior is undefined if any referenced submodule is open and has index + * or workdir modifications. + * + * @param {NodeGit.Repository} repo + * @param {Open.Opener} opener + * @param {NodeGit.Index} index + * @param {Object} submodules name to Submodule + */ +exports.changeSubmodules = co.wrap(function *(repo, + opener, + index, + submodules) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(opener, Open.Opener); + assert.instanceOf(index, NodeGit.Index); + assert.isObject(submodules); + if (0 === Object.keys(submodules).count) { + return; // RETURN + } + const urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + const changes = {}; + function rmrf(dir) { + return new Promise(callback => { + return rimraf(path.join(repo.workdir(), dir), {}, callback); + }); + } + const fetcher = yield opener.fetcher(); + for (let name in submodules) { + const sub = submodules[name]; + if (null === sub) { + yield DeinitUtil.deinit(repo, name); + changes[name] = null; + delete urls[name]; + yield rmrf(name); + } + else if (yield opener.isOpen(name)) { + const subRepo = yield opener.getSubrepo(name); + yield fetcher.fetchSha(subRepo, name, sub.sha); + const commit = yield subRepo.getCommit(sub.sha); + yield GitUtil.setHeadHard(subRepo, commit); + yield index.addByPath(name); + } else { + changes[name] = new TreeUtil.Change( + NodeGit.Oid.fromString(sub.sha), + NodeGit.TreeEntry.FILEMODE.COMMIT); + urls[name] = sub.url; + const subPath = path.join(repo.workdir(), name); + mkdirp.sync(subPath); + } + } + const parentTreeId = yield index.writeTree(); + const parentTree = yield repo.getTree(parentTreeId); + const newTree = yield TreeUtil.writeTree(repo, parentTree, changes); + yield index.readTree(newTree); + yield SubmoduleConfigUtil.writeUrls(repo, index, urls); +}); + +/** + * Return true if the specified `commit` in the specified `repo` contains any + * URL changes and false otherwise. A URL change is an alteration to a + * submodule's URL in the `.gitmodules` file that is not an addition or + * removal. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + * @return {Bool} + */ +exports.containsUrlChanges = co.wrap(function *(repo, commit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(commit, NodeGit.Commit); + + let parentUrls = {}; + const parents = yield commit.getParents(); + if (0 !== parents.length) { + parentUrls = + yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, parents[0]); + } + const commitUrls = + yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, commit); + for (let name in parentUrls) { + const parentUrl = parentUrls[name]; + const commitUrl = commitUrls[name]; + if (undefined !== commitUrl && parentUrl !== commitUrl) { + return true; // RETURN + } + } + return false; +}); + +/** + * Return the entry for the specified `path` in the optionally specified `tree` + * or null if `null === tree` or `path` does not exist in `tree`. + * + * @param {NodeGit.Tree|null} tree + * @param {String} path + * @return {NodeGit.TreeEntry} + */ +const getTreeEntry = co.wrap(function *(tree, path) { + if (null === tree) { + return null; + } + try { + return yield tree.entryByPath(path); + } catch (e) { + // only way to tell if entry doesn't exist + } + return null; +}); + +/** + * Return the tree corresponding to the merge-base for the specified `lhs` and + * `rhs` commits in the specified `repo`, or null if there is no ancestor + * commit in common between `lhs` and `rhs`. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} lhs + * @param {NodeGit.Commit} rhs + * @return {NodeGit.Tree|null} + */ +const getMergeBaseTree = co.wrap(function *(repo, lhs, rhs) { + let baseId; + try { + baseId = yield NodeGit.Merge.base(repo, lhs.id(), rhs.id()); + } catch (e) { + // only way to detect lack of base + return null; + } + const mergeBase = yield repo.getCommit(baseId); + return yield mergeBase.getTree(); +}); + +/** + * Determine how to apply the submodule changes introduced in the + * specified `commit` to the commit on the head of the specified `repo`. + * Return an object describing what changes to make, including which submodules + * cannot be updated at all due to a conflicts, such as a change being + * introduced to a submodule that does not exist in HEAD. Throw a `UserError` + * if non-submodule changes are detected. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + * @return {Object} return + * @return {Object} return.changes from sub name to `SubmoduleChange` + * @return {Object} return.simpleChanges from sub name to `Submodule` or null + * @return {Object} return.conflicts map from sub name to `Conflict` + */ +exports.computeChanges = co.wrap(function *(repo, commit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(commit, NodeGit.Commit); + const head = yield repo.getHeadCommit(); + const headTree = yield head.getTree(); + const baseTree = yield getMergeBaseTree(repo, head, commit); + const changes = yield SubmoduleUtil.getSubmoduleChanges(repo, + commit, + false); + const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, + commit); + const result = { + changes: {}, + simpleChanges: {}, + conflicts: {}, + }; + + const ConflictEntry = ConflictUtil.ConflictEntry; + const Conflict = ConflictUtil.Conflict; + const FILEMODE = NodeGit.TreeEntry.FILEMODE; + + yield Object.keys(changes).map(co.wrap(function *(sub) { + const change = changes[sub]; + const headEntry = yield getTreeEntry(headTree, sub); + + const makeConflict = co.wrap(function *() { + const baseEntry = yield getTreeEntry(baseTree, sub); + let ancestor = null; + if (null !== baseEntry) { + ancestor = new ConflictEntry(baseEntry.filemode(), + baseEntry.sha()); + } + let our = null; + if (null !== headEntry) { + our = new ConflictEntry(headEntry.filemode(), headEntry.sha()); + } + let their = null; + if (null !== change.newSha) { + their = new ConflictEntry(FILEMODE.COMMIT, change.newSha); + } + return new Conflict(ancestor, our, their); + }); + + if (null === headEntry) { + // If doesn't exist on HEAD, only valid change is an addition; + // ignore a removal. + + if (null === change.oldSha) { + result.simpleChanges[sub] = new Submodule(urls[sub], + change.newSha); + } else if (null !== change.newSha) { + result.conflicts[sub] = yield makeConflict(); + } + } else if (FILEMODE.COMMIT !== headEntry.filemode()) { + // If the path on HEAD is not a submodule, we have a conflict. + + result.conflicts[sub] = yield makeConflict(); + } else if (null === change.oldSha) { + // We have an addition. We've already covered the case where this + // sub doesn't exist on head, so it's going to be a conflict unless + // the SHA on HEAD is the same. + + if (headEntry.sha() !== change.newSha) { + result.conflicts[sub] = yield makeConflict(); + } + } else if (null === change.newSha) { + // Register a deletion unless the SHA was changed on HEAD. + + if (headEntry.sha() === change.oldSha) { + result.simpleChanges[sub] = null; + } else { + result.conflicts[sub] = yield makeConflict(); + } + } else if (change.newSha !== headEntry.sha()) { + // Finally, we have a normal update, new commits in the submodule. + // We still deem it to be a "simple" change not needing a + // cherry-pick if the old sha for the change is the same as that on + // head. + + if (change.oldSha === headEntry.sha()) { + result.simpleChanges[sub] = new Submodule(urls[sub], + change.newSha); + } else { + result.changes[sub] = change; + } + } + })); + return result; +}); + +/** + * Pick the specified `subs` in the specified `metaRepo` having the specified + * `metaIndex`. Stage new submodule commits in `metaRepo`. Return an object + * describing any commits that were generated and conflicted commits. Use the + * specified `opener` to open submodules. + * + * @param {NodeGit.Repository} metaRepo + * @param {Open.Opener} opener + * @param {NodeGit.Index} metaIndex + * @param {Object} subs map from name to SubmoduleChange + * @return {Object} + * @return {Object} return.commits map from name to map from new to old ids + * @return {Object} return.conflicts map from name to commit causing conflict + */ +exports.pickSubs = co.wrap(function *(metaRepo, opener, metaIndex, subs) { + assert.instanceOf(metaRepo, NodeGit.Repository); + assert.instanceOf(opener, Open.Opener); + assert.instanceOf(metaIndex, NodeGit.Index); + assert.isObject(subs); + const result = { + commits: {}, + conflicts: {}, + }; + const fetcher = yield opener.fetcher(); + const pickSub = co.wrap(function *(name) { + const repo = yield opener.getSubrepo(name); + const change = subs[name]; + const commitText = "(" + GitUtil.shortSha(change.oldSha) + ".." + + GitUtil.shortSha(change.newSha) + "]"; + console.log(`Sub-repo ${colors.blue(name)}: cherry-picking commits \ +${colors.green(commitText)}.`); + + // Fetch the commit; it may not be present. + yield fetcher.fetchSha(repo, name, change.newSha); + yield fetcher.fetchSha(repo, name, change.oldSha); + const newCommit = yield repo.getCommit(change.newSha); + const oldCommit = yield repo.getCommit(change.oldSha); + const rewriteResult = yield RebaseUtil.rewriteCommits(repo, + newCommit, + oldCommit); + result.commits[name] = rewriteResult.commits; + yield metaIndex.addByPath(name); + if (null !== rewriteResult.conflictedCommit) { + result.conflicts[name] = rewriteResult.conflictedCommit; + } + }); + yield DoWorkQueue.doInParallel(Object.keys(subs), pickSub); + return result; +}); + +/** + * Write the specified `conflicts` to the specified `index` in the specified + * `repo`. If `conflicts` is non-empty, return a non-empty string desribing + * them. Otherwise, return the empty string. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Index} index + * @param {Object} conflicts from sub name to `Conflict` + * @return {String} + */ +exports.writeConflicts = co.wrap(function *(repo, index, conflicts) { + let errorMessage = ""; + const names = Object.keys(conflicts).sort(); + for (let name of names) { + yield ConflictUtil.addConflict(index, name, conflicts[name]); + errorMessage += `\ +Conflicting entries for submodule ${colors.red(name)} +`; + } + return errorMessage; +}); + +/** + * Finish the cherry-pick in progress in the specified `repo` having the + * specified `index`. Use the message and signature from the specified + * original `commit`. Return the sha of the newly-created commit. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Index} index + * @param {NodeGit.Commit} commit + * @return {String} + */ +const finish = co.wrap(function *(repo, commit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(commit, NodeGit.Commit); + + repo.stateCleanup(); + const defaultSig = repo.defaultSignature(); + const metaCommit = yield repo.createCommitOnHead([], + defaultSig, + commit.committer(), + commit.message()); + yield CherryPickFileUtil.cleanCherryPick(repo.path()); + console.log("Cherry-pick complete."); + return metaCommit.tostrS(); +}); + +/** + * Cherry-pick the specified `commit` in the specified `metaRepo`. Return an + * object with the cherry-picked commits ids. This object contains the id of + * the newly-generated meta-repo commit and for each sub-repo, a map from + * new (cherry-pick) sha to the original commit sha. Throw a `UserError` if + * the repository is not in a state that can allow a cherry-pick (e.g., it's + * rebasing), if `commit` contains changes that we cannot cherry-pick (e.g., + * URL-only changes), or if the cherry-pick would result in no changes (TODO: + * provide support for '--allow-empty' if needed). If the cherry-pick is + * initiated but results in a conflicts, the `errorMessage` of the returned + * object will be non-null and will contain a description of the conflicts. + * + * @async + * @param {NodeGit.Repository} metaRepo + * @param {NodeGit.Commit} commit + * @return {Object} return + * @return {String} return.newMetaCommit + * @return {Object} returm.submoduleCommits + * @return {String|null} return.errorMessage + */ +exports.cherryPick = co.wrap(function *(metaRepo, commit) { + assert.instanceOf(metaRepo, NodeGit.Repository); + assert.instanceOf(commit, NodeGit.Commit); + + const status = yield StatusUtil.getRepoStatus(metaRepo); + StatusUtil.ensureReady(status); + + // First, perform sanity checks to see if the repo is in a state that we + // can pick in and if `commit` is something that we can pick. + + if (!status.isDeepClean(false)) { + // TODO: Git will refuse to run if there are staged changes, but will + // attempt a cherry-pick if there are just workdir changes. We should + // support this in the future, but it basically requires us to dry-run + // the rebases in all the submodules, and I'm uncertain how to do that + // at the moment. + + throw new UserError(`\ +The repository has uncommitted changes. Please stash or commit them before +running cherry-pick.`); + } + + const hasUrlChanges = yield exports.containsUrlChanges(metaRepo, commit); + if (hasUrlChanges) { + // TODO: Dealing with these would be a huge hassle and is probably not + // worth it at the moment since the recommended policy for monorepo + // implementations is to prevent users from making URL changes anyway. + + throw new UserError(`\ +Cherry-picking commits with submodule URL changes is not currently supported. +Please try with normal 'git cherry-pick'.`); + } + + const changes = yield exports.computeChanges(metaRepo, commit); + const metaIndex = yield metaRepo.index(); + + // We're going to attempt a cherry-pick if we've made it this far, record a + // cherry-pick file. + + const head = yield metaRepo.getHeadCommit(); + const cherryPick = new CherryPick(head.id().tostrS(), + commit.id().tostrS()); + yield CherryPickFileUtil.writeCherryPick(metaRepo.path(), cherryPick); + + + const opener = new Open.Opener(metaRepo, null); + + // Perform simple changes that don't require picks -- addition, deletions, + // and fast-forwards. + + yield exports.changeSubmodules(metaRepo, + opener, + metaIndex, + changes.simpleChanges); + + // Render any conflicts + + let errorMessage = + yield exports.writeConflicts(metaRepo, metaIndex, changes.conflicts); + + // Then do the cherry-picks. + + const picks = yield exports.pickSubs(metaRepo, + opener, + metaIndex, + changes.changes); + + const conflicts = picks.conflicts; + Object.keys(conflicts).sort().forEach(name => { + errorMessage += `Submodule ${colors.red(name)} is conflicted.\n`; + }); + + const result = { + submoduleCommits: picks.commits, + errorMessage: errorMessage === "" ? null : errorMessage, + newMetaCommit: null, + }; + yield metaIndex.write(); + if ("" === errorMessage) { + if (0 === Object.keys(changes.simpleChanges).length && + 0 === Object.keys(picks.commits).length) { + yield CherryPickFileUtil.cleanCherryPick(metaRepo.path()); + throw new UserError("Nothing to commit."); + } + result.newMetaCommit = yield finish(metaRepo, commit); + } + return result; +}); + +/** + * Continue the in-progress cherry-pick in the specified `repo`. Throw a + * `UserError` if the continue cannot be initiated, e.g., because there is not + * a cherry-pick in progress or there are still conflicts. Return an object + * describing the commits that were made and any errors that were generated. + * + * @async + * @param {NodeGit.Repository} repo + * @return {Object} return + * @return {String|null} return.newMetaCommit + * @return {Object} returm.submoduleCommits + * @return {Object} returm.newSubmoduleCommits + * @return {String|null} return.errorMessage + */ +exports.continue = co.wrap(function *(repo) { + assert.instanceOf(repo, NodeGit.Repository); + + const status = yield StatusUtil.getRepoStatus(repo); + const cherryPick = status.cherryPick; + if (null === cherryPick) { + throw new UserError("No cherry-pick in progress."); + } + if (status.isConflicted()) { + throw new UserError("Resolve conflicts then continue cherry-pick."); + } + const index = yield repo.index(); + const commit = yield repo.getCommit(cherryPick.picked); + + const subResult = yield RebaseUtil.continueSubmodules(repo, + index, + status, + commit); + yield index.write(); + const result = { + newMetaCommit: null, + submoduleCommits: subResult.commits, + newSubmoduleCommits: subResult.newCommits, + errorMessage: subResult.errorMessage, + }; + yield index.write(); + if (null === subResult.errorMessage) { + if (status.isIndexDeepClean() && + 0 === Object.keys(subResult.commits).length && + 0 === Object.keys(subResult.newCommits).length) { + yield CherryPickFileUtil.cleanCherryPick(repo.path()); + throw new UserError("Nothing to commit."); + } + + result.newMetaCommit = yield finish(repo, commit); + } + return result; +}); + +/** + * Abort the cherry-pick in progress in the specified `repo` and return the + * repository to exactly the state of the initial commit. Throw a `UserError` + * if no cherry-pick is in progress. + * + * @param {NodeGit.Repository} repo + */ +exports.abort = co.wrap(function *(repo) { + assert.instanceOf(repo, NodeGit.Repository); + + const cherry = yield CherryPickFileUtil.readCherryPick(repo.path()); + if (null === cherry) { + throw new UserError("No cherry-pick in progress."); + } + const commit = yield repo.getCommit(cherry.originalHead); + yield Reset.reset(repo, commit, Reset.TYPE.HARD); + yield CherryPickFileUtil.cleanCherryPick(repo.path()); + console.log("Cherry-pick aborted."); +}); diff --git a/node/lib/util/cherrypick.js b/node/lib/util/cherrypick.js deleted file mode 100644 index e70a833e1..000000000 --- a/node/lib/util/cherrypick.js +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (c) 2016, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -const assert = require("chai").assert; -const co = require("co"); -const colors = require("colors"); -const NodeGit = require("nodegit"); - -const Open = require("../util/open"); -const SubmoduleUtil = require("../util/submodule_util"); -const UserError = require("../util/user_error"); - -/** - * Cherry-pick the specified `commit` in the specified `metaRepo`. Return an - * object with the cherry-picked commits ids. This object contains the id of - * the newly-generated meta-repo commit and for each sub-repo, a map from - * original commit sha to cherry-picked commit sha. The behavior is undefined - * unless the `metaRepo` is in a consistent state according to - * `Status.ensureCleanAndConsistent`. Throw a `UserError` object if a - * cherry-pick results in confict; do not generate a meta-repo commit in this - * case. - * - * @async - * @param {NodeGit.Repository} metaRepo - * @param {NodeGit.Commit} commit - * @return {Object} return - * @return {String} return.newMetaCommit - * @return {Object} returm.submoduleCommits - */ -exports.cherryPick = co.wrap(function *(metaRepo, commit) { - assert.instanceOf(metaRepo, NodeGit.Repository); - assert.instanceOf(commit, NodeGit.Commit); - - // TODO: handle possibility of a (single) meta-repo commit corresponding to - // multiple commits. - // TODO: See how we do with a variety of edge cases, e.g.: submodules added - // and removed. - // TODO: Deal with conflicts. - - // Basic algorithm: - // - start cherry-pick on meta-repo - // - detect changes in sub-repos - // - cherry-pick changes in sub-repos - // - if any conflicts in sub-repos, bail - // - finalize commit in meta-repo - - const head = yield metaRepo.getHeadCommit(); - const changes = yield SubmoduleUtil.getSubmoduleChanges(metaRepo, commit); - - yield SubmoduleUtil.cacheSubmodules(metaRepo, () => { - return NodeGit.Cherrypick.cherrypick(metaRepo, commit, {}); - }); - - let errorMessage = ""; - let indexChanged = false; - let pickers = []; - - const headSubs = yield SubmoduleUtil.getSubmodulesForCommit(metaRepo, - head); - const commitSubs = yield SubmoduleUtil.getSubmodulesForCommit(metaRepo, - commit); - - const metaIndex = yield metaRepo.index(); - const opener = new Open.Opener(metaRepo, null); - let submoduleCommits = {}; - const subFetcher = yield opener.fetcher(); - - const picker = co.wrap(function *(subName, headSha, commitSha) { - let commitMap = {}; - submoduleCommits[subName] = commitMap; - const repo = yield opener.getSubrepo(subName); - - console.log(`Sub-repo ${colors.blue(subName)}: cherry-picking commit \ -${colors.green(commitSha)}.`); - - // Fetch the commit; it may not be present. - - yield subFetcher.fetchSha(repo, subName, commitSha); - - const commit = yield repo.getCommit(commitSha); - yield NodeGit.Cherrypick.cherrypick(repo, commit, {}); - const index = yield repo.index(); - if (index.hasConflicts()) { - errorMessage += - `Submodule ${colors.red(subName)} is conflicted.\n`; - } - else { - repo.stateCleanup(); - const newCommit = yield repo.createCommitOnHead( - [], - commit.author(), - commit.committer(), - commit.message()); - yield metaIndex.addByPath(subName); - commitMap[commitSha] = newCommit.tostrS(); - indexChanged = true; - } - }); - - - // Cherry-pick each submodule changed in `commit`. - - Object.keys(changes).forEach(subName => { - const change = changes[subName]; - if (null !== change.oldSha && null !== change.newSha) { - const headSub = headSubs[subName]; - const commitSub = commitSubs[subName]; - const headSha = headSub.sha; - if (undefined !== commitSub && - headSha !== commitSub.sha) { - pickers.push(picker(subName, headSha, commitSub.sha)); - } - } - }); - - // Then execute the submodule pickers in parallel. - - yield pickers; - - // If one of the submodules could not be picked, exit. - - if ("" !== errorMessage) { - throw new UserError(errorMessage); - } - - // After all the submodules are picked, write the index, perform cleanup, - // and make the cherry-pick commit on the meta-repo. - - if (indexChanged) { - yield metaIndex.conflictCleanup(); - yield metaIndex.write(); - } - - metaRepo.stateCleanup(); - const metaCommit = yield metaRepo.createCommitOnHead([], - commit.author(), - commit.committer(), - commit.message()); - return { - newMetaCommit: metaCommit.tostrS(), - submoduleCommits: submoduleCommits, - }; -}); diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 2eefd0205..87ad4e0df 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -877,7 +877,8 @@ exports.getAmendStatus = co.wrap(function *(repo, options) { } const diff = yield NodeGit.Diff.treeToTree(repo, parentTree, headTree, null); - const changes = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff); + const changes = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff, + true); const submodules = baseStatus.submodules; // holds resulting sub statuses const opener = new Open.Opener(repo, null); diff --git a/node/lib/util/conflict_util.js b/node/lib/util/conflict_util.js new file mode 100644 index 000000000..186ca162d --- /dev/null +++ b/node/lib/util/conflict_util.js @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; +const assert = require("chai").assert; +const co = require("co"); +const NodeGit = require("nodegit"); + +/** + * @class ConflictEntry + * This class represents a part of a conflict. + */ +class ConflictEntry { + /** + * @constructor + * Create a new `ConflictEntry` having the specified `mode` and `id`. + * + * @param {Number} mode + * @param {String} id + */ + constructor(mode, id) { + assert.isNumber(mode); + assert.isString(id); + this.d_mode = mode; + this.d_id = id; + Object.freeze(this); + } + + /** + * @property {Number} mode + * the type of entry to create, determining if it is a file, commit, etc. + */ + get mode() { + return this.d_mode; + } + + /** + * @property {String} id + * the id of the entry to create, e.g., commit SHA or blob hash + */ + get id() { + return this.d_id; + } +} + +exports.ConflictEntry = ConflictEntry; + +/** + * @class Conflict + * This class represents a conflict in its three parts. + */ +class Conflict { + constructor(ancestor, our, their) { + if (null !== ancestor) { + assert.instanceOf(ancestor, ConflictEntry); + } + if (null !== our) { + assert.instanceOf(our, ConflictEntry); + } + if (null !== their) { + assert.instanceOf(their, ConflictEntry); + } + this.d_ancestor = ancestor; + this.d_our = our; + this.d_their = their; + Object.freeze(this); + } + + get ancestor() { + return this.d_ancestor; + } + + get our() { + return this.d_our; + } + + get their() { + return this.d_their; + } +} + +exports.Conflict = Conflict; + +/** + * Create the specified `conflict` in the specified `index` at the specified + * `path`. Note that this method does not flush the index to disk. + * + * @param {NodeGit.Index} index + * @param {String} path + * @param {Conflict} conflict + */ +exports.addConflict = co.wrap(function *(index, path, conflict) { + assert.instanceOf(index, NodeGit.Index); + assert.isString(path); + assert.instanceOf(conflict, Conflict); + + function makeEntry(entry) { + if (null === entry) { + return null; // RETURN + } + const result = new NodeGit.IndexEntry(); + result.path = path; + result.mode = entry.mode; + result.id = NodeGit.Oid.fromString(entry.id); + return result; + } + + const ancestorEntry = makeEntry(conflict.ancestor); + const ourEntry = makeEntry(conflict.our); + const theirEntry = makeEntry(conflict.their); + yield index.conflictAdd(ancestorEntry, ourEntry, theirEntry); +}); diff --git a/node/lib/util/diff_util.js b/node/lib/util/diff_util.js index 3c20a993c..e89afa9cd 100644 --- a/node/lib/util/diff_util.js +++ b/node/lib/util/diff_util.js @@ -37,6 +37,8 @@ const NodeGit = require("nodegit"); const RepoStatus = require("./repo_status"); const SubmoduleConfigUtil = require("./submodule_config_util"); +const DELTA = NodeGit.Diff.DELTA; + /** * Return the `RepoStatus.FILESTATUS` value that corresponds to the specified * flag. The behavior is undefined unless `flag` represents one of the types @@ -46,13 +48,11 @@ const SubmoduleConfigUtil = require("./submodule_config_util"); * @return {RepoStatus.FILESTATUS} */ exports.convertDeltaFlag = function (flag) { - const DELTA = NodeGit.Diff.DELTA; const FILESTATUS = RepoStatus.FILESTATUS; switch (flag) { case DELTA.MODIFIED: return FILESTATUS.MODIFIED; case DELTA.ADDED: return FILESTATUS.ADDED; case DELTA.DELETED: return FILESTATUS.REMOVED; - case DELTA.CONFLICTED: return FILESTATUS.CONFLICTED; case DELTA.RENAMED: return FILESTATUS.RENAMED; case DELTA.TYPECHANGE: return FILESTATUS.TYPECHANGED; @@ -62,7 +62,7 @@ exports.convertDeltaFlag = function (flag) { case DELTA.UNTRACKED: return FILESTATUS.ADDED; } - assert(`Unrecognized DELTA type: ${flag}.`); + assert(false, `Unrecognized DELTA type: ${flag}.`); }; function readDiff(diff) { @@ -72,6 +72,9 @@ function readDiff(diff) { for (let i = 0; i < numDeltas; ++i) { const delta = diff.getDelta(i); const diffStatus = delta.status(); + if (DELTA.CONFLICTED === diffStatus) { + continue; // CONTINUE + } const fileStatus = exports.convertDeltaFlag(diffStatus); const file = FILESTATUS.REMOVED === fileStatus ? delta.oldFile() : @@ -100,6 +103,8 @@ function readDiff(diff) { * `workdir` field, the difference between the workir and the index; and in the * `staged` field, the difference between the index and `tree`. Note that when * `ignoreIndex` is true, the returned `staged` field will always be `{}`. + * Note also that conflicts are ignored; we don't have enough information here + * to handle them properly. * * @param {NodeGit.Repository} repo * @param {NodeGit.Tree|null} tree diff --git a/node/lib/util/merge_file_util.js b/node/lib/util/merge_file_util.js index ac2d9509f..78bd10385 100644 --- a/node/lib/util/merge_file_util.js +++ b/node/lib/util/merge_file_util.js @@ -89,10 +89,10 @@ exports.writeMerge = co.wrap(function *(gitDir, merge) { const root = path.join(gitDir, metaMergeDir); yield fs.mkdir(root); - yield fs.writeFile(path.join(root, "MSG"), merge.message); - yield fs.writeFile(path.join(root, "ORIG_HEAD"), + yield fs.writeFile(path.join(root, messageFile), merge.message); + yield fs.writeFile(path.join(root, originalHeadFile), merge.originalHead + "\n"); - yield fs.writeFile(path.join(root, "MERGE_HEAD"), + yield fs.writeFile(path.join(root, mergeHeadFile), merge.mergeHead + "\n"); }); diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 34f441131..2c4b46360 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -199,14 +199,7 @@ exports.merge = co.wrap(function *(repo, showMetaChanges: true, }); - // Cannot merge if there is an existing merge - - if (null !== status.merge) { - throw new UserError(`\ -There is an existing merge in progress. Run 'git meta merge --continue' -to complete it, or 'git meta merge --abort' to abandon it. -`); - } + StatusUtil.ensureReady(status); // Cannot merge if any staged changes. @@ -297,7 +290,7 @@ ${colors.red(commitSha)}.`); const mergeEntry = co.wrap(function *(entry) { const path = entry.path; - const stage = RepoStatus.getStage(entry.flags); + const stage = NodeGit.Index.entryStage(entry); if (path === SubmoduleConfigUtil.modulesFileName) { hasModulesFile = true; @@ -393,7 +386,7 @@ ${colors.green(subSha)}.`); const entries = subIndex.entries(); for (let i = 0; i < entries.length; ++i) { const subEntry = entries[i]; - const subStage = RepoStatus.getStage(subEntry.flags); + const subStage = NodeGit.Index.entryStage(subEntry); if (RepoStatus.STAGE.OURS === subStage) { errorMessage += `\t${colors.red(subEntry.path)}\n`; } @@ -487,7 +480,7 @@ const checkForConflicts = function (index) { const entries = index.entries(); for (let i = 0; i < entries.length; ++i) { const entry = entries[i]; - const stage = RepoStatus.getStage(entry.flags); + const stage = NodeGit.Index.entryStage(entry); if (RepoStatus.STAGE.OURS === stage && NodeGit.TreeEntry.FILEMODE.COMMIT !== entry.mode) { throw new UserError("Meta-repo has conflicts."); diff --git a/node/lib/util/print_status_util.js b/node/lib/util/print_status_util.js index 912ecf490..cba073ba7 100644 --- a/node/lib/util/print_status_util.js +++ b/node/lib/util/print_status_util.js @@ -42,6 +42,7 @@ const path = require("path"); const GitUtil = require("./git_util"); const Merge = require("./merge"); +const CherryPick = require("./cherry_pick"); const Rebase = require("./rebase"); const RepoStatus = require("./repo_status"); @@ -51,9 +52,9 @@ const RepoStatus = require("./repo_status"); */ class StatusDescriptor { /** - * @param {RepoStatus.FILESTATUS} status - * @param {String} path - * @param {String} detail + * @param {RepoStatus.FILESTATUS|RepoStatus.Conflict} status + * @param {String} path + * @param {String} detail */ constructor(status, path, detail) { this.status = status; @@ -72,25 +73,28 @@ class StatusDescriptor { print(color, cwd) { let result = ""; const FILESTATUS = RepoStatus.FILESTATUS; - switch(this.status) { - case FILESTATUS.ADDED: - result += "new file: "; - break; - case FILESTATUS.MODIFIED: - result += "modified: "; - break; - case FILESTATUS.REMOVED: - result += "deleted: "; - break; - case FILESTATUS.CONFLICTED: - result += "conflicted: "; - break; - case FILESTATUS.RENAMED: - result += "renamed: "; - break; - case FILESTATUS.TYPECHANGED: - result += "type changed: "; - break; + if (this.status instanceof RepoStatus.Conflict) { + // TODO: more detail, e.g., "we added" + result += "conflicted: "; + } + else { + switch(this.status) { + case FILESTATUS.ADDED: + result += "new file: "; + break; + case FILESTATUS.MODIFIED: + result += "modified: "; + break; + case FILESTATUS.REMOVED: + result += "deleted: "; + break; + case FILESTATUS.RENAMED: + result += "renamed: "; + break; + case FILESTATUS.TYPECHANGED: + result += "type changed: "; + break; + } } let filename = path.relative(cwd, this.path); if ("" === filename) { @@ -355,7 +359,8 @@ exports.printRebase = function (rebase) { const shortSha = GitUtil.shortSha(rebase.onto); return `${colors.red("rebase in progress; onto ", shortSha)} You are currently rebasing branch '${rebase.headName}' on '${shortSha}'. - (fix conflicts and then run "git meta rebase --continue") + (after resolving conflicts mark the corrected paths + with 'git meta add', then run "git meta rebase --continue") (use "git meta rebase --abort" to check out the original branch) `; }; @@ -370,8 +375,25 @@ function printMerge(merge) { assert.instanceOf(merge, Merge); return `\ A merge is in progress. - (fix conflicts and run "git meta merge --continue") - (use "git meta merge --abort" to abort the merge) + (after resolving conflicts mark the corrected paths + with 'git meta add', then run "git meta merge --continue") + (use "git meta merge --abort" to check out the original branch) +`; +} + +/** + * Return a message describing the specified `cherryPick`. + * + * @param {CherryPick} + * @return {String} + */ +function printCherryPick(cherryPick) { + assert.instanceOf(cherryPick, CherryPick); + return `\ +A cherry-pick is in progress. + (after resolving conflicts mark the corrected paths + with 'git meta add', then run "git meta cherry-pick --continue") + (use "git meta cherry-pick --abort" to check out the original branch) `; } @@ -414,6 +436,10 @@ exports.printRepoStatus = function (status, cwd) { result += printMerge(status.merge); } + if (null !== status.cherryPick) { + result += printCherryPick(status.cherryPick); + } + let changes = ""; const fileStatuses = exports.accumulateStatus(status); const staged = fileStatuses.staged; diff --git a/node/lib/util/read_repo_ast_util.js b/node/lib/util/read_repo_ast_util.js index 23ce300ab..32a452c1f 100644 --- a/node/lib/util/read_repo_ast_util.js +++ b/node/lib/util/read_repo_ast_util.js @@ -42,6 +42,7 @@ const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); +const CherryPickFileUtil = require("./cherry_pick_file_util"); const RepoAST = require("./repo_ast"); const RebaseFileUtil = require("./rebase_file_util"); const MergeFileUtil = require("./merge_file_util"); @@ -82,16 +83,41 @@ const loadIndexAndWorkdir = co.wrap(function *(repo, headCommit) { const workdir = {}; const submodules = - yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, repoIndex); - + yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, repoIndex); const referencedSubmodules = new Set(); // set of changed submodules const STATUS = NodeGit.Status.STATUS; + const readWorkdir = co.wrap(function *(filePath) { + const absPath = path.join(repo.workdir(), filePath); + let data; + try { + data = yield fs.readFile(absPath, { encoding: "utf8" }); + } catch (e) { + // no file + } + if (undefined !== data) { + workdir[filePath] = data; + } + }); + + const readEntryFile = co.wrap(function *(entry) { + if (undefined === entry) { + return null; // RETURN + } + const oid = entry.id; + if (NodeGit.TreeEntry.FILEMODE.COMMIT === entry.mode) { + return new RepoAST.Submodule("", oid.tostrS()); + } + const blob = yield repo.getBlob(oid); + return blob.toString(); + }); + const stats = yield repo.getStatusExt(); for (let i = 0; i < stats.length; ++i) { - const stat = stats[i].statusBit(); - let filePath = stats[i].path(); + const statusFile = stats[i]; + const stat = statusFile.statusBit(); + let filePath = statusFile.path(); // If the path ends with a slash, knock it off so we'll be able to // match it to submodules. @@ -108,6 +134,23 @@ const loadIndexAndWorkdir = co.wrap(function *(repo, headCommit) { // Check index. + if (statusFile.isConflicted()) { + // If the file is conflicted, read the contents for each stage, and + // the contents of the file in the workdir. + + const ancestorEntry = repoIndex.getByPath(filePath, 1); + const ourEntry = repoIndex.getByPath(filePath, 2); + const theirEntry = repoIndex.getByPath(filePath, 3); + const ancestorData = yield readEntryFile(ancestorEntry); + const ourData = yield readEntryFile(ourEntry); + const theirData = yield readEntryFile(theirEntry); + index[filePath] = new RepoAST.Conflict(ancestorData, + ourData, + theirData); + yield readWorkdir(filePath); + continue; // CONTINUE + } + if (stat & STATUS.INDEX_DELETED) { index[filePath] = null; } @@ -126,12 +169,8 @@ const loadIndexAndWorkdir = co.wrap(function *(repo, headCommit) { else { // Otherwise, read the blob for the file from the index. - const entry = repoIndex.getByPath(filePath, 0); - const oid = entry.id; - const blob = yield repo.getBlob(oid); - const data = blob.toString(); - index[filePath] = data; + index[filePath] = yield readEntryFile(entry); } } @@ -142,11 +181,7 @@ const loadIndexAndWorkdir = co.wrap(function *(repo, headCommit) { } else if (stat & STATUS.WT_NEW || stat & STATUS.WT_MODIFIED) { if (!(filePath in submodules)) { - const absPath = path.join(repo.workdir(), filePath); - const data = yield fs.readFile(absPath, { - encoding: "utf8" - }); - workdir[filePath] = data; + yield readWorkdir(filePath); } } } @@ -441,7 +476,10 @@ exports.readRAST = co.wrap(function *(repo) { let openSubmodules = {}; if (!bare) { - const subNames = yield repo.getSubmoduleNames(); + const index = yield repo.index(); + const subs = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, + index); + const subNames = Object.keys(subs); for (let i = 0; i < subNames.length; ++i) { const subName = subNames[i]; const status = yield NodeGit.Submodule.status(repo, subName, 0); @@ -511,6 +549,13 @@ exports.readRAST = co.wrap(function *(repo) { yield loadCommit(NodeGit.Oid.fromString(merge.mergeHead)); } + const cherryPick = yield CherryPickFileUtil.readCherryPick(repo.path()); + + if (null !== cherryPick) { + yield loadCommit(NodeGit.Oid.fromString(cherryPick.originalHead)); + yield loadCommit(NodeGit.Oid.fromString(cherryPick.picked)); + } + return new RepoAST({ commits: commits, branches: branchTargets, @@ -524,6 +569,7 @@ exports.readRAST = co.wrap(function *(repo) { openSubmodules: openSubmodules, rebase: rebase, merge: merge, + cherryPick: cherryPick, bare: bare, }); }); diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index 9567b0368..191a8e90b 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -44,10 +44,21 @@ const GitUtil = require("./git_util"); const Hook = require("../util/hook"); const RepoStatus = require("./repo_status"); const RebaseFileUtil = require("./rebase_file_util"); +const StatusUtil = require("./status_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); const SubmoduleUtil = require("./submodule_util"); const UserError = require("./user_error"); +/** + * Return a conflict description for the submodule having the specified `name`. + * + * @param {String} name + * @return {String} + */ +function subConflictErrorMessage(name) { + return `Conflict in ${colors.red(name)}`; +} + /** * Put the head of the specified `repo` on the specified `commitSha`. */ @@ -60,12 +71,14 @@ const setHead = co.wrap(function *(repo, commitSha) { * Call `next` on the specified `rebase`; return the rebase operation for the * rebase or null if there is no further operation. * + * TODO: independent test + * * @async * @private * @param {NodeGit.Rebase} rebase * @return {RebaseOperation|null} */ -const callNext = co.wrap(function *(rebase) { +exports.callNext = co.wrap(function *(rebase) { try { return yield rebase.next(); } @@ -110,50 +123,83 @@ const callFinish = co.wrap(function *(repo, rebase) { }); /** - * Process the specified `rebase` for the submodule having the specified - * `name`and open `repo`, beginning with the specified `op` and proceeding - * until there are no more commits to rebase. Return an object describing any - * encountered error and commits made. + * Process the specified `rebase` for the specified `repo`, beginning with the + * specified `op`. Return an object describing any encountered error and + * commits made. If successful, clean up and finish the rebase. If + * `null === op`, finish the rebase and return. * - * @param {NodeGit.Repository} rep - * @param {String} name - * @param {NodeGit.Rebase} rebase - * @param {NodeGit.RebaseOperation} op + * @param {NodeGit.Repository} repo + * @param {NodeGit.Rebase} rebase + * @param {NodeGit.RebaseOperation|null} op * @return {Object} * @return {Object} return.commits - * @return {String|null} return.error + * @return {String|null} return.conflictedCommit */ -const processSubmoduleRebase = co.wrap(function *(repo, - name, - rebase, - op) { +exports.processRebase = co.wrap(function *(repo, rebase, op) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(rebase, NodeGit.Rebase); + if (null !== op) { + assert.instanceOf(op, NodeGit.RebaseOperation); + } const result = { commits: {}, - error: null, + conflictedCommit: null, }; const signature = repo.defaultSignature(); while (null !== op) { - console.log(`Submodule ${colors.blue(name)}: applying \ -commit ${colors.green(op.id().tostrS())}.`); const index = yield repo.index(); if (index.hasConflicts()) { - result.error = `\ -Conflict rebasing the submodule ${colors.red(name)}.`; - break; // BREAK + result.conflictedCommit = op.id().tostrS(); + return result; // RETURN } const newCommit = rebase.commit(null, signature, null); const originalCommit = op.id().tostrS(); result.commits[newCommit.tostrS()] = originalCommit; - op = yield callNext(rebase); - } - if (null === result.error) { - console.log(`Submodule ${colors.blue(name)}: finished \ -rebase.`); - yield callFinish(repo, rebase); + op = yield exports.callNext(rebase); } + yield callFinish(repo, rebase); return result; }); +/** + * Rebease the commits from the specified `branch` commit on the HEAD of + * the specified `repo`. If the optionally specified `upstream` is provided, + * rewrite only commits beginning with `upstream`; otherwise, rewrite all + * reachable commits. Return an object containing a map that describes any + * written commits and an error message if some part of the rewrite failed. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + * @param {NodeGit.Commit|null} upstream + * @return {Object} + * @return {Object} return.commits new sha to original sha + * @return {String|null} return.conflictedCommit error message if failed + */ +exports.rewriteCommits = co.wrap(function *(repo, branch, upstream) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(branch, NodeGit.Commit); + if (null !== upstream) { + assert.instanceOf(upstream, NodeGit.Commit); + } + + const head = yield repo.head(); + const ontoAnnotated = yield NodeGit.AnnotatedCommit.fromRef(repo, head); + const branchAnnotated = + yield NodeGit.AnnotatedCommit.lookup(repo, branch.id()); + let upstreamAnnotated = null; + if (null !== upstream) { + upstreamAnnotated = + yield NodeGit.AnnotatedCommit.lookup(repo, upstream.id()); + } + const rebase = yield NodeGit.Rebase.init(repo, + branchAnnotated, + upstreamAnnotated, + ontoAnnotated, + null); + const op = yield exports.callNext(rebase); + return yield exports.processRebase(repo, rebase, op); +}); + /** * Return an object indicating the commits that were created during rebasing * and/or an error message indicating that the rebase was stopped due to a @@ -165,7 +211,7 @@ rebase.`); * @param {String} onto commit rebasing onto * @return {Object} * @return {Object} return.commits map from original to created commit - * @return {Strring|null} return.error failure message if non-null + * @return {Strring|null} return.conflictedCommit sha of conflicted commit */ const rebaseSubmodule = co.wrap(function *(opener, name, from, onto) { const fetcher = yield opener.fetcher(); @@ -186,9 +232,8 @@ const rebaseSubmodule = co.wrap(function *(opener, name, from, onto) { null); console.log(`Submodule ${colors.blue(name)}: starting \ rebase; rewinding to ${colors.green(ontoCommitId.tostrS())}.`); - - let op = yield callNext(rebase); - return yield processSubmoduleRebase(repo, name, rebase, op); + const op = yield exports.callNext(rebase); + return yield exports.processRebase(repo, rebase, op); }); /** @@ -213,7 +258,7 @@ const processMetaRebaseEntry = co.wrap(function *(metaRepo, const id = entry.id; const isSubmodule = entry.mode === NodeGit.TreeEntry.FILEMODE.COMMIT; const fetcher = yield opener.fetcher(); - const stage = RepoStatus.getStage(entry.flags); + const stage = NodeGit.Index.entryStage(entry); const result = { error: null, @@ -337,7 +382,6 @@ const processMetaRebaseOp = co.wrap(function *(metaRepo, fromCommit, ontoCommit); if (null !== ret.error) { - console.log("E:", ret.error); errorMessage += ret.error + "\n"; } else if (null !== ret.subToRebase) { @@ -370,8 +414,8 @@ const processMetaRebaseOp = co.wrap(function *(metaRepo, result.submoduleCommits[name] = ret.commits; } - if (null !== ret.error) { - errorMessage += ret.error + "\n"; + if (null !== ret.conflictedCommit) { + errorMessage += subConflictErrorMessage(name) + "\n"; conflicted.add(name); } } @@ -436,7 +480,7 @@ const driveRebase = co.wrap(function *(metaRepo, let idx = rebase.operationCurrent(); const total = rebase.operationEntrycount(); function makeCallNext() { - return callNext(rebase); + return exports.callNext(rebase); } while (idx < total) { const rebaseOper = rebase.operationByIndex(idx); @@ -548,7 +592,7 @@ up-to-date.`); null, null); console.log(`Rewinding to ${colors.green(commitId.tostrS())}.`); - yield callNext(rebase); + yield exports.callNext(rebase); return { rebase: rebase, submoduleCommits: {}, @@ -560,6 +604,7 @@ up-to-date.`); // Run post-rewrite hook with "rebase" as args, means rebase command // invoked this hook. + console.log("Finished rebase."); yield Hook.execHook("post-rewrite", ["rebase"]); return result; }); @@ -602,6 +647,97 @@ ${colors.green(rebaseInfo.originalHead)}.`); })); }); +/** + * Continue the rebase in the specified `repo` and return an object describing + * any generated commits and the sha of the conflicted commit if there was one. + * The behavior is undefined unless `true === repo.isRebasing()`. + * + * @param {NodeGit.Repository} repo + * @return {Object} return + * @return {Object} return.commits + * @return {String|null} return.conflictedCommit + */ +const continueRebase = co.wrap(function *(repo) { + assert.instanceOf(repo, NodeGit.Repository); + assert(repo.isRebasing()); + + const rebase = yield NodeGit.Rebase.open(repo); + const idx = rebase.operationCurrent(); + const op = rebase.operationByIndex(idx); + return yield exports.processRebase(repo, rebase, op); +}); + +/** + * Continue rebases in the submodules in the specifed `repo` having the + * `index and `status`. If staged changes are found in submodules that don't + * have in-progress rebases, commit them using the specified message and + * signature from the specified original `commit`. Return an object describing + * any commits that were generated along with an error message if any continues + * failed. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Index} index + * @param {RepoStatus} status + * @param {NodeGit.Commit} commit + * @return {Object} + * @return {Object} return.commits map from name to sha map + * @return {Object} return.newCommits from name to newly-created commits + * @return {String|null} return.errorMessage + */ +exports.continueSubmodules = co.wrap(function *(repo, index, status, commit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(index, NodeGit.Index); + assert.instanceOf(status, RepoStatus); + assert.instanceOf(commit, NodeGit.Commit); + + const commits = {}; + const newCommits = {}; + const subs = status.submodules; + let errorMessage = ""; + const continueSub = co.wrap(function *(name) { + const sub = subs[name]; + const workdir = sub.workdir; + if (null === workdir) { + // Return early if the submodule is closed. + return; // RETURN + } + const subStatus = workdir.status; + const rebaseInfo = subStatus.rebase; + const subRepo = yield SubmoduleUtil.getRepo(repo, name); + if (null === rebaseInfo) { + if (0 !== Object.keys(subStatus.staged).length) { + const id = yield subRepo.createCommitOnHead([], + commit.author(), + commit.committer(), + commit.message()); + newCommits[name] = id.tostrS(); + } + yield index.addByPath(name); + + // Return early if no rebase in this submodule. + return; // RETURN + } + console.log(`Submodule ${colors.blue(name)} continuing \ +rewrite from ${colors.green(rebaseInfo.originalHead)} onto \ +${colors.green(rebaseInfo.onto)}.`); + const result = yield continueRebase(subRepo); + commits[name] = result.commits; + if (null !== result.conflictedCommit) { + errorMessage += subConflictErrorMessage(name) + "\n"; + } + else { + yield index.addByPath(name); + yield index.conflictRemove(name); + } + }); + yield DoWorkQueue.doInParallel(Object.keys(subs), continueSub); + return { + errorMessage: "" === errorMessage ? null : errorMessage, + commits: commits, + newCommits: newCommits, + }; +}); + /** * Continue the rebase in progress on the specified `repo`. * @@ -610,16 +746,21 @@ ${colors.green(rebaseInfo.originalHead)}.`); exports.continue = co.wrap(function *(repo) { assert.instanceOf(repo, NodeGit.Repository); - const rebaseInfo = yield RebaseFileUtil.readRebase(repo.path()); + const status = yield StatusUtil.getRepoStatus(repo); + + const rebaseInfo = status.rebase; if (null === rebaseInfo) { throw new UserError("Error: no rebase in progress"); } - const fromCommit = yield repo.getCommit(rebaseInfo.originalHead); - const ontoCommit = yield repo.getCommit(rebaseInfo.onto); - let errorMessage = ""; + if (status.isConflicted()) { + throw new UserError("Cannot continue rebase due to conflicts."); + } + + const fromCommit = yield repo.getCommit(rebaseInfo.originalHead); + const ontoCommit = yield repo.getCommit(rebaseInfo.onto); - const initializer = co.wrap(function *(metaRepo) { + const initializer = co.wrap(function *(repo) { console.log(`Continuing rebase from \ ${colors.green(rebaseInfo.originalHead)} onto \ ${colors.green(rebaseInfo.onto)}.`); @@ -627,47 +768,23 @@ ${colors.green(rebaseInfo.onto)}.`); return NodeGit.Rebase.open(repo); }); const index = yield repo.index(); - const subs = yield SubmoduleUtil.listOpenSubmodules(metaRepo); - const subCommits = {}; - for (let i = 0; i !== subs.length; ++i) { - const name = subs[i]; - const subRepo = yield SubmoduleUtil.getRepo(metaRepo, name); - - // If this submodule isn't rebasing, continue to the next one. - - if (!subRepo.isRebasing()) { - yield index.addByPath(name); - continue; // CONTINUE - } - yield index.addByPath(name); - - const subInfo = yield RebaseFileUtil.readRebase(repo.path()); - console.log(`Submodule ${colors.blue(name)} continuing \ -rebase from ${colors.green(subInfo.originalHead)} onto \ -${colors.green(subInfo.onto)}.`); - const rebase = yield NodeGit.Rebase.open(subRepo); - const idx = rebase.operationCurrent(); - const op = rebase.operationByIndex(idx); - const result = yield processSubmoduleRebase(subRepo, - name, - rebase, - op); - subCommits[name] = result.commits; - if (null !== result.error) { - errorMessage += result.error + "\n"; - } - else { - yield index.addByPath(name); - yield index.conflictRemove(name); - } - } - if ("" !== errorMessage) { - throw new UserError(errorMessage); + const idx = rebase.operationCurrent(); + const op = rebase.operationByIndex(idx); + const baseCommit = yield repo.getCommit(op.id()); + const result = yield exports.continueSubmodules(repo, + index, + status, + baseCommit); + if (null !== result.errorMessage) { + throw new UserError(result.errorMessage); } return { rebase: rebase, - submoduleCommits: subCommits, + submoduleCommits: result.commits, }; }); - return yield driveRebase(repo, initializer, fromCommit, ontoCommit); + const result = + yield driveRebase(repo, initializer, fromCommit, ontoCommit); + console.log("Finished rebase."); + return result; }); diff --git a/node/lib/util/repo_ast.js b/node/lib/util/repo_ast.js index 790dcc05f..553a3ec55 100644 --- a/node/lib/util/repo_ast.js +++ b/node/lib/util/repo_ast.js @@ -39,6 +39,7 @@ const deepCopy = require("deepcopy"); const Rebase = require("./rebase"); const Merge = require("./merge"); +const CherryPick = require("./cherry_pick"); /** * @class {Branch} @@ -115,12 +116,98 @@ class Submodule { get sha() { return this.d_sha; } + + /** + * Return true if the specified `rhs` represents the same value as this + * `Submodule` object and false otherwise. Two `Submodule` objects + * represent the same value if they have the same `url` and `sha`. + * + * @param {Submodule} rhs + * @return {Bool} + */ + equal(rhs) { + assert.instanceOf(rhs, Submodule); + return this.d_url === rhs.d_url && this.d_sha === rhs.d_sha; + } } Submodule.prototype.toString = function () { return `Submodule(url=${this.d_url}, sha=${this.d_sha || ""})`; }; +/** + * @class Conflict + * + * This class is used to represent a file that is conflicted in the index. + */ +class Conflict { + + /** + * Create a new `Conflict` having the specified `ancestor`, `our`, and + * `their` file content. Null content indicates a deletion. + * + * @constructor + * @param {String|Submodule|null} ancestor + * @param {String|Submodule|null} our + * @param {String||Submodule|null} their + */ + constructor(ancestor, our, their) { + assert(null === ancestor || + "string" === typeof ancestor || + ancestor instanceof Submodule, ancestor); + assert(null === our || + "string" === typeof our || + our instanceof Submodule, our); + assert(null === their || + "string" === typeof their || + their instanceof Submodule, their); + this.d_ancestor = ancestor; + this.d_our = our; + this.d_their = their; + } + + /** + * @property {String|Submodule|null} ancestor + * the content of the file in the common ancestor commit + */ + get ancestor() { + return this.d_ancestor; + } + + /** + * @property {String|Submodule|null} our + * the content of the file in the "current" commit being integrated into + */ + get our() { + return this.d_our; + } + + /** + * @property {String|Submodule|null} their + * the content of the file in the commit being integrated + */ + get their() { + return this.d_their; + } + + /** + * Return true if the specified `rhs` represents the same value as this + * `Conflict` and false otherwise. Two `Conflict` objects represent the + * same value if the have the same `ancestor`, `our`, and `their`. + * + * @param {Conflict} rhs + * @return {Bool} + */ + equal(rhs) { + assert.instanceOf(rhs, Conflict); + return deeper(this, rhs); + } +} + +Conflict.prototype.toString = function () { + return `Conflict(ancestor=${this.d_ancestor}, our=${this.d_our} \ +their=${this.d_their})`; +}; /** * This module provides the RepoAST class and associated classes. Supported @@ -291,6 +378,7 @@ class AST { * - If provided, the `onto` and `originalHead` commits in `rebase` exist * in `commits`. * - if 'bare', `index` and `workdir` are empty, and `rebase` is null + * - any conflicted path in the index has a value specified in the workdir * * @param {Object} args * @param {Object} [args.commits] @@ -306,6 +394,7 @@ class AST { * @param {Object} [args.openSubmodules] * @param {Rebase} [args.rebase] * @param {Merge} [args.merge] + * @param {CherryPick} [args.cherryPick] */ constructor(args) { if (undefined === args) { @@ -483,28 +572,25 @@ in commit ${id}.`); } } + this.d_cherryPick = null; + if ("cherryPick" in args) { + const cherryPick = args.cherryPick; + if (null !== cherryPick) { + assert.instanceOf(cherryPick, CherryPick); + assert.isFalse(this.d_bare); + checkAndTraverse(cherryPick.originalHead, + "original head of cherry-pick"); + checkAndTraverse(cherryPick.picked, "picked commit"); + this.d_cherryPick = cherryPick; + } + } + // Validate that all commits have been reached. for (let key in commits) { assert(seen.has(key), `Commit '${key}' is not reachable.`); } - // Copy and validate index changes. - - this.d_index = {}; - if ("index" in args) { - const index = args.index; - assert.isObject(index); - for (let path in index) { - assert.isFalse(this.d_bare); - const change = index[path]; - if (!(change instanceof Submodule) && null !== change) { - assert.isString(change); - } - this.d_index[path] = change; - } - } - // Copy and validate notes changes. this.d_notes = {}; @@ -539,6 +625,25 @@ in commit ${id}.`); } } + // Copy and validate index changes. + + this.d_index = {}; + if ("index" in args) { + const index = args.index; + assert.isObject(index); + for (let path in index) { + assert.isFalse(this.d_bare); + const change = index[path]; + assert(null === change || + "string" === typeof change || + change instanceof Submodule || + change instanceof Conflict, + `Invalid value in index for ${path} -- ${change}`); + this.d_index[path] = change; + } + } + + // Copy and validate open submodules. Each open submodule must be an // instance of `AST` and there must be a `Submodule` defined in the // current index for that path. @@ -659,6 +764,13 @@ in commit ${id}.`); return this.d_merge; } + /** + * @property {CherryPick} null unless a cherry-pick is in progress + */ + get cherryPick() { + return this.d_cherryPick; + } + /** * Accumulate the specified `changes` into the specified `dest` map. A * non-null value in `changes` overrides any existing value in `dest`; a @@ -719,6 +831,8 @@ in commit ${id}.`); args.openSubmodules : this.d_openSubmodules, rebase: ("rebase" in args) ? args.rebase : this.d_rebase, merge: ("merge" in args) ? args.merge : this.d_merge, + cherryPick: ("cherryPick" in args) ? + args.cherryPick : this.cherryPick, bare: ("bare" in args) ? args.bare : this.d_bare, }); } @@ -784,17 +898,26 @@ in commit ${id}.`); assert.isObject(commitMap); assert.isString(commitId); assert.isObject(indexChanges); - let cache = {}; - let fromCommit = AST.renderCommit(cache, commitMap, commitId); - AST.accumulateDirChanges(fromCommit, indexChanges); + const cache = {}; + const fromCommit = AST.renderCommit(cache, commitMap, commitId); + const filteredIndex = {}; + for (let key in indexChanges) { + const value = indexChanges[key]; + if (!(value instanceof Conflict)) { + filteredIndex[key] = value; + } + } + AST.accumulateDirChanges(fromCommit, filteredIndex); return fromCommit; } } AST.Branch = Branch; AST.Commit = Commit; +AST.Conflict = Conflict; AST.Rebase = Rebase; AST.Merge = Merge; +AST.CherryPick = CherryPick; AST.Remote = Remote; AST.Submodule = Submodule; module.exports = AST; diff --git a/node/lib/util/repo_ast_test_util.js b/node/lib/util/repo_ast_test_util.js index 21345e3ef..67ce5c5f6 100644 --- a/node/lib/util/repo_ast_test_util.js +++ b/node/lib/util/repo_ast_test_util.js @@ -351,3 +351,50 @@ exports.testMultiRepoManipulator = RepoASTUtil.assertEqualRepoMaps(actualASTs, expectedASTs); }); + +function addNewCommit(result, commitMap, newCommit, oldCommit, suffix) { + assert.property(commitMap, oldCommit); + const oldLogicalCommit = commitMap[oldCommit]; + result[newCommit] = oldLogicalCommit + suffix; +} + +/** + * Populate the specified `result` with translation of the specified `commits` + * using the specified `commitMap` such that each sha becomes ${logical + * sha}${suffix}. + * + * @param {Object} result output, new physical to new logical sha + * @param {Object} commits from new physical to old physical sha + * @param {Object} commitMap from old physical to old logical sha + * @param {String} suffix + */ +exports.mapCommits = function (result, commits, commitMap, suffix) { + assert.isObject(result); + assert.isObject(commits); + assert.isObject(commitMap); + assert.isString(suffix); + + Object.keys(commits).forEach(newCommit => { + addNewCommit(result, commitMap, newCommit, commits[newCommit], suffix); + }); +}; + +/** + * Populate the specified `result` with translations of the specified + * `subCommits` based on the specified `commitMap` such that each submodule + * commit becomes ${logical id}${sub name}. + * + * @param {Object} result output, new physical to new logical sha + * @param {Object} subCommits from sub name to map from new to old sha + * @param {Object} commitMap from old physical to old logical sha + */ +exports.mapSubCommits = function (result, subCommits, commitMap) { + assert.isObject(result); + assert.isObject(subCommits); + assert.isObject(commitMap); + + Object.keys(subCommits).forEach(subName => { + const commits = subCommits[subName]; + exports.mapCommits(result, commits, commitMap, subName); + }); +}; diff --git a/node/lib/util/repo_ast_util.js b/node/lib/util/repo_ast_util.js index d70d34cb2..ecce55a96 100644 --- a/node/lib/util/repo_ast_util.js +++ b/node/lib/util/repo_ast_util.js @@ -131,12 +131,26 @@ function diffChanges(actual, expected) { function compare(path) { const actualChange = actual[path]; const expectedChange = expected[path]; - let different = actualChange !== expectedChange; + let different; + if ("string" === typeof actualChange && + "string" === typeof expectedChange && + expectedChange.startsWith("^")) { + const exp = expectedChange.substr(1); + const matcher = new RegExp(exp); + const match = matcher.exec(actualChange); + different = null === match; + } else { + different = actualChange !== expectedChange; + } if (different && actualChange instanceof RepoAST.Submodule && expectedChange instanceof RepoAST.Submodule) { - different = actualChange.url !== expectedChange.url || - actualChange.sha !== expectedChange.sha; + different = !actualChange.equal(expectedChange); + } + else if (different && + actualChange instanceof RepoAST.Conflict && + expectedChange instanceof RepoAST.Conflict) { + different = !actualChange.equal(expectedChange); } if (different) { result.push(`\ @@ -498,6 +512,21 @@ ${colorAct(actual.merge.mergeHead)}.`); } } + // Check cherry-pick + + if (null === actual.cherryPick && null !== expected.cherryPick) { + result.push("Missing cherry-pick."); + } + else if (null !== actual.cherryPick && null === expected.cherryPick) { + result.push("Unexpected cherry-pick."); + } + else if (null !== actual.cherryPick && + !actual.cherryPick.equal(expected.cherryPick)) { + result.push(`\ +Expected cherry pick to be ${actual.cherryPick} but got \ +${expected.cherryPick}`); + } + return result; } @@ -632,12 +661,23 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { return new RepoAST.Submodule(url, sha); } + function mapData(data) { + if (data instanceof RepoAST.Submodule) { + return mapSubmodule(data); + } + return data; + } + function mapChanges(input) { let changes = {}; for (let path in input) { let change = input[path]; - if (change instanceof RepoAST.Submodule) { - change = mapSubmodule(change); + if (change instanceof RepoAST.Conflict) { + change = new RepoAST.Conflict(mapData(change.ancestor), + mapData(change.our), + mapData(change.their)); + } else { + change = mapData(change); } changes[path] = change; } @@ -730,6 +770,13 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { mapCommitId(merge.mergeHead)); } + let cherryPick = ast.cherryPick; + if (null !== cherryPick) { + cherryPick = new RepoAST.CherryPick( + mapCommitId(cherryPick.originalHead), + mapCommitId(cherryPick.picked)); + } + return ast.copy({ commits: commits, branches: branches, @@ -743,6 +790,7 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { openSubmodules: openSubmodules, rebase: rebase, merge: merge, + cherryPick: cherryPick, }); }; diff --git a/node/lib/util/repo_status.js b/node/lib/util/repo_status.js index c2f74ca90..2daa42ae5 100644 --- a/node/lib/util/repo_status.js +++ b/node/lib/util/repo_status.js @@ -33,12 +33,62 @@ const assert = require("chai").assert; const Rebase = require("./rebase"); const Merge = require("./merge"); +const CherryPick = require("./cherry_pick"); /** * This modules defines the type `RepoStatus`, used to describe modifications * to a repo. */ +/** + * @class Conflict + * describes a conflict in a file or submodule + */ +class Conflict { + /** + * Create a new `Conflict` object having the specified `ancestorMode`, + * `ourMode`, and `theirMode` modes. + * + * @param {Number|null} ancestorMode + * @param {Number|null} ourMode + * @param {Number|null} theirMode + */ + constructor(ancestorMode, ourMode, theirMode) { + if (null !== ancestorMode) { + assert.isNumber(ancestorMode); + } + if (null !== ourMode) { + assert.isNumber(ourMode); + } + if (null !== theirMode) { + assert.isNumber(theirMode); + } + + this.d_ancestorMode = ancestorMode; + this.d_ourMode = ourMode; + this.d_theirMode = theirMode; + Object.freeze(this); + } + + /** + * @property {Number} ancestorMode file mode of the ancestor part + */ + get ancestorMode() { + return this.d_ancestorMode; + } + + /** + * @property {Number} ourMode file mode of our part of conflict + */ + get ourMode() { + return this.d_ourMode; + } + + get theirMode() { + return this.d_theirMode; + } +} + /** * @enum {RepoStatus.STAGE} * indicates the meaning of an entry in the index @@ -47,6 +97,7 @@ const Merge = require("./merge"); // the nodegit or libgit2 documentation. const STAGE = { NORMAL: 0, // normally staged file + ANCESTOR: 1, OURS : 2, // our side of a stage THEIRS: 3, // their side of a stage }; @@ -60,7 +111,6 @@ const FILESTATUS = { MODIFIED: 0, ADDED: 1, REMOVED: 2, - CONFLICTED: 3, RENAMED: 4, TYPECHANGED: 5 }; @@ -385,30 +435,19 @@ class Submodule { */ class RepoStatus { - /** - * Return the `STAGE` for the specified `flags`. - * - * @static - * @param {Number} flags - * @return {STAGE} - */ - static getStage(flags) { - const GIT_IDXENTRY_STAGESHIFT = 12; - return flags >> GIT_IDXENTRY_STAGESHIFT; - } - /** * Create a new status object having the specified properties. * @constructor * - * @param {Object} [args] - * @param {String} [args.currentBranchName] - * @param {String} [args.headCommit] - * @param {Object} [args.staged] map from name to `FILESTATUS` - * @param {Object} [args.submodules] map from name to `Submodule` - * @param {Object} [args.workdir] map from name to `FILESTATUS` - * @param {Rebase} [args.rebase] rebase, if one is in progress - * @param {Merge} [args.merge] merge, if one is in progress + * @param {Object} [args] + * @param {String} [args.currentBranchName] + * @param {String} [args.headCommit] + * @param {Object} [args.staged] map from name to `FILESTATUS` + * @param {Object} [args.submodules] map from name to `Submodule` + * @param {Object} [args.workdir] map from name to `FILESTATUS` + * @param {Rebase} [args.rebase] rebase, if one is in progress + * @param {Merge} [args.merge] merge, if one is in progress + * @param {CherryPick} [args.cherryPick] cherry-pick, if one is in progress */ constructor(args) { if (undefined === args) { @@ -424,6 +463,7 @@ class RepoStatus { this.d_submodules = {}; this.d_rebase = null; this.d_merge = null; + this.d_cherryPick = null; if ("currentBranchName" in args) { if (null !== args.currentBranchName) { @@ -442,7 +482,9 @@ class RepoStatus { assert.isObject(staged); for (let name in staged) { const value = staged[name]; - assert.isNumber(value); + if ("number" !== typeof(value)) { + assert.instanceOf(value, Conflict); + } this.d_staged[name] = value; } } @@ -474,6 +516,14 @@ class RepoStatus { } this.d_merge = merge; } + + if ("cherryPick" in args) { + const cherryPick = args.cherryPick; + if (null !== cherryPick) { + assert.instanceOf(cherryPick, CherryPick); + } + this.d_cherryPick = cherryPick; + } Object.freeze(this); } @@ -577,6 +627,27 @@ class RepoStatus { }); } + /** + * Return true if there are any conflicts in the repository represented by + * this object and false otherwise. + * + * @return {Bool} + */ + isConflicted() { + for (let name in this.d_staged) { + if (this.staged[name] instanceof Conflict) { + return true; // RETURN + } + } + for (let name in this.d_submodules) { + const sub = this.d_submodules[name]; + if (null !== sub.workdir && sub.workdir.status.isConflicted()) { + return true; // RETURN + } + } + return false; + } + // PROPERTIES /** @@ -631,6 +702,13 @@ class RepoStatus { return this.d_merge; } + /** + * @property {CherryPick} cherryPick if~null, state of cherryPick + */ + get cherryPick() { + return this.d_cherryPick; + } + /** * Return a new `RepoStatus` object having the same value as this one, but * with replacing properties defined in the specified `args`. @@ -656,11 +734,14 @@ class RepoStatus { workdir: ("workdir" in args) ? args.workdir : this.d_workdir, rebase: ("rebase" in args) ? args.rebase : this.d_rebase, merge: ("merge" in args) ? args.merge : this.d_merge, + cherryPick: ("cherryPick" in args) ? + args.cherryPick : this.d_cherryPick, }); } } module.exports = RepoStatus; +RepoStatus.Conflict = Conflict; RepoStatus.STAGE = STAGE; RepoStatus.FILESTATUS = FILESTATUS; RepoStatus.Submodule = Submodule; diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index a6527e8a2..58be2f453 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -88,7 +88,8 @@ exports.reset = co.wrap(function *(repo, commit, type) { commitTree, headTree, null); - const changedSubs = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff); + const changedSubs = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff, + true); // Prep the opener to open submodules on HEAD; otherwise, our resets will // be noops. diff --git a/node/lib/util/shorthand_parser_util.js b/node/lib/util/shorthand_parser_util.js index 594d91ca6..35ad7b2f7 100644 --- a/node/lib/util/shorthand_parser_util.js +++ b/node/lib/util/shorthand_parser_util.js @@ -57,7 +57,7 @@ const RepoASTUtil = require("../util/repo_ast_util"); * base repo type = 'S' | 'B' | ('C') | 'A' | 'N' * override = | | | | * | | | | - * | | + * | | | * head = 'H='| nothing means detached * nothing = * commit = + @@ -67,7 +67,9 @@ const RepoASTUtil = require("../util/repo_ast_util"); * current branch = '*=' * new commit = 'C'[message#]['-'(,)*] * [' '(',\s*'*)] - * change = path ['=' | "~" | ] + * change = path ['=' | "~" | ] | + * *path='*''*' + * conflict item = | * path = (|'/')+ * submodule = Surl:[] * data = ('0-9'|'a-z'|'A-Z'|' ')* basically non-delimiter ascii @@ -76,6 +78,7 @@ const RepoASTUtil = require("../util/repo_ast_util"); * note = N =message * rebase = E,, * merge = M,, + * cherry-pick = P, * index = I [,]* * workdir = W [,]* * open submodule = 'O'[' '('!')*] @@ -141,6 +144,12 @@ const RepoASTUtil = require("../util/repo_ast_util"); * testing of relative URLs, a relative URL in the form of "../foo" will be * translated into "foo" in a open submodule. * + * In some cases, it's difficult to specify the exact contents of a file. For + * example, a file with a conflict that contains the SHA of a commit. In such + * cases, you can specify a regex for the contents of a file by using "^" as + * the first character in the data, e.g. `^ab$` would match a file with a line + * ending in `ab`. + * * Examples: * * S -- same as RepoType.S @@ -175,6 +184,12 @@ const RepoASTUtil = require("../util/repo_ast_util"); * S:Mmerging commit,1,2 -- merge in progress started with commit * message "merging commit" * original head "1", merge head "2" + * S:CP1,2 -- cherry-pick in progress started with + * original head "1", picking commit "2" + * S:I *foo=a*b*c -- The file 'foo' is flagged in the index + * as conflicted, having a base content of 'a', + * "our" content as 'b', and "their" content as + * 'c'. * * Note that the "clone' type may not be used with single-repo ASTs, and the * url must map to the name of another repo. A cloned repository has the @@ -246,6 +261,22 @@ function findChar(str, c, begin, end) { return (-1 === index || end <= index) ? null : index; } +/** + * Return the submodule described by the specified `data`. + * @param {String} data + * @return {Submodule} + */ +function parseSubmodule(data) { + const end = data.length; + const urlEnd = findChar(data, ":", 0, end); + assert.isNotNull(urlEnd); + const commitIdBegin = urlEnd + 1; + const sha = (commitIdBegin === end) ? + null : + data.substr(commitIdBegin, end - commitIdBegin); + return new RepoAST.Submodule(data.substr(0, urlEnd), sha); +} + /** * Return the path change data described by the specified `commitData`. If * `commitData` begins with an `S` it is a submodule description; otherwise, it @@ -263,20 +294,20 @@ function parseChangeData(commitData) { if (0 === end || "S" !== commitData[0]) { return commitData; // RETURN } - - // Must have room for 'S', ':', and at least one char for the url and - // commit id - - const urlBegin = 1; - const urlEnd = findChar(commitData, ":", urlBegin, end); - assert.isNotNull(urlEnd); - const commitIdBegin = urlEnd + 1; - const sha = (commitIdBegin === end) ? - null : - commitData.substr(commitIdBegin, end - commitIdBegin); - return new RepoAST.Submodule(commitData.substr(urlBegin, - urlEnd - urlBegin), - sha); + assert(1 !== commitData.length); + return parseSubmodule(commitData.substr(1)); +} +/** + * Return the path change data described by the specified `data`. If + * `data` begins with an `S` it is a submodule description; if it is empty, it + * is null; otherwise, it is just 'data'. + * @private + * @param {String} data + * @return {String|RepoAST.Submodule|null} + */ +function parseConflictData(data) { + const result = parseChangeData(data); + return result === undefined ? null : result; } /** @@ -325,6 +356,7 @@ function prepareASTArguments(baseAST, rawRepo) { openSubmodules: baseAST.openSubmodules, rebase: baseAST.rebase, merge: baseAST.merge, + cherryPick: baseAST.cherryPick, bare: baseAST.bare, }; @@ -443,6 +475,12 @@ function prepareASTArguments(baseAST, rawRepo) { resultArgs.merge = rawRepo.merge; } + // Override cherry-pick if provided + + if ("cherryPick" in rawRepo) { + resultArgs.cherryPick = rawRepo.cherryPick; + } + return resultArgs; } @@ -521,6 +559,7 @@ function parseOverrides(shorthand, begin, end, delimiter) { let openSubmodules = {}; let rebase; let merge; + let cherryPick; /** * Parse a set of changes from the specified `begin` character to the @@ -528,6 +567,7 @@ function parseOverrides(shorthand, begin, end, delimiter) { * * @param {Number} begin * @param {Number} end + * @param {Bool} allowConflicts */ function parseChanges(begin, end) { let changes = {}; @@ -537,16 +577,29 @@ function parseOverrides(shorthand, begin, end, delimiter) { const assign = findChar(shorthand, "=", begin, currentEnd.begin); assert.notEqual(begin, assign, "no path"); let change = null; - let pathEnd = currentEnd.begin; + const pathEnd = assign || currentEnd.begin; + let path = shorthand.substr(begin, pathEnd - begin); if (null !== assign) { - pathEnd = assign; const dataBegin = assign + 1; - const rawChange = - shorthand.substr(dataBegin, - currentEnd.begin - dataBegin); - change = parseChangeData(rawChange); + const rawChange = shorthand.substr( + dataBegin, + currentEnd.begin - dataBegin); + + if (path.startsWith("*")) { + // Handle 'Conflict'. + + assert.notEqual(path.length, 1, "empty conflict path"); + path = path.substr(1); + const changes = rawChange.split("*"); + assert.equal(changes.length, 3, "missing conflict parts"); + const parts = changes.map(parseConflictData); + change = new RepoAST.Conflict(parts[0], + parts[1], + parts[2]); + } else { + change = parseChangeData(rawChange); + } } - const path = shorthand.substr(begin, pathEnd - begin); changes[path] = change; begin = currentEnd.end; } @@ -893,6 +946,26 @@ function parseOverrides(shorthand, begin, end, delimiter) { merge = new RepoAST.Merge(parts[0], parts[1], parts[2]); } + /** + * Parse the cherry-pick definition beginning at the specified `begin` and + * terminating at the specified `end`. + * + * @param {Number} begin + * @param {Number} end + */ + function parseCherryPick(begin, end) { + if (begin === end) { + cherryPick = null; + return; // RETURN + } + const cherryPickDef = shorthand.substr(begin, end - begin); + const parts = cherryPickDef.split(","); + assert.equal(parts.length, + 2, + `Wrong number of cherry-pick parts in ${cherryPickDef}`); + cherryPick = new RepoAST.CherryPick(parts[0], parts[1]); + } + /** * Parse the override beginning at the specified `begin` and finishing at * the specified `end`. @@ -914,6 +987,7 @@ function parseOverrides(shorthand, begin, end, delimiter) { case "R": return parseRemote; case "I": return parseIndex; case "M": return parseMerge; + case "P": return parseCherryPick; case "N": return parseNote; case "W": return parseWorkdir; case "O": return parseOpenSubmodule; @@ -962,6 +1036,9 @@ function parseOverrides(shorthand, begin, end, delimiter) { if (undefined !== merge) { result.merge = merge; } + if (undefined !== cherryPick) { + result.cherryPick = cherryPick; + } return result; } @@ -1222,6 +1299,10 @@ exports.parseMultiRepoShorthand = function (shorthand, existingRepos) { includeCommit(resultArgs.merge.originalHead); includeCommit(resultArgs.merge.mergeHead); } + if (resultArgs.cherryPick) { + includeCommit(resultArgs.cherryPick.originalHead); + includeCommit(resultArgs.cherryPick.picked); + } for (let remoteName in resultArgs.remotes) { const remote = resultArgs.remotes[remoteName]; for (let branch in remote.branches) { diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 272971018..fb8fae72d 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -290,8 +290,12 @@ exports.apply = co.wrap(function *(repo, id) { const parentId = (yield commit.parent(0)).id(); const parent = yield repo.getCommit(parentId); - const baseSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, parent); - const newSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, commit); + const baseSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, + parent, + null); + const newSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, + commit, + null); const opener = new Open.Opener(repo, null); let result = {}; yield Object.keys(newSubs).map(co.wrap(function *(name) { diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 9b3edf11f..2547f9464 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -40,15 +40,19 @@ const assert = require("chai").assert; const co = require("co"); const NodeGit = require("nodegit"); +const CherryPick = require("./cherry_pick"); +const CherryPickFileUtil = require("./cherry_pick_file_util"); const DiffUtil = require("./diff_util"); const GitUtil = require("./git_util"); const Merge = require("./merge"); const MergeFileUtil = require("./merge_file_util"); +const PrintStatusUtil = require("./print_status_util"); const Rebase = require("./rebase"); const RebaseFileUtil = require("./rebase_file_util"); const RepoStatus = require("./repo_status"); const SubmoduleUtil = require("./submodule_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); +const UserError = require("./user_error"); /** * Return a new `RepoStatus.Submodule` object having the same value as the @@ -99,6 +103,7 @@ exports.remapSubmodule = function (sub, commitMap, urlMap) { * * @param {Rebase} rebase * @param {Object} commitMap from sha to sha + * @return {Rebase} */ function remapRebase(rebase, commitMap) { assert.instanceOf(rebase, Rebase); @@ -119,8 +124,9 @@ function remapRebase(rebase, commitMap) { * Return a new `Merge` object having the same value as the specified `merge` * but with commit shas being replaced by commits in the specified `commitMap`. * - * @param {Merge} merg + * @param {Merge} merge * @param {Object} commitMap from sha to sha + * @return {Merge} */ function remapMerge(merge, commitMap) { assert.instanceOf(merge, Merge); @@ -137,6 +143,29 @@ function remapMerge(merge, commitMap) { return new Merge(merge.message, originalHead, mergeHead); } +/** + * Return a new `CherryPick` object having the same value as the specified + * `cherryPick` but with commit shas being replaced by commits in the specified + * `commitMap`. + * + * @param {CherryPick} cherryPick + * @param {Object} commitMap from sha to sha + * @return {CherryPick} + */ +function remapCherryPick(cherryPick, commitMap) { + assert.instanceOf(cherryPick, CherryPick); + assert.isObject(commitMap); + + let originalHead = cherryPick.originalHead; + let picked = cherryPick.picked; + if (originalHead in commitMap) { + originalHead = commitMap[originalHead]; + } + if (picked in commitMap) { + picked = commitMap[picked]; + } + return new CherryPick(originalHead, picked); +} /** * Return a new `RepoStatus` object having the same value as the specified @@ -175,6 +204,9 @@ exports.remapRepoStatus = function (status, commitMap, urlMap) { commitMap), merge: status.merge === null ? null : remapMerge(status.merge, commitMap), + cherryPick: status.cherryPick === null ? + null : + remapCherryPick(status.cherryPick, commitMap), }); }; @@ -314,6 +346,59 @@ exports.getSubmoduleStatus = co.wrap(function *(repo, }); }); +/** + * Return the conflicts listed in the specified `index`. + * + * @param {NodeGit.Index} index + * @return {Object} map from entry name to `RepoStatus.Conflict` + */ +exports.readConflicts = function (index) { + assert.instanceOf(index, NodeGit.Index); + const conflicted = {}; + function getConflict(path) { + let obj = conflicted[path]; + if (undefined === obj) { + obj = { + ancestor: null, + our: null, + their: null, + }; + conflicted[path] = obj; + } + return obj; + } + const entries = index.entries(); + const STAGE = RepoStatus.STAGE; + for (let entry of entries) { + const stage = NodeGit.Index.entryStage(entry); + switch (stage) { + case STAGE.NORMAL: + break; + case STAGE.ANCESTOR: + getConflict(entry.path).ancestor = entry.mode; + break; + case STAGE.OURS: + getConflict(entry.path).our = entry.mode; + break; + case STAGE.THEIRS: + getConflict(entry.path).their = entry.mode; + break; + } + } + const result = {}; + const COMMIT = NodeGit.TreeEntry.FILEMODE.COMMIT; + for (let name in conflicted) { + const c = conflicted[name]; + + // Ignore the conflict if it's just between submodule SHAs + + if (COMMIT !== c.our || COMMIT !== c.their) { + result[name] = new RepoStatus.Conflict(c.ancestor, c.our, c.their); + } + } + return result; +}; + /** * Return a description of the status of changes to the specified `repo`. If * the optionally specified `options.showAllUntracked` is true (default false), @@ -410,6 +495,7 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { } args.merge = yield MergeFileUtil.readMerge(repo.path()); + args.cherryPick = yield CherryPickFileUtil.readCherryPick(repo.path()); if (options.showMetaChanges && !repo.isBare()) { const head = yield repo.getHeadCommit(); @@ -444,9 +530,13 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { const openArray = yield SubmoduleUtil.listOpenSubmodules(repo); const openSet = new Set(openArray); const index = yield repo.index(); + + Object.assign(args.staged, exports.readConflicts(index)); + const headTree = yield headCommit.getTree(); const diff = yield NodeGit.Diff.treeToIndex(repo, headTree, index); - const changes = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff); + const changes = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff, + true); const indexUrls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); const headUrls = @@ -538,3 +628,39 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { return new RepoStatus(args); }); + +/** + * Throw a `UserError` unless the specified `status` reflects a repository in a + * normal, ready state, that is, it does not have any conflicts or in-progress + * operations such as a rebase, merge, or cherry-pick. Adjust output paths to + * be relative to the specified `cwd`. + * + * @param {RepoStatus} status + */ +exports.ensureReady = function (status) { + assert.instanceOf(status, RepoStatus); + + if (null !== status.rebase) { + throw new UserError(`\ +Before proceeding, you must complete the rebase in progress (by running +'git meta rebase --continue') or abort it (by running +'git meta rebase --abort').`); + } + if (null !== status.merge) { + throw new UserError(`\ +Before proceeding, you must complete the merge in progress (by running +'git meta merge --continue') or abort it (by running +'git meta merge --abort').`); + } + if (null !== status.cherryPick) { + throw new UserError(`\ +Before proceeding, you must complete the cherry-pick in progress (by running +'git meta cherry-pick --continue') or abort it (by running +'git meta cherry-pick --abort').`); + } + if (status.isConflicted()) { + throw new UserError(`\ +Please resolve outstanding conflicts before proceeding: +${PrintStatusUtil.printRepoStatus(status, "")}`); + } +}; diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index defab64aa..d8836f600 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -558,3 +558,19 @@ exports.writeConfigText = function (urls) { } return result; }; + +/** + * Write, to the `.gitmodules` file, the specified `urls` in the specified + * `index`, in the specified `repo` and stage the change to the index. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Index} index + * @param {Object} urls submodule name to url + */ +exports.writeUrls = co.wrap(function *(repo, index, urls) { + const modulesPath = path.join(repo.workdir(), + exports.modulesFileName); + const newConf = exports.writeConfigText(urls); + yield fs.writeFile(modulesPath, newConf); + yield index.addByPath(exports.modulesFileName); +}); diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 8937e1cef..f6e88f053 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -46,6 +46,7 @@ const Submodule = require("./submodule"); const SubmoduleChange = require("./submodule_change"); const SubmoduleFetcher = require("./submodule_fetcher"); const SubmoduleConfigUtil = require("./submodule_config_util"); +const UserError = require("./user_error"); /** * Return the names of the submodules (visible or otherwise) for the index @@ -313,20 +314,24 @@ exports.getSubmoduleRepos = co.wrap(function *(repo) { }); /** - * Return a summary of the submodule SHA changes in the specified `diff`. - * TODO: Test this separately from `getSubmoduleChanges`. + * Return a summary of the submodule SHA changes in the specified `diff`. Fail + * if the specified 'allowMetaChanges' is not true and `diff` contains + * non-submodule changes to the meta-repo. * * @asycn * @param {NodeGit.Diff} diff + * @param {Bool} allowMetaChanges * @return {Object} map from name to `SubmoduleChange` */ -exports.getSubmoduleChangesFromDiff = function (diff) { +exports.getSubmoduleChangesFromDiff = function (diff, allowMetaChanges) { assert.instanceOf(diff, NodeGit.Diff); + assert.isBoolean(allowMetaChanges); const num = diff.numDeltas(); const result = {}; const DELTA = NodeGit.Diff.DELTA; const COMMIT = NodeGit.TreeEntry.FILEMODE.COMMIT; + const modulesFileName = SubmoduleConfigUtil.modulesFileName; for (let i = 0; i < num; ++i) { const delta = diff.getDelta(i); switch (delta.status()) { @@ -346,6 +351,9 @@ exports.getSubmoduleChangesFromDiff = function (diff) { result[path] = new SubmoduleChange( delta.oldFile().id().tostrS(), newFile.id().tostrS()); + } else if (!allowMetaChanges && path !== modulesFileName) { + throw new UserError(`\ +Modification to meta-repo file ${colors.red(path)} is not supported.`); } } break; case DELTA.ADDED: { @@ -354,6 +362,9 @@ exports.getSubmoduleChangesFromDiff = function (diff) { if (COMMIT === newFile.mode()) { result[path] = new SubmoduleChange(null, newFile.id().tostrS()); + } else if (!allowMetaChanges && path !== modulesFileName) { + throw new UserError(`\ +Addtion to meta-repo of file ${colors.red(path)} is not supported.`); } } break; case DELTA.DELETED: { @@ -362,6 +373,9 @@ exports.getSubmoduleChangesFromDiff = function (diff) { if (COMMIT === oldFile.mode()) { result[path] = new SubmoduleChange(oldFile.id().tostrS(), null); + } else if (!allowMetaChanges && path !== modulesFileName) { + throw new UserError(`\ +Deletion of meta-repo file ${colors.red(path)} is not supported.`); } } break; } @@ -372,16 +386,21 @@ exports.getSubmoduleChangesFromDiff = function (diff) { /** * Return a summary of the submodule SHAs changed by the specified `commitId` * in the specified `repo`, and flag denoting whether or not the `.gitmodules` - * file was changed. + * file was changed. If 'commit' contains changes to the meta-repo and the + * specified 'allowMetaChanges' is not true, throw a 'UserError'. * * @asycn * @param {NodeGit.Repository} repo * @param {NodeGit.Commit} commit + * @param {Bool} allowMetaChanges * @return {Object} map from name to `SubmoduleChange` */ -exports.getSubmoduleChanges = co.wrap(function *(repo, commit) { +exports.getSubmoduleChanges = co.wrap(function *(repo, + commit, + allowMetaChanges) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); + assert.isBoolean(allowMetaChanges); // We calculate the changes of a commit against its first parent. If it // has no parents, then the calculation is against an empty tree. @@ -394,24 +413,34 @@ exports.getSubmoduleChanges = co.wrap(function *(repo, commit) { const tree = yield commit.getTree(); const diff = yield NodeGit.Diff.treeToTree(repo, parentTree, tree, null); - return yield exports.getSubmoduleChangesFromDiff(diff); + return yield exports.getSubmoduleChangesFromDiff(diff, allowMetaChanges); }); /** * Return the states of the submodules in the specified `commit` in the - * specified `repo`. + * specified `repo`. If the specified 'names' is not null, return only + * submodules in 'names'; otherwise, return all submodules. * * @async * @param {NodeGit.Repository} repo * @param {NodeGit.Commit} commit + * @param {String[]|null} names * @return {Object} map from submodule name to `Submodule` object */ -exports.getSubmodulesForCommit = co.wrap(function *(repo, commit) { +exports.getSubmodulesForCommit = co.wrap(function *(repo, commit, names) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); + if (null !== names) { + assert.isArray(names); + } const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, commit); - const names = Object.keys(urls); + if (null === names) { + names = Object.keys(urls); + } + else { + names = names.filter(n => n in urls); + } const shas = yield exports.getSubmoduleShasForCommit(repo, names, commit); let result = {}; names.forEach(name => { @@ -713,4 +742,3 @@ exports.mergeModulesFile = co.wrap(function *(repo, } return false; }); - diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 58591ef62..4d5f06637 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -128,9 +128,10 @@ function* checkSubmodules(repo, commit) { assert.instanceOf(commit, NodeGit.Commit); const submodules = yield SubmoduleUtil.getSubmodulesForCommit(repo, - commit); + commit, + null); const getChanges = SubmoduleUtil.getSubmoduleChanges; - const changes = yield getChanges(repo, commit); + const changes = yield getChanges(repo, commit, true); const allChanges = [ Object.keys(changes).filter(changeName => { const change = changes[changeName]; diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index 1fa924d40..f69910584 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -45,6 +45,8 @@ const mkdirp = require("mkdirp"); const NodeGit = require("nodegit"); const path = require("path"); +const CherryPickFileUtil = require("./cherry_pick_file_util"); +const ConflictUtil = require("./conflict_util"); const DoWorkQueue = require("./do_work_queue"); const MergeFileUtil = require("./merge_file_util"); const RebaseFileUtil = require("./rebase_file_util"); @@ -54,6 +56,8 @@ const SubmoduleConfigUtil = require("./submodule_config_util"); const TestUtil = require("./test_util"); const TreeUtil = require("./tree_util"); +const FILEMODE = NodeGit.TreeEntry.FILEMODE; + // Begin module-local methods /** @@ -69,7 +73,7 @@ const TreeUtil = require("./tree_util"); const hashObject = co.wrap(function *(db, data) { const BLOB = 3; const res = yield db.write(data, data.length, BLOB); - return res.tostrS(); + return res; }); /** @@ -117,7 +121,6 @@ const writeTree = co.wrap(function *(repo, Object.assign(submodules, parent.submodules); } - const FILEMODE = NodeGit.TreeEntry.FILEMODE; const wereSubs = 0 !== Object.keys(submodules).length; const pathToChange = {}; // name to `TreeUtil.Change` @@ -125,6 +128,11 @@ const writeTree = co.wrap(function *(repo, for (let filename in changes) { const entry = changes[filename]; + if (entry instanceof RepoAST.Conflict) { + // Skip conflicts + continue; // CONTINUE + } + let isSubmodule = false; if (null === entry) { @@ -135,7 +143,7 @@ const writeTree = co.wrap(function *(repo, else if ("string" === typeof entry) { // A string is just plain data. - const id = yield hashObject(db, entry); + const id = (yield hashObject(db, entry)).tostrS(); pathToChange[filename] = new TreeUtil.Change(id, FILEMODE.BLOB); } else if (entry instanceof RepoAST.Submodule) { @@ -173,7 +181,7 @@ const writeTree = co.wrap(function *(repo, \turl = ${url} `; } - const dataId = yield hashObject(db, data); + const dataId = (yield hashObject(db, data)).tostrS(); pathToChange[SubmoduleConfigUtil.modulesFileName] = new TreeUtil.Change(dataId, FILEMODE.BLOB); } @@ -325,6 +333,26 @@ const writeAllCommits = co.wrap(function *(repo, commits, treeCache) { * @param {Object} treeCache map of tree entries */ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { + + const makeConflictEntry = co.wrap(function *(data) { + assert.instanceOf(repo, NodeGit.Repository); + if (null === data) { + return null; + } + if (data instanceof RepoAST.Submodule) { + //TODO: some day support putting conflicts in the .gitmodules file. + assert.equal("", + data.url, + "submodule conflicts must have empty URL"); + const sha = commitMap[data.sha]; + return new ConflictUtil.ConflictEntry(FILEMODE.COMMIT, sha); + } + const db = yield repo.odb(); + const id = yield hashObject(db, data); + const BLOB = FILEMODE.BLOB; + return new ConflictUtil.ConflictEntry(BLOB, id.tostrS()); + }); + const makeRef = co.wrap(function *(name, commit) { const newSha = commitMap[commit]; const newId = NodeGit.Oid.fromString(newSha); @@ -451,6 +479,18 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { yield MergeFileUtil.writeMerge(repo.path(), merge); } + // Write out the cherry-pick info, if it exists. + + if (null !== ast.cherryPick) { + // Map commits + + const originalSha = commitMap[ast.cherryPick.originalHead]; + const pickSha = commitMap[ast.cherryPick.picked]; + const cherryPick = new RepoAST.CherryPick(originalSha, + pickSha); + yield CherryPickFileUtil.writeCherryPick(repo.path(), cherryPick); + } + // Set up the index. We render the current commit and apply the index // on top of it. @@ -467,6 +507,18 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { const index = yield repo.index(); const treeObj = trees.tree; yield index.readTree(treeObj); + for (let filename in ast.index) { + const data = ast.index[filename]; + if (data instanceof RepoAST.Conflict) { + const ancestor = yield makeConflictEntry(data.ancestor); + const our = yield makeConflictEntry(data.our); + const their = yield makeConflictEntry(data.their); + const conflict = new ConflictUtil.Conflict(ancestor, + our, + their); + yield ConflictUtil.addConflict(index, filename, conflict); + } + } yield index.write(); // TODO: Firgure out if this can be done with NodeGit; extend if diff --git a/node/test/util/add_submodule.js b/node/test/util/add_submodule.js index c973b2b6a..3f528353c 100644 --- a/node/test/util/add_submodule.js +++ b/node/test/util/add_submodule.js @@ -37,42 +37,47 @@ const AddSubmodule = require("../../lib/util/add_submodule"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); describe("AddSubmodule", function () { - const cases = { - "simple": { - input: "a=B|x=Ca", - name: "s", - url: "/foo", - expected: "x=E:I s=S/foo:;Os", - }, - "nested": { - input: "a=B|x=Ca", - name: "s/t/u", - url: "/foo/bar", - expected: "x=E:I s/t/u=S/foo/bar:;Os/t/u", - }, - "import": { - input: "a=B|h=B:Cy-1;Bmaster=y|x=Ca", - name: "s", - url: "/foo/bar", - import: { url: "h", branch: "master" }, - expected: "x=E:I s=S/foo/bar:;Os Rupstream=h master=y!H=y", - }, - }; - Object.keys(cases).forEach(caseName => { - const c = cases[caseName]; - it(caseName, co.wrap(function *() { - const doNew = co.wrap(function *(repos) { - let imp = c.import || null; - if (null !== imp) { - const url = yield fs.realpath(repos[imp.url].path()); - imp = { url: url, branch: imp.branch}; - } - yield AddSubmodule.addSubmodule(repos.x, c.url, c.name, imp); - }); - yield RepoASTTestUtil.testMultiRepoManipulator(c.input, - c.expected, - doNew, - c.fails); - })); + describe("addSubmodule", function () { + const cases = { + "simple": { + input: "a=B|x=Ca", + name: "s", + url: "/foo", + expected: "x=E:I s=S/foo:;Os", + }, + "nested": { + input: "a=B|x=Ca", + name: "s/t/u", + url: "/foo/bar", + expected: "x=E:I s/t/u=S/foo/bar:;Os/t/u", + }, + "import": { + input: "a=B|h=B:Cy-1;Bmaster=y|x=Ca", + name: "s", + url: "/foo/bar", + import: { url: "h", branch: "master" }, + expected: "x=E:I s=S/foo/bar:;Os Rupstream=h master=y!H=y", + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const doNew = co.wrap(function *(repos) { + let imp = c.import || null; + if (null !== imp) { + const url = yield fs.realpath(repos[imp.url].path()); + imp = { url: url, branch: imp.branch}; + } + yield AddSubmodule.addSubmodule(repos.x, + c.url, + c.name, + imp); + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.input, + c.expected, + doNew, + c.fails); + })); + }); }); }); diff --git a/node/test/util/cherry_pick.js b/node/test/util/cherry_pick.js new file mode 100644 index 000000000..dd16c030e --- /dev/null +++ b/node/test/util/cherry_pick.js @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; + +const CherryPick = require("../../lib/util/cherry_pick"); + +describe("CherryPick", function () { + it("constructor", function () { + const c = new CherryPick("12", "32"); + assert.instanceOf(c, CherryPick); + assert.isFrozen(c); + assert.equal(c.originalHead, "12"); + assert.equal(c.picked, "32"); + }); + describe("equal", function () { + const cases = { + "same": { + lhs: new CherryPick("1", "2"), + rhs: new CherryPick("1", "2"), + expected: true, + }, + "diff original head": { + lhs: new CherryPick("1", "2"), + rhs: new CherryPick("2", "2"), + expected: false, + }, + "diff pick": { + lhs: new CherryPick("1", "2"), + rhs: new CherryPick("1", "1"), + expected: false, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, function () { + const result = c.lhs.equal(c.rhs); + assert.equal(result, c.expected); + }); + }); + }); +}); diff --git a/node/test/util/cherry_pick_file_util.js b/node/test/util/cherry_pick_file_util.js new file mode 100644 index 000000000..e4e197fb1 --- /dev/null +++ b/node/test/util/cherry_pick_file_util.js @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const fs = require("fs-promise"); +const path = require("path"); + +const CherryPickFileUtil = require("../../lib//util/cherry_pick_file_util"); +const CherryPick = require("../../lib//util/cherry_pick"); +const TestUtil = require("../../lib/util/test_util"); + +describe("CherryPickFileUtil", function () { + describe("readCherryPick", function () { + it("found", co.wrap(function *() { + const tempDir = yield TestUtil.makeTempDir(); + const gitDir = path.join(tempDir, "META_CHERRY_PICK"); + yield fs.mkdir(gitDir); + yield fs.writeFile(path.join(gitDir, "ORIG_HEAD"), "123\n"); + yield fs.writeFile(path.join(gitDir, "PICK"), "456\n"); + const result = yield CherryPickFileUtil.readCherryPick(tempDir); + assert.deepEqual(result, new CherryPick("123", "456")); + })); + it("not found", co.wrap(function *() { + const tempDir = yield TestUtil.makeTempDir(); + const gitDir = path.join(tempDir, "META_CHERRY_PICK"); + yield fs.mkdir(gitDir); + yield fs.writeFile(path.join(gitDir, "ORIG_HEAD"), "123\n"); + const result = yield CherryPickFileUtil.readCherryPick(tempDir); + assert.isNull(result); + })); + }); + it("writeCherryPick", co.wrap(function *() { + const tempDir = yield TestUtil.makeTempDir(); + const cherryPick = new CherryPick("bar", "baz"); + yield CherryPickFileUtil.writeCherryPick(tempDir, cherryPick); + const root = path.join(tempDir, "META_CHERRY_PICK"); + const originalHead = yield fs.readFile(path.join(root, "ORIG_HEAD"), + "utf8"); + assert.equal(originalHead, "bar\n"); + const pick = yield fs.readFile(path.join(root, "PICK"), "utf8"); + assert.equal(pick, "baz\n"); + })); + it("cleanCherryPick", co.wrap(function *() { + const tempDir = yield TestUtil.makeTempDir(); + yield CherryPickFileUtil.writeCherryPick( + tempDir, + new CherryPick("there", "all")); + yield CherryPickFileUtil.cleanCherryPick(tempDir); + const result = yield CherryPickFileUtil.readCherryPick(tempDir); + assert.isNull(result); + })); +}); diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js new file mode 100644 index 000000000..ccc0e3884 --- /dev/null +++ b/node/test/util/cherry_pick_util.js @@ -0,0 +1,828 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const colors = require("colors"); +const NodeGit = require("nodegit"); + +const ConflictUtil = require("../../lib/util/conflict_util"); +const CherryPickUtil = require("../../lib/util/cherry_pick_util"); +const Open = require("../../lib/util/open"); +const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const Submodule = require("../../lib/util/submodule"); +const SubmoduleChange = require("../../lib/util/submodule_change"); + +describe("CherryPickUtil", function () { +const Conflict = ConflictUtil.Conflict; +const ConflictEntry = ConflictUtil.ConflictEntry; +const FILEMODE = NodeGit.TreeEntry.FILEMODE; + +describe("changeSubmodules", function () { + const cases = { + "noop": { + input: "x=S", + submodules: {}, + }, + "simple": { + input: "x=S", + submodules: { + "s": new Submodule("/a", "1"), + }, + expected: "x=S:I s=S/a:1", + }, + "update open": { + input: "a=B|x=U:Os Ca-1!Ba=a", + submodules: { + "s": new Submodule("a", "a"), + }, + expected: "x=E:I s=Sa:a;Os Ba=a", + }, + "update open, need fetch": { + input: "a=B:Ca-1;Ba=a|x=U:Os", + submodules: { + "s": new Submodule("a", "a"), + }, + expected: "x=E:I s=Sa:a;Os H=a", + }, + "simple and update open": { + input: "a=B:Ca-1;Ba=a|x=U:Os", + submodules: { + "a": new Submodule("a", "1"), + "s": new Submodule("a", "a"), + }, + expected: "x=E:I a=Sa:1,s=Sa:a;Os H=a", + }, + "nested": { + input: "x=S", + submodules: { + "s/t/u": new Submodule("/a", "1"), + }, + expected: "x=S:I s/t/u=S/a:1", + }, + "multiple": { + input: "x=S", + submodules: { + "s/t/u": new Submodule("/a", "1"), + "z/t/u": new Submodule("/b", "1"), + }, + expected: "x=S:I s/t/u=S/a:1,z/t/u=S/b:1", + }, + "added": { + input: "a=B|x=U", + submodules: { + "s": new Submodule("/a", "1"), + }, + expected: "x=E:I s=S/a:1", + }, + "removed": { + input: "a=B|x=U", + submodules: { + "s": null, + }, + expected: "x=E:I s", + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const adder = co.wrap(function *(repos, maps) { + const repo = repos.x; + const subs = {}; + Object.keys(c.submodules).forEach(name => { + const sub = c.submodules[name]; + if (null !== sub) { + const sha = maps.reverseCommitMap[sub.sha]; + const url = maps.reverseUrlMap[sub.url] || sub.url; + subs[name] = new Submodule(url, sha); + } + else { + subs[name] = null; + } + }); + const opener = new Open.Opener(repo, null); + const index = yield repo.index(); + yield CherryPickUtil.changeSubmodules(repo, + opener, + index, + subs); + yield index.write(); + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.input, + c.expected, + adder, + c.fails); + })); + }); +}); + +describe("containsUrlChanges", function () { + const cases = { + "no subs, no parent": { + input: "S", + expected: false, + }, + "added a sub": { + input: "S:C2-1 s=Sa:1;H=2", + expected: false, + }, + "removed a sub": { + input: "S:C2-1 s=Sa:1;C3-2 s;H=3", + expected: false, + }, + "changed a sha": { + input: "S:Ca-1;C2-1 s=Sa:1;C3-2 s=Sa:a;H=3;Ba=a", + expected: false, + }, + "changed a URL": { + input: "S:C2-1 s=Sa:1;C3-2 s=Sb:1;H=3", + expected: true, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(c.input); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const result = yield CherryPickUtil.containsUrlChanges(repo, head); + assert.equal(result, c.expected); + })); + }); +}); +describe("computeChanges", function () { + const Conflict = ConflictUtil.Conflict; + const ConflictEntry = ConflictUtil.ConflictEntry; + const FILEMODE = NodeGit.TreeEntry.FILEMODE; + const cases = { + "ffwd change": { + input: "a=B:Ca-1;Ba=a|x=U:Ct-2 s=Sa:a;Bt=t", + simpleChanges: { + "s": new Submodule("a", "a"), + }, + }, + "non-ffwd change": { + input: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;Ct-2 s=Sa:b;Bt=t;Bmaster=3`, + changes: { + "s": new SubmoduleChange("1", "b"), + }, + }, + "addition": { + input: "a=B|x=S:Ct-1 s=Sa:1;Bt=t", + simpleChanges: { + "s": new Submodule("a", "1"), + }, + }, + "double addition": { + input: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=S:C2-1 s=Sa:a;Ct-1 s=Sa:b;Bmaster=2;Bt=t`, + conflicts: { + "s": new Conflict(null, + new ConflictEntry(FILEMODE.COMMIT, "a"), + new ConflictEntry(FILEMODE.COMMIT, "b")), + }, + }, + "same double addition": { + input: ` +a=B:Ca-1;Ba=a| +x=S:C2-1 s=Sa:a;Ct-1 s=Sa:a;Bmaster=2;Bt=t`, + }, + "deletion": { + input: "a=B|x=U:Ct-2 s;Bt=t", + simpleChanges: { + "s": null, + }, + }, + "double deletion": { + input: "a=B|x=U:C3-2 s;Ct-2 s;Bt=t;Bmaster=3", + }, + "change, but gone on HEAD": { + input: "a=B:Ca-1;Ba=a|x=U:C3-2 s;Ct-2 s=Sa:a;Bt=t;Bmaster=3", + conflicts: { + "s": new Conflict(new ConflictEntry(FILEMODE.COMMIT, "1"), + null, + new ConflictEntry(FILEMODE.COMMIT, "a")), + }, + }, + "change, but never on HEAD": { + input: "a=B:Ca-1;Ba=a|x=U:Bmaster=1;Ct-2 s=Sa:a;Bt=t", + conflicts: { + "s": new Conflict(null, + null, + new ConflictEntry(FILEMODE.COMMIT, "a")), + }, + }, + "deletion, but not a submodule any more": { + input: "a=B|x=U:C3-2 s=foo;Ct-2 s;Bmaster=3;Bt=t", + conflicts: { + "s": new Conflict(new ConflictEntry(FILEMODE.COMMIT, "1"), + new ConflictEntry(FILEMODE.BLOB, "foo"), + null), + }, + }, + "deletion, but was changed on HEAD": { + input: ` +a=B:Ca-1;Ba=a| +x=U:C3-2 s=Sa:a;Ct-2 s;Bt=t;Bmaster=3`, + conflicts: { + "s": new Conflict(new ConflictEntry(FILEMODE.COMMIT, "1"), + new ConflictEntry(FILEMODE.COMMIT, "a"), + null), + }, + }, + "change, but gone on HEAD and no ancestor": { + input: "a=B:Ca-1;Ba=a|x=S:C2 s=Sa:1;Ct-2 s=Sa:a;Bt=t", + conflicts: { + "s": new Conflict(null, + null, + new ConflictEntry(FILEMODE.COMMIT, "a")), + }, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const w = yield RepoASTTestUtil.createMultiRepos(c.input); + const repos = w.repos; + const repo = repos.x; + const commitMap = w.commitMap; + const reverseCommitMap = w.reverseCommitMap; + const urlMap = w.urlMap; + const target = yield repo.getCommit(reverseCommitMap.t); + const result = yield CherryPickUtil.computeChanges(repo, target); + + const changes = {}; + for (let name in result.changes) { + const change = result.changes[name]; + changes[name] = new SubmoduleChange(commitMap[change.oldSha], + commitMap[change.newSha]); + } + assert.deepEqual(changes, c.changes || {}); + + const simpleChanges = {}; + for (let name in result.simpleChanges) { + const change = result.simpleChanges[name]; + let mapped = null; + if (null !== change) { + mapped = new Submodule(urlMap[change.url], + commitMap[change.sha]); + } + simpleChanges[name] = mapped; + } + + const mapConflict = co.wrap(function *(entry) { + if (null === entry) { + return entry; + } + if (FILEMODE.COMMIT === entry.mode) { + return new ConflictEntry(entry.mode, + commitMap[entry.id]); + } + const data = + yield repo.getBlob(NodeGit.Oid.fromString(entry.id)); + return new ConflictEntry(entry.mode, data.toString()); + }); + assert.deepEqual(simpleChanges, c.simpleChanges || {}); + + const conflicts = {}; + for (let name in result.conflicts) { + const conflict = result.conflicts[name]; + const ancestor = yield mapConflict(conflict.ancestor); + const our = yield mapConflict(conflict.our); + const their = yield mapConflict(conflict.their); + conflicts[name] = new Conflict(ancestor, our, their); + } + assert.deepEqual(conflicts, c.conflicts || {}); + })); + }); +}); +describe("pickSubs", function () { + // Most of the logic is done via `RebaseUtil.rewriteCommits`. We need to + // validate that we invoke that method correctly and that we fetch commits + // as needed. + + const cases = { + "no subs": { + state: "x=S", + subs: {}, + }, + "pick a sub": { + state: `a=B:Ca-1;Cb-1;Ba=a;Bb=b|x=U:C3-2 s=Sa:a;H=3`, + subs: { + "s": new SubmoduleChange("1", "b"), + }, + expected: `x=E:Os Cbs-a b=b!H=bs;I s=Sa:bs`, + }, + "pick two": { + state: ` +a=B:Ca-1;Caa-1;Cb-1;Cc-b;Ba=a;Bb=b;Bc=c;Baa=aa| +x=U:C3-2 s=Sa:a,t/u=Sa:a;H=3 +`, + subs: { + "s": new SubmoduleChange("1", "aa"), + "t/u": new SubmoduleChange("1", "c"), + }, + expected: ` +x=E:Os Caas-a aa=aa!H=aas;Ot/u Cct/u-bt/u c=c!Cbt/u-a b=b!H=ct/u; + I s=Sa:aas,t/u=Sa:ct/u`, + }, + "a conflict": { + state: `a=B:Ca-1;Cb-1 a=foo;Ba=a;Bb=b|x=U:C3-2 s=Sa:a;H=3`, + subs: { + "s": new SubmoduleChange("1", "b"), + }, + conflicts: { + "s": "b", + }, + expected: `x=E:Os I *a=~*a*foo!Edetached HEAD,b,a!W a=\ +<<<<<<< HEAD +a +======= +foo +>>>>>>> message +;`, + }, + "commit and a conflict": { + state: ` +a=B:Ca-1;Cb-1;Cc-b a=foo;Ba=a;Bb=b;Bc=c| +x=U:C3-2 s=Sa:a;H=3`, + subs: { + "s": new SubmoduleChange("1", "c"), + }, + conflicts: { + "s": "c", + }, + expected: ` +x=E:I s=Sa:bs;Os Cbs-a b=b!H=bs!I *a=~*a*foo!Edetached HEAD,c,a!W a=\ +<<<<<<< HEAD +a +======= +foo +>>>>>>> message +;`, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + const picker = co.wrap(function *(repos, maps) { + const repo = repos.x; + const index = yield repo.index(); + const commitMap = maps.commitMap; + const reverseMap = maps.reverseCommitMap; + const subs = {}; + Object.keys(c.subs).forEach(name => { + const change = c.subs[name]; + subs[name] = new SubmoduleChange(reverseMap[change.oldSha], + reverseMap[change.newSha]); + }); + const opener = new Open.Opener(repo, null); + const result = yield CherryPickUtil.pickSubs(repo, + opener, + index, + subs); + yield index.write(); + const conflicts = {}; + Object.keys(result.conflicts).forEach(name => { + const sha = result.conflicts[name]; + conflicts[name] = commitMap[sha]; + }); + assert.deepEqual(conflicts, c.conflicts || {}, "conflicts"); + const mappedCommits = {}; + Object.keys(result.commits).forEach(name => { + const subCommits = result.commits[name]; + Object.keys(subCommits).forEach(newSha => { + const oldSha = subCommits[newSha]; + const oldLogicalSha = commitMap[oldSha]; + const newLogicalSha = oldLogicalSha + name; + mappedCommits[newSha] = newLogicalSha; + }); + }); + return { + commitMap: mappedCommits, + }; + }); + + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.state, + c.expected, + picker, + c.fails); + })); + }); +}); +describe("writeConflicts", function () { + const cases = { + "trivial": { + state: "x=S", + conflicts: {}, + result: "", + }, + "with a conflict": { + state: "x=S", + conflicts: { + "README.md": new Conflict(null, + null, + new ConflictEntry(FILEMODE.COMMIT, + "1")), + }, + expected: "x=E:I *README.md=~*~*S:1;W README.md=hello world", + result: `\ +Conflicting entries for submodule ${colors.red("README.md")} +`, + }, + "two conflicts": { + state: "x=S", + conflicts: { + z: new Conflict(null, + null, + new ConflictEntry(FILEMODE.COMMIT, "1")), + a: new Conflict(null, + null, + new ConflictEntry(FILEMODE.COMMIT, "1")), + }, + expected: "x=E:I *z=~*~*S:1,*a=~*~*S:1", + result: `\ +Conflicting entries for submodule ${colors.red("a")} +Conflicting entries for submodule ${colors.red("z")} +`, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const writer = co.wrap(function *(repos, maps) { + const repo = repos.x; + const index = yield repo.index(); + const conflicts = {}; + function mapEntry(entry) { + if (null === entry || FILEMODE.COMMIT !== entry.mode) { + return entry; + } + return new ConflictEntry(entry.mode, + maps.reverseCommitMap[entry.id]); + } + for (let name in c.conflicts) { + const con = c.conflicts[name]; + conflicts[name] = new Conflict(mapEntry(con.ancestor), + mapEntry(con.our), + mapEntry(con.their)); + } + const result = + yield CherryPickUtil.writeConflicts(repo, index, conflicts); + yield index.write(); + assert.equal(result, c.result); + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.state, + c.expected, + writer, + c.fails); + })); + }); +}); +describe("cherryPick", function () { + // We will always cherry-pick commit 8 from repo x and will name the + // meta-repo cherry-picked commit 9. Cherry-picked commits from + // submodules will have the submodule name appended to the commit. + // + // One concern with these tests is that the logic used by Git to generate + // new commit ids can result in collision when the exact same commit would + // be generated very quickly. For example, if you have this setup: + // + // S:C2-1;Bfoo=2 + // + // And try to cherry-pick "2" to master, it will likely generate the exact + // same physical commit id already mapped to "2" and screw up the test, + // which expects a new commit id. This problem means that we cannot easily + // test that case (without adding timers), which is OK as we're not + // actually testing libgit2's cherry-pick facility, but also that we need + // to have unique commit histories in our submodules -- we can't have 'S' + // everywhere for these tests. + // + // Cases to check: + // * add when already exists + // * delete when already delete + // * change when deleted + // * conflicts + + const cases = { + "picking one sub": { + input: ` +a=Ax:Cz-y;Cy-x;Bfoo=z| +x=S:C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2;Os H=x`, + expected: "x=E:C9-2 s=Sa:zs;Bmaster=9;Os Czs-x z=z!H=zs", + }, + "nothing to commit": { + input: "a=B|x=S:C2-1;C8-1 ;Bmaster=2;B8=8", + fails: true, + }, + "in-progress will fail": { + input: ` +a=Ax:Cz-y;Cy-x;Bfoo=z| +x=S:P2,1;C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2`, + fails: true, + }, + "dirty will fail": { + input: ` +a=Ax:Cz-y;Cy-x;Bfoo=z| +x=S:C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2;Os W x=9`, + fails: true, + }, + "URL change will fail": { + input: ` +a=Ax:Cz-y;Cy-x;Bfoo=z|b=B| +x=S:C8-3 s=Sb:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2`, + fails: true, + }, + "meta change will fail": { + input: ` +a=Ax:Cz-y;Cy-x;Bfoo=z| +x=S:C8-3 s=Sa:z,README.md=9;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2`, + fails: true, + }, + "picking one ffwd sub": { + input: ` +a=Ax:Cz-x;Bfoo=z| +x=S:C8-2 s=Sa:z;C3-2;C2-1 s=Sa:x;Bfoo=8;Bmaster=3;Os H=x`, + expected: "x=E:C9-3 s=Sa:z;Bmaster=9;Os", + }, + "picking one sub introducing two commits": { + input: ` +a=Aw:Cz-y;Cy-x;Cx-w;Bfoo=z| +x=S:C8-3 s=Sa:z;C3-2 s=Sa:x;C2-1 s=Sa:w;Bfoo=8;Bmaster=2;Os H=w`, + expected: ` +x=E:C9-2 s=Sa:zs;Bmaster=9;Os Czs-ys z=z!Cys-w y=y!H=zs`, + }, + "picking closed sub": { + input: ` +a=Ax:Cz-y;Cy-x;Bfoo=z| +x=S:C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2`, + expected: "x=E:C9-2 s=Sa:zs;Bmaster=9;Os Czs-x z=z!H=zs", + }, + "picking closed sub with change": { + input: "\ +a=Ax:Cw-x;Cz-x;Cy-x;Bfoo=z;Bbar=y;Bbaz=w|\ +x=S:C4-2 s=Sa:w;C8-3 s=Sa:z;C3-2;C2-1 s=Sa:y;Bfoo=8;Bmaster=4", + expected: "x=E:C9-4 s=Sa:zs;Bmaster=9;Os Czs-w z=z!H=zs", + }, + "picking two closed subs": { + input: ` +a=Ax:Cz-y;Cy-x;Bfoo=z| +b=Aa:Cc-b;Cb-a;Bfoo=c| +x=S:C8-3 s=Sa:z,t=Sb:c;C3-2 s=Sa:y,t=Sb:b;C2-1 s=Sa:x,t=Sb:a; +Bfoo=8;Bmaster=2`, + expected: ` +x=E:C9-2 s=Sa:zs,t=Sb:ct; +Bmaster=9; +Os Czs-x z=z!H=zs; +Ot Cct-a c=c!H=ct`, + }, + "new sub on head": { + input: ` +a=B| +x=U:C8-2 r=Sa:1;C4-2 t=Sa:1;Bmaster=4;Bfoo=8`, + expected: "x=E:C9-4 r=Sa:1;Bmaster=9", + }, + "don't pick subs from older commit": { + input: ` +a=B:Cr-1;Cq-r;Bq=q| +x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q,t=Sa:r;C8-3 t=Sa:q;Bmaster=2;Bfoo=8`, + expected: ` +x=E:C9-2 t=Sa:qt;Bmaster=9;Ot Cqt-1 q=q!H=qt`, + }, + "remove a sub": { + input: "a=B|x=U:C3-2;Bmaster=3;C8-2 s;Bfoo=8", + expected: "a=B|x=E:C9-3 s;Bmaster=9", + }, + "conflict in a sub pick": { + input: ` +a=B:Ca-1;Cb-1 a=8;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C8-2 s=Sa:b;Bmaster=3;Bfoo=8`, + expected: ` +x=E:P3,8;Os Edetached HEAD,b,a!I *a=~*a*8!W a=\ +<<<<<<< HEAD +a +======= +8 +>>>>>>> message +; +`, + errorMessage: `\ +Submodule ${colors.red("s")} is conflicted. +`, + }, + "conflict in a sub pick, success in another": { + input: ` +a=B:Ca-1;Cb-1 a=8;Cc-1;Ba=a;Bb=b;Bc=c| +x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:a,t=Sa:a;C8-2 s=Sa:b,t=Sa:c;Bmaster=3;Bfoo=8`, + expected: ` +x=E:P3,8;I t=Sa:ct;Ot Cct-a c=c!H=ct; +Os Edetached HEAD,b,a!I *a=~*a*8!W a=\ +<<<<<<< HEAD +a +======= +8 +>>>>>>> message +; +`, + errorMessage: `\ +Submodule ${colors.red("s")} is conflicted. +`, + }, + "type conflict": { + input: ` +a=B:Ca-1;Ba=a| +x=U:C3-2 s=foo;C8-2 s=Sa:a;Bmaster=3;Bfoo=8`, + expected: `x=E:P3,8;I *s=S:1*foo*S:a;W s=foo`, + errorMessage: `\ +Conflicting entries for submodule ${colors.red("s")} +`, + }, + }; + + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + const picker = co.wrap(function *(repos, maps) { + const x = repos.x; + const oldMap = maps.commitMap; + const reverseCommitMap = maps.reverseCommitMap; + assert.property(reverseCommitMap, "8"); + const eightCommitSha = reverseCommitMap["8"]; + const eightCommit = yield x.getCommit(eightCommitSha); + const result = yield CherryPickUtil.cherryPick(x, eightCommit); + + assert.equal(result.errorMessage, c.errorMessage || null); + + // Now we need to build a map from new physical commit id to new + // logical commit id. For the meta commit, this is easy: we map + // the new id to the hard-coded value of "9". + + let commitMap = {}; + commitMap[result.newMetaCommit] = "9"; + + // For the submodules, we need to first figure out what the old + // logical commit (the one from the shorthand) was, then create the + // new logical commit id by appending the submodule name. We map + // the new (physical) commit id to this new logical id. + + Object.keys(result.submoduleCommits).forEach(name => { + const subCommits = result.submoduleCommits[name]; + Object.keys(subCommits).forEach(newPhysicalId => { + const oldId = subCommits[newPhysicalId]; + const oldLogicalId = oldMap[oldId]; + const newLogicalId = oldLogicalId + name; + commitMap[newPhysicalId] = newLogicalId; + }); + }); + return { + commitMap: commitMap, + }; + }); + + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.input, + c.expected, + picker, + c.fails); + })); + }); +}); +describe("continue", function () { + const cases = { + "no cherry-pick": { + input: "a=B|x=U:Os I foo=bar;Cfoo#g;Bg=g", + fails: true, + }, + "conflicted": { + input: "a=B|x=U:Os I foo=bar,*x=a*b*c;Cfoo#g;Bg=g;P2,g", + fails: true, + }, + "continue with a staged submodule commit": { + input: "a=B:Ca-1;Ba=a|x=U:I s=Sa:a;Cmoo#g;Bg=g;P2,g", + expected: "x=E:Cmoo#CP-2 s=Sa:a;Bmaster=CP;P;I s=~", + }, + "regular continue": { + input: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:b;Cfoo#g-2;Bg=g;P1,g;Bmaster=3;Os EHEAD,b,a!I b=b`, + expected: ` +x=E:P;Cfoo#CP-3 s=Sa:bs;Bmaster=CP;Os Cbs-a b=b!E`, + }, + "nothing to do": { + input: "a=B|x=U:Os;Cfoo#g;Bg=g;P2,g", + expected: "x=E:P", + fails: true, + }, + "continue with staged files": { + input: "a=B|x=U:Os I foo=bar;Cfoo#g;Bg=g;P2,g", + expected: ` +x=E:Cfoo#CP-2 s=Sa:Ns;Bmaster=CP;Os Cfoo#Ns-1 foo=bar;P`, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const continuer = co.wrap(function *(repos, maps) { + const repo = repos.x; + const result = yield CherryPickUtil.continue(repo); + const commitMap = {}; + RepoASTTestUtil.mapSubCommits(commitMap, + result.submoduleCommits, + maps.commitMap); + Object.keys(result.newSubmoduleCommits).forEach(name => { + const sha = result.newSubmoduleCommits[name]; + commitMap[sha] = `N${name}`; + }); + if (null !== result.newMetaCommit) { + commitMap[result.newMetaCommit] = "CP"; + } + assert.equal(result.errorMessage, c.errorMessage || null); + return { + commitMap: commitMap, + }; + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.input, + c.expected, + continuer, + c.fails); + })); + }); +}); + +describe("abort", function () { + const cases = { + "no cherry": { + input: "x=S", + fails: true, + }, + "some changes in meta": { + input: "x=S:C2-1;Bmaster=2;P1,2", + expected: "x=S", + }, + "some conflicted changes in meta": { + input: ` +x=S:C2-1;Bmaster=2;P1,2;I *README.md=b*c*d;W README.md=8`, + expected: "x=S", + }, + + "sub with a conflict": { + input: ` +a=B:Ca-1;Cb-1 a=8;Ba=a;Bb=b| +x=U:P3,8;C3-2 s=Sa:a;C8-2 s=Sa:b;Bmaster=3;Bfoo=8; + Os Ba=a!Bb=b!Edetached HEAD,b,a!I *a=~*a*8!W a=\ +<<<<<<< HEAD +a +======= +8 +>>>>>>> message +; +`, + expected: `x=E:P;Os E!I a=~!W a=~!Ba=a!Bb=b`, + } + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const aborter = co.wrap(function *(repos) { + const repo = repos.x; + yield CherryPickUtil.abort(repo); + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.input, + c.expected, + aborter, + c.fails); + })); + }); +}); +}); diff --git a/node/test/util/cherrypick.js b/node/test/util/cherrypick.js deleted file mode 100644 index 65ba45457..000000000 --- a/node/test/util/cherrypick.js +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (c) 2016, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -const assert = require("chai").assert; -const co = require("co"); - -const Cherrypick = require("../../lib/util/cherrypick"); -const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); - -describe("cherrypick", function () { - // We will always cherry-pick commit 8 from repo x and will name the - // meta-repo cherry-picked commit 9. Cherry-picked commits from - // submodules will have the submodule name appended to the commit. - // - // One concern with these tests is that the logic used by Git to generate - // new commit ids can result in collision when the exact same commit would - // be generated very quickly. For example, if you have this setup: - // - // S:C2-1;Bfoo=2 - // - // And try to cherry-pick "2" to master, it will likely generate the exact - // same physical commit id already mapped to "2" and screw up the test, - // which expects a new commit id. This problem means that we cannot easily - // test that case (without adding timers), which is OK as we're not - // actually testing libgit2's cherry-pick facility, but also that we need - // to have unique commit histories in our submodules -- we can't have 'S' - // everywhere for these tests. - - const picker = co.wrap(function *(repos, maps) { - const x = repos.x; - const oldMap = maps.commitMap; - const reverseCommitMap = maps.reverseCommitMap; - assert.property(reverseCommitMap, "8"); - const eightCommitSha = reverseCommitMap["8"]; - const eightCommit = yield x.getCommit(eightCommitSha); - const result = yield Cherrypick.cherryPick(x, eightCommit); - - // Now we need to build a map from new physical commit id to new - // logical commit id. For the meta commit, this is easy: we map the - // new id to the hard-coded value of "9". - - let commitMap = {}; - commitMap[result.newMetaCommit] = "9"; - - // For the submodules, we need to first figure out what the old logical - // commit (the one from the shorthand) was, then create the new logical - // commit id by appending the submodule name. We map the new - // (physical) commit id to this new logical id. - - Object.keys(result.submoduleCommits).forEach(name => { - const subCommits = result.submoduleCommits[name]; - Object.keys(subCommits).forEach(oldId => { - const oldLogicalId = oldMap[oldId]; - const newLogicalId = oldLogicalId + name; - const newPhysicalId = subCommits[oldId]; - commitMap[newPhysicalId] = newLogicalId; - }); - }); - return { - commitMap: commitMap, - }; - }); - - const cases = { - "simplest": { - input: "x=S:C8-2;C2-1;Bfoo=8", - expected:"x=E:C9-1 8=8;Bmaster=9", - }, - "no change to sub": { - input: "a=Ax|x=S:C8-2;C2-1 s=Sa:x;Bfoo=8", - expected: "x=E:C9-1 8=8;Bmaster=9", - }, - "picking one sub": { - input: "\ -a=Ax:Cz-y;Cy-x;Bfoo=z|\ -x=S:C8-3 s=Sa:z;C3-2;C2-1 s=Sa:x;Bfoo=8;Bmaster=2;Os H=x", - expected: "x=E:C9-2 s=Sa:zs;Bmaster=9;Os Czs-x z=z!H=zs", - }, - "picking closed sub": { - input: "\ -a=Ax:Cz-y;Cy-x;Bfoo=z|\ -x=S:C8-3 s=Sa:z;C3-2;C2-1 s=Sa:x;Bfoo=8;Bmaster=2", - expected: "x=E:C9-2 s=Sa:zs;Bmaster=9;Os Czs-x z=z!H=zs", - }, - "picking closed sub with change": { - input: "\ -a=Ax:Cw-x;Cz-x;Cy-x;Bfoo=z;Bbar=y;Bbaz=w|\ -x=S:C4-2 s=Sa:w;C8-3 s=Sa:z;C3-2;C2-1 s=Sa:y;Bfoo=8;Bmaster=4", - expected: "x=E:C9-4 s=Sa:zs;Bmaster=9;Os Czs-w z=z!H=zs", - }, - "picking two closed subs": { - input: "\ -a=Ax:Cz-y;Cy-x;Bfoo=z|\ -b=Aa:Cc-b;Cb-a;Bfoo=c|\ -x=S:\ -C8-3 s=Sa:z,t=Sb:c;C3-2;C2-1 s=Sa:x,t=Sb:a;\ -Bfoo=8;Bmaster=2", - expected: "\ -x=E:C9-2 s=Sa:zs,t=Sb:ct;\ -Bmaster=9;\ -Os Czs-x z=z!H=zs;\ -Ot Cct-a c=c!H=ct", - }, - "new sub on head": { - input: ` -a=B| -x=U:C8-2;C4-2 t=Sa:1;Bmaster=4;Bfoo=8`, - expected: "x=E:C9-4 8=8;Bmaster=9", - }, - "don't pick subs from older commit": { - input: ` -a=B:Cr-1;Cq-r;Bq=q| -x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q;C8-3 t=Sa:q;Bmaster=2;Bfoo=8`, - expected: ` -x=E:C9-2 t=Sa:qt;Bmaster=9;Ot Cqt-1 q=q!H=qt`, - }, - }; - - Object.keys(cases).forEach(caseName => { - const c = cases[caseName]; - it(caseName, co.wrap(function *() { - yield RepoASTTestUtil.testMultiRepoManipulator(c.input, - c.expected, - picker, - c.fails); - })); - }); -}); diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 1f7a5a1e2..c5ed9eb95 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -1072,7 +1072,9 @@ a=B:Chi#a-1;Ba=a|x=U:C3-2 s=Sa:a;Bmaster=3;Os W README.md=888`, let oldSubs = {}; if (null !== parent) { oldSubs = - yield SubmoduleUtil.getSubmodulesForCommit(repo, parent); + yield SubmoduleUtil.getSubmodulesForCommit(repo, + parent, + null); } const old = oldSubs.s || null; const getRepo = co.wrap(function *() { diff --git a/node/test/util/conflict_util.js b/node/test/util/conflict_util.js new file mode 100644 index 000000000..a595aea82 --- /dev/null +++ b/node/test/util/conflict_util.js @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const fs = require("fs-promise"); +const NodeGit = require("nodegit"); +const path = require("path"); + +const ConflictUtil = require("../../lib/util/conflict_util"); +const TestUtil = require("../../lib/util/test_util"); + +const hashObject = co.wrap(function *(repo, data) { + const db = yield repo.odb(); + const BLOB = 3; + return yield db.write(data, data.length, BLOB); +}); + +describe("ConflictUtil", function () { +const ConflictEntry = ConflictUtil.ConflictEntry; +const Conflict = ConflictUtil.Conflict; +describe("ConflictEntry", function () { + it("breathe", function () { + const entry = new ConflictUtil.ConflictEntry(2, "1"); + assert.equal(entry.mode, 2); + assert.equal(entry.id, "1"); + }); +}); +describe("Conflict", function () { + it("breathe", function () { + const ancestor = new ConflictEntry(2, "1"); + const our = new ConflictEntry(3, "1"); + const their = new ConflictEntry(4, "1"); + const conflict = new Conflict(ancestor, our, their); + assert.equal(conflict.ancestor, ancestor); + assert.equal(conflict.our, our); + assert.equal(conflict.their, their); + + const nullC = new Conflict(null, null, null); + assert.isNull(nullC.ancestor); + assert.isNull(nullC.our); + assert.isNull(nullC.their); + }); +}); +describe("addConflict", function () { + const FILEMODE = NodeGit.TreeEntry.FILEMODE; + it("existing", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const makeEntry = co.wrap(function *(data) { + const id = yield hashObject(repo, data); + return new ConflictEntry(FILEMODE.BLOB, id.tostrS()); + }); + const ancestor = yield makeEntry("xxx"); + const our = yield makeEntry("yyy"); + const their = yield makeEntry("zzz"); + const index = yield repo.index(); + const conflict = new Conflict(ancestor, our, their); + const filename = "README.md"; + yield ConflictUtil.addConflict(index, filename, conflict); + yield index.write(); + yield fs.writeFile(path.join(repo.workdir(), filename), "foo"); + const ancestorEntry = index.getByPath(filename, 1); + assert.equal(ancestorEntry.id, ancestor.id); + const ourEntry = index.getByPath(filename, 2); + assert.equal(ourEntry.id, our.id); + const theirEntry = index.getByPath(filename, 3); + assert.equal(theirEntry.id, their.id); + })); + it("multiple values", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const makeEntry = co.wrap(function *(data) { + const id = yield hashObject(repo, data); + return new ConflictEntry(FILEMODE.BLOB, id.tostrS()); + }); + const ancestor = yield makeEntry("xxx"); + const our = yield makeEntry("yyy"); + const their = yield makeEntry("zzz"); + const conflict = new Conflict(ancestor, our, their); + const index = yield repo.index(); + const path = "foo/bar.md"; + yield ConflictUtil.addConflict(index, path, conflict); + const ancestorEntry = index.getByPath(path, 1); + assert.equal(ancestorEntry.id, ancestor.id); + const ourEntry = index.getByPath(path, 2); + assert.equal(ourEntry.id, our.id); + const theirEntry = index.getByPath(path, 3); + assert.equal(theirEntry.id, their.id); + })); + it("with null", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const makeEntry = co.wrap(function *(data) { + const id = yield hashObject(repo, data); + return new ConflictEntry(FILEMODE.BLOB, id.tostrS()); + }); + const ancestor = yield makeEntry("xxx"); + const their = yield makeEntry("zzz"); + const conflict = new Conflict(ancestor, null, their); + const index = yield repo.index(); + const path = "foo/bar.md"; + yield ConflictUtil.addConflict(index, path, conflict); + const ancestorEntry = index.getByPath(path, 1); + assert.equal(ancestorEntry.id, ancestor.id); + const ourEntry = index.getByPath(path, 2); + assert.equal(ourEntry, null); + const theirEntry = index.getByPath(path, 3); + assert.equal(theirEntry.id, their.id); + })); +}); +}); diff --git a/node/test/util/diff_util.js b/node/test/util/diff_util.js index 396ca92bd..8b808ac7e 100644 --- a/node/test/util/diff_util.js +++ b/node/test/util/diff_util.js @@ -56,10 +56,6 @@ describe("DiffUtil", function () { input: DELTA.DELETED, expected: FILESTATUS.REMOVED, }, - "conflicted": { - input: DELTA.CONFLICTED, - expected: FILESTATUS.CONFLICTED, - }, "renamed": { input: DELTA.RENAMED, expected: FILESTATUS.RENAMED, @@ -88,6 +84,9 @@ describe("DiffUtil", function () { "trivial": { input: "x=S", }, + "conflict ignored": { + input: "x=S:I *READMEmd=a*b*c", + }, "index - modified": { input: "x=S:I README.md=hhh", staged: { "README.md": FILESTATUS.MODIFIED }, diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index ec07fb585..aa9fe9777 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -312,7 +312,14 @@ x=S:C2-1 s=Sa:1;C3-1 s=Sb:1;Bmaster=2;Bfoo=3`, "conflict in meta": { initial: "x=S:C2-1 foo=bar;C3-1 foo=baz;Bmaster=2;Bfoo=3", fromCommit: "3", - expected: "x=E:Mmessage\n,2,3", + expected: ` +x=E:Mmessage\n,2,3;I *foo=~*bar*baz;W foo=<<<<<<< ours +bar +======= +baz +>>>>>>> theirs +; +`, fails: true, }, "conflict in submodule": { @@ -320,7 +327,16 @@ x=S:C2-1 s=Sa:1;C3-1 s=Sb:1;Bmaster=2;Bfoo=3`, a=B:Ca-1 README.md=8;Cb-1 README.md=9;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, fromCommit: "4", - expected: "x=E:Mmessage\n,3,4;Os Mmessage\n,a,b", + expected: ` +x=E:Mmessage\n,3,4;I *s=S:1*S:a*S:b; +Os Mmessage\n,a,b!I *README.md=hello world*8*9!W README.md=\ +<<<<<<< ours +8 +======= +9 +>>>>>>> theirs +; +`, fails: true, }, "new commit in sub in target branch but not in HEAD branch": { diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js index bca4fd995..3de16dbf1 100644 --- a/node/test/util/print_status_util.js +++ b/node/test/util/print_status_util.js @@ -32,13 +32,17 @@ const assert = require("chai").assert; const colors = require("colors"); +const NodeGit = require("nodegit"); const Rebase = require("../../lib/util/rebase"); const Merge = require("../../lib/util/merge"); +const CherryPick = require("../../lib/util/cherry_pick"); const RepoStatus = require("../../lib/util/repo_status"); const PrintStatusUtil = require("../../lib/util/print_status_util"); describe("PrintStatusUtil", function () { + const FILEMODE = NodeGit.TreeEntry.FILEMODE; + const BLOB = FILEMODE.BLOB; const FILESTATUS = RepoStatus.FILESTATUS; const RELATION = RepoStatus.Submodule.COMMIT_RELATION; const StatusDescriptor = PrintStatusUtil.StatusDescriptor; @@ -77,7 +81,10 @@ describe("PrintStatusUtil", function () { check: /^deleted/, }, "conflicted": { - des: new StatusDescriptor(FILESTATUS.CONFLICTED, "x", "y"), + des: new StatusDescriptor( + new RepoStatus.Conflict(BLOB, BLOB, BLOB), + "x", + "y"), check: /^conflicted/, }, "renamed": { @@ -706,8 +713,23 @@ nothing to commit, working tree clean exact: `\ On branch ${colors.green("master")}. A merge is in progress. - (fix conflicts and run "git meta merge --continue") - (use "git meta merge --abort" to abort the merge) + (after resolving conflicts mark the corrected paths + with 'git meta add', then run "git meta merge --continue") + (use "git meta merge --abort" to check out the original branch) +nothing to commit, working tree clean +`, + }, + "cherry-pick": { + input: new RepoStatus({ + currentBranchName: "master", + cherryPick: new CherryPick("1", "1"), + }), + exact: `\ +On branch ${colors.green("master")}. +A cherry-pick is in progress. + (after resolving conflicts mark the corrected paths + with 'git meta add', then run "git meta cherry-pick --continue") + (use "git meta cherry-pick --abort" to check out the original branch) nothing to commit, working tree clean `, }, diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index f71b962a6..23653d93f 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -36,6 +36,9 @@ const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); +const CherryPick = require("../../lib/util/cherry_pick"); +const CherryPickFileUtil = require("../../lib/util/cherry_pick_file_util"); +const ConflictUtil = require("../../lib/util/conflict_util"); const DeinitUtil = require("../../lib/util/deinit_util"); const Merge = require("../../lib/util/merge"); const MergeFileUtil = require("../../lib/util/merge_file_util"); @@ -73,6 +76,11 @@ const astFromSimpleRepo = co.wrap(function *(repo) { }); }); +const hashObject = co.wrap(function *(repo, data) { + const db = yield repo.odb(); + const BLOB = 3; + return yield db.write(data, data.length, BLOB); +}); /** * Create a repository with a branch and two commits and a `RepoAST` object @@ -1490,6 +1498,54 @@ describe("readRAST", function () { assert.deepEqual(actualMerge, merge); })); + it("cherry-pick", co.wrap(function *() { + // Start out with a base repo having two branches, "master", and "foo", + // foo having one commit on top of master. + + const start = yield repoWithCommit(); + const r = start.repo; + + // Switch to master + + yield r.checkoutBranch("master"); + + const head = yield r.getHeadCommit(); + const sha = head.id().tostrS(); + + const cherryPick = new CherryPick(sha, sha); + + const original = yield ReadRepoASTUtil.readRAST(r); + const expected = original.copy({ + cherryPick: cherryPick, + }); + + yield CherryPickFileUtil.writeCherryPick(r.path(), cherryPick); + + const actual = yield ReadRepoASTUtil.readRAST(r); + + RepoASTUtil.assertEqualASTs(actual, expected); + })); + + it("cherry-pick - unreachable", co.wrap(function *() { + const r = yield TestUtil.createSimpleRepository(); + r.detachHead(); + const secondCommit = yield TestUtil.generateCommit(r); + const thirdCommit = yield TestUtil.generateCommit(r); + + // Then begin a cherry-pick. + + const cherryPick = new CherryPick(secondCommit.id().tostrS(), + thirdCommit.id().tostrS()); + yield CherryPickFileUtil.writeCherryPick(r.path(), cherryPick); + + // Remove the branches, making the commits reachable only from the + // rebase. + + const ast = yield ReadRepoASTUtil.readRAST(r); + const actualCherryPick = ast.cherryPick; + assert.deepEqual(actualCherryPick, cherryPick); + })); + it("add subs again", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); let expected = yield astFromSimpleRepo(repo); @@ -1675,5 +1731,144 @@ describe("readRAST", function () { NodeGit.Stash.FLAGS.INCLUDE_UNTRACKED); yield ReadRepoASTUtil.readRAST(repo); })); + describe("conflicts", function () { + const FILEMODE = NodeGit.TreeEntry.FILEMODE; + const ConflictEntry = ConflictUtil.ConflictEntry; + it("three versions", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const makeEntry = co.wrap(function *(data) { + const id = yield hashObject(repo, data); + return new ConflictEntry(FILEMODE.BLOB, id.tostrS()); + }); + const ancestor = yield makeEntry("xxx"); + const our = yield makeEntry("yyy"); + const their = yield makeEntry("zzz"); + const index = yield repo.index(); + const filename = "README.md"; + const conflict = new ConflictUtil.Conflict(ancestor, our, their); + yield ConflictUtil.addConflict(index, filename, conflict); + yield index.write(); + yield fs.writeFile(path.join(repo.workdir(), filename), + "conflicted"); + const result = yield ReadRepoASTUtil.readRAST(repo); + const simple = yield astFromSimpleRepo(repo); + const expected = simple.copy({ + index: { + "README.md": new RepoAST.Conflict("xxx", "yyy", "zzz"), + }, + workdir: { + "README.md": "conflicted", + }, + }); + RepoASTUtil.assertEqualASTs(result, expected); + })); + it("with a deletion", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const makeEntry = co.wrap(function *(data) { + const id = yield hashObject(repo, data); + return new ConflictEntry(FILEMODE.BLOB, id.tostrS()); + }); + const ancestor = yield makeEntry("xxx"); + const their = yield makeEntry("zzz"); + const conflict = new ConflictUtil.Conflict(ancestor, null, their); + const index = yield repo.index(); + const filename = "README.md"; + yield ConflictUtil.addConflict(index, filename, conflict); + yield index.write(); + yield fs.writeFile(path.join(repo.workdir(), filename), + "conflicted"); + const result = yield ReadRepoASTUtil.readRAST(repo); + const simple = yield astFromSimpleRepo(repo); + const expected = simple.copy({ + index: { + "README.md": new RepoAST.Conflict("xxx", null, "zzz"), + }, + workdir: { + "README.md": "conflicted", + }, + }); + RepoASTUtil.assertEqualASTs(result, expected); + })); + it("with submodule", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const head = yield repo.getHeadCommit(); + const sha = head.id().tostrS(); + const entry = new ConflictEntry(FILEMODE.COMMIT, sha); + const index = yield repo.index(); + const conflict = new ConflictUtil.Conflict(null, entry, null); + yield ConflictUtil.addConflict(index, "s", conflict); + yield index.write(); + const result = yield ReadRepoASTUtil.readRAST(repo); + const simple = yield astFromSimpleRepo(repo); + const expected = simple.copy({ + index: { + "s": new RepoAST.Conflict(null, + new RepoAST.Submodule("", sha), + null), + }, + }); + RepoASTUtil.assertEqualASTs(result, expected); + })); + it("with submodule and open", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const head = yield repo.getHeadCommit(); + const sha = head.id().tostrS(); + const baseSub = yield TestUtil.createSimpleRepository(); + const subHead = yield baseSub.getHeadCommit(); + const baseHead = yield baseSub.getHeadCommit(); + const baseSha = baseHead.id().tostrS(); + const subAST = yield astFromSimpleRepo(baseSub); + yield addSubmodule(repo, baseSub.workdir(), "foo", baseSha); + const commit = yield TestUtil.makeCommit(repo, + ["foo", ".gitmodules"]); + + const entry = new ConflictEntry(FILEMODE.COMMIT, sha); + const index = yield repo.index(); + const conflict = new ConflictUtil.Conflict(null, entry, null); + yield ConflictUtil.addConflict(index, "foo", conflict); + yield index.write(); + let commits = {}; + commits[sha] = new Commit({ + changes: {"README.md":""}, + message: "first commit", + }); + commits[commit.id().tostrS()] = new Commit({ + parents: [sha], + changes: { + "foo": new RepoAST.Submodule(baseSub.workdir(), + subHead.id().tostrS()), + }, + message: "message\n", + }); + const expected = new RepoAST({ + commits: commits, + branches: { + master: new RepoAST.Branch(commit.id().tostrS(), null), + }, + currentBranchName: "master", + head: commit.id().tostrS(), + index: { + "foo": new RepoAST.Conflict(null, + new RepoAST.Submodule("", sha), + null), + }, + openSubmodules: { + "foo": subAST.copy({ + branches: {}, + currentBranchName: null, + remotes: { + origin: new RepoAST.Remote(baseSub.workdir(), { + branches: { + master: baseSha, + } + }), + }, + }), + }, + }); + const actual = yield ReadRepoASTUtil.readRAST(repo); + RepoASTUtil.assertEqualASTs(actual, expected); + })); + }); }); diff --git a/node/test/util/rebase_util.js b/node/test/util/rebase_util.js index b0668030f..c99dbf089 100644 --- a/node/test/util/rebase_util.js +++ b/node/test/util/rebase_util.js @@ -30,11 +30,14 @@ */ "use strict"; -const assert = require("chai").assert; -const co = require("co"); +const assert = require("chai").assert; +const co = require("co"); +const colors = require("colors"); +const NodeGit = require("nodegit"); const RebaseUtil = require("../../lib/util/rebase_util"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const StatusUtil = require("../../lib/util/status_util"); const SubmoduleUtil = require("../../lib/util/submodule_util"); function makeRebaser(operation) { @@ -44,24 +47,14 @@ function makeRebaser(operation) { // Now build a map from the newly generated commits to the // logical names that will be used in the expected case. - let commitMap = {}; - function addNewCommit(newCommit, oldCommit, suffix) { - const oldLogicalCommit = maps.commitMap[oldCommit]; - commitMap[newCommit] = oldLogicalCommit + suffix; - } - Object.keys(result.metaCommits).forEach(newCommit => { - addNewCommit(newCommit, - result.metaCommits[newCommit], - "M"); - }); - Object.keys(result.submoduleCommits).forEach(subName => { - const subCommits = result.submoduleCommits[subName]; - Object.keys(subCommits).forEach(newCommit => { - addNewCommit(newCommit, - subCommits[newCommit], - subName); - }); - }); + const commitMap = {}; + RepoASTTestUtil.mapCommits(commitMap, + result.metaCommits, + maps.commitMap, + "M"); + RepoASTTestUtil.mapSubCommits(commitMap, + result.submoduleCommits, + maps.commitMap); return { commitMap: commitMap, }; @@ -69,6 +62,202 @@ function makeRebaser(operation) { } describe("rebase", function () { + it("callNext", co.wrap(function *() { + const init = "S:C2-1;Bmaster=2;C3-1;Bfoo=3"; + const written = yield RepoASTTestUtil.createRepo(init); + const repo = written.repo; + const ontoSha = written.oldCommitMap["3"]; + const fromId = NodeGit.Oid.fromString(ontoSha); + const fromAnnotated = + yield NodeGit.AnnotatedCommit.lookup(repo, fromId); + const head = yield repo.head(); + const ontoAnnotated = yield NodeGit.AnnotatedCommit.fromRef(repo, + head); + const rebase = yield NodeGit.Rebase.init(repo, + fromAnnotated, + ontoAnnotated, + null, + null); + const first = yield RebaseUtil.callNext(rebase); + assert.equal(first.id().tostrS(), ontoSha); + const second = yield RebaseUtil.callNext(rebase); + assert.isNull(second); + })); + + describe("processRebase", function () { + const cases = { + "no conflicts": { + initial: "x=S:C2-1;Cr-1;Bmaster=2;Br=r", + expected: "x=E:Crr-2 r=r;H=rr", + conflictedCommit: null, + }, + "conflict": { + initial: "x=S:C2-1;Cr-1 2=3;Bmaster=2;Br=r", + expected: "x=E:I *2=~*2*3;W 2=u;H=2;Edetached HEAD,r,2", + conflictedCommit: "r", + expectedTransformer: function (expected, mapping) { + const content = `\ +<<<<<<< ${mapping.reverseCommitMap["2"]} +2 +======= +3 +>>>>>>> message +`; + expected.x = expected.x.copy({ + workdir: { + "2": content, + }, + }); + return expected; + }, + }, + "fast forward": { + initial: "x=S:C2-r;Cr-1;Bmaster=2;Br=r", + expected: "x=E:H=2", + conflictedCommit: null, + }, + }; + Object.keys(cases).forEach(caseName => { + it(caseName, co.wrap(function *() { + const c = cases[caseName]; + const op = co.wrap(function *(repos, maps) { + const repo = repos.x; + const headCommit = yield repo.getHeadCommit(); + const AnnotatedCommit = NodeGit.AnnotatedCommit; + const headAnnotated = yield AnnotatedCommit.lookup( + repo, + headCommit.id()); + const targetCommitSha = maps.reverseCommitMap.r; + const targetCommit = yield repo.getCommit(targetCommitSha); + const targetAnnotated = yield AnnotatedCommit.lookup( + repo, + targetCommit.id()); + const rebase = yield NodeGit.Rebase.init(repo, + targetAnnotated, + headAnnotated, + null, + null); + const op = yield RebaseUtil.callNext(rebase); + const result = yield RebaseUtil.processRebase(repo, + rebase, + op); + if (null === c.conflictedCommit) { + assert.isNull(result.conflictedCommit); + } else { + assert.equal( + result.conflictedCommit, + maps.reverseCommitMap[c.conflictedCommit]); + } + const commitMap = {}; + Object.keys(result.commits).forEach(newSha => { + const oldSha = result.commits[newSha]; + const oldLogicalCommit = maps.commitMap[oldSha]; + commitMap[newSha] = oldLogicalCommit + "r"; + }); + return { + commitMap: commitMap, + }; + }); + const options = {}; + if (undefined !== c.expectedTransformer) { + options.expectedTransformer = c.expectedTransformer; + } + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + c.expected, + op, + c.fails, + options); + })); + }); + }); + + describe("rewriteCommits", function () { + const cases = { + "normal rebase": { + initial: "x=S:C2-1;Cr-1;Bmaster=2;Br=r", + expected: "x=E:Crr-2 r=r;H=rr", + upstream: null, + conflictedCommit: null, + }, + "skip none": { + initial: "x=S:C2-1;Cr-1;Bmaster=2;Br=r", + expected: "x=E:Crr-2 r=r;H=rr", + upstream: "1", + conflictedCommit: null, + }, + "conflict": { + initial: "x=S:C2-1;Cr-1 2=3;Bmaster=2;Br=r", + expected: `x=E:I *2=~*2*3;H=2;Edetached HEAD,r,2;W 2=\ +<<<<<<< master +2 +======= +3 +>>>>>>> message +; +`, + upstream: null, + conflictedCommit: "r", + }, + "multiple commits": { + initial: "x=S:C2-1;C3-1;Cr-3;Bmaster=2;Br=r", + expected: "x=E:Crr-3r r=r;C3r-2 3=3;H=rr", + upstream: null, + conflictedCommit: null, + }, + "skip a commit": { + initial: "x=S:C2-1;C3-1;Cr-3;Bmaster=2;Br=r", + expected: "x=E:Crr-2 r=r;H=rr", + upstream: "3", + conflictedCommit: null, + }, + }; + Object.keys(cases).forEach(caseName => { + it(caseName, co.wrap(function *() { + const c = cases[caseName]; + const op = co.wrap(function *(repos, maps) { + const repo = repos.x; + const targetSha = maps.reverseCommitMap.r; + const targetCommit = yield repo.getCommit(targetSha); + let upstreamCommit = null; + if (null !== c.upstream) { + const upstreamSha = maps.reverseCommitMap[c.upstream]; + upstreamCommit = yield repo.getCommit(upstreamSha); + } + const result = yield RebaseUtil.rewriteCommits( + repo, + targetCommit, + upstreamCommit); + if (null === c.conflictedCommit) { + assert.isNull(result.conflictedCommit); + } else { + assert.equal( + result.conflictedCommit, + maps.reverseCommitMap[c.conflictedCommit]); + } + const commitMap = {}; + Object.keys(result.commits).forEach(newSha => { + const oldSha = result.commits[newSha]; + const oldLogicalCommit = maps.commitMap[oldSha]; + commitMap[newSha] = oldLogicalCommit + "r"; + }); + return { + commitMap: commitMap, + }; + }); + const options = {}; + if (undefined !== c.expectedTransformer) { + options.expectedTransformer = c.expectedTransformer; + } + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + c.expected, + op, + c.fails, + options); + })); + }); + }); + + describe("rebase", function () { // Will append the leter 'M' to any created meta-repo commits, and the @@ -85,7 +274,7 @@ describe("rebase", function () { const originalCommit = yield repo.getCommit(originalActualCommit); - return yield RebaseUtil.rebase(repo, originalCommit); + return yield RebaseUtil.rebase(repo, originalCommit); }); return makeRebaser(rebaseOper); @@ -346,6 +535,110 @@ x=U:C3-2 s=Sa:q;C4-2 s=Sa:r; }); }); + describe("continueSubmodules", function () { + const cases = { + "nothing to do": { + initial: `x=S`, + }, + "a closed sub": { + initial: "x=S:C2-1 s=Sa:1;Bmaster=2", + }, + "change in sub is staged": { + initial: "a=B:Ca-1;Ba=a|x=U:Os H=a", + expected: "x=E:I s=Sa:a", + }, + "rebase in a sub": { + initial: ` +a=B:Cq-1;Cr-1;Bq=q;Br=r| +x=U:C3-2 s=Sa:q;Bmaster=3;Os EHEAD,q,r!I q=q`, + expected: ` +x=E:I s=Sa:qs;Os Cqs-r q=q!H=qs!E` + }, + "rebase in a sub, was conflicted": { + initial: ` +a=B:Cq-1;Cr-1;Bq=q;Br=r| +x=U:C3-2 s=Sa:q;Bmaster=3;I *s=S:1*S:r*S:q;Os EHEAD,q,r!I q=q`, + expected: ` +x=E:I s=Sa:qs;Os Cqs-r q=q!H=qs!E` + }, + "rebase two in a sub": { + initial: ` +a=B:Cp-q;Cq-1;Cr-1;Bp=p;Br=r| +x=U:C3-2 s=Sa:q;Bmaster=3;Os EHEAD,p,r!I q=q!Bp=p`, + expected: ` +x=E:I s=Sa:ps;Os Cps-qs p=p!Cqs-r q=q!H=ps!E!Bp=p` + }, + "rebase in two subs": { + initial: ` +a=B:Cp-q;Cq-1;Cr-1;Cz-1;Bp=p;Br=r;Bz=z| +x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q,t=Sa:q;Bmaster=3; + Os EHEAD,p,r!I q=q!Bp=p; + Ot EHEAD,z,r!I z=8!Bz=z; +`, + expected: ` +x=E:I s=Sa:ps,t=Sa:zt; + Os Cps-qs p=p!Cqs-r q=q!H=ps!E!Bp=p; + Ot Czt-r z=8!H=zt!E!Bz=z; +`, + }, + "rebase in two subs, conflict in one": { + initial: ` +a=B:Cp-q r=8;Cq-1;Cr-1;Cz-1;Bp=p;Br=r;Bz=z| +x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q,t=Sa:q;Bmaster=3; + Os EHEAD,p,r!I q=q!Bp=p; + Ot EHEAD,z,r!I z=8!Bz=z; +`, + expected: ` +x=E:I t=Sa:zt; + Os Cqs-r q=q!H=qs!EHEAD,p,r!Bp=p!I *r=~*r*8!W r=^<<<<; + Ot Czt-r z=8!H=zt!E!Bz=z; +`, + errorMessage: `\ +Conflict in ${colors.red("s")} +`, + }, + "made a commit in a sub without a rebase": { + initial: `a=B|x=U:Cfoo#9-1;B9=9;Os I a=b`, + expected: `x=E:I s=Sa:Ns;Os Cfoo#Ns-1 a=b!H=Ns`, + baseCommit: "9", + message: "foo", + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const continuer = co.wrap(function *(repos, maps) { + const repo = repos.x; + const index = yield repo.index(); + const status = yield StatusUtil.getRepoStatus(repo); + const baseSha = c.baseCommit || "1"; + const baseCommit = + yield repo.getCommit(maps.reverseCommitMap[baseSha]); + const result = yield RebaseUtil.continueSubmodules( + repo, + index, + status, + baseCommit); + assert.equal(result.errorMessage, c.errorMessage || null); + const commitMap = {}; + RepoASTTestUtil.mapSubCommits(commitMap, + result.commits, + maps.commitMap); + Object.keys(result.newCommits).forEach(name => { + commitMap[result.newCommits[name]] = "N" + name; + }); + return { + commitMap: commitMap, + }; + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + c.expected, + continuer, + c.fails); + })); + }); + }); + describe("continue", function () { const cases = { "meta-only": { diff --git a/node/test/util/repo_ast.js b/node/test/util/repo_ast.js index cf71f28f6..f23568d1c 100644 --- a/node/test/util/repo_ast.js +++ b/node/test/util/repo_ast.js @@ -61,6 +61,83 @@ describe("RepoAST", function () { const s = new RepoAST.Submodule("foo", null); assert.isNull(s.sha); }); + it("equal", function () { + const Submodule = RepoAST.Submodule; + const cases = { + "same": { + lhs: new Submodule("foo", "bar"), + rhs: new Submodule("foo", "bar"), + expected: true, + }, + "diff url": { + lhs: new Submodule("boo", "bar"), + rhs: new Submodule("foo", "bar"), + expected: false, + }, + "diff sha": { + lhs: new Submodule("foo", "bar"), + rhs: new Submodule("foo", "baz"), + expected: false, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + assert.equal(c.lhs.equal(c.rhs), c.expected); + }); + }); + }); + + describe("Conflict", function () { + it("breath", function () { + const c = new RepoAST.Conflict("foo", "bar", "baz"); + assert.equal(c.ancestor, "foo"); + assert.equal(c.our, "bar"); + assert.equal(c.their, "baz"); + }); + it("nulls", function () { + const c = new RepoAST.Conflict(null, null, null); + assert.equal(c.ancestor, null); + assert.equal(c.our, null); + assert.equal(c.their, null); + }); + it("subs", function () { + const s0 = new RepoAST.Submodule("foo", null); + const s1 = new RepoAST.Submodule("bar", null); + const s2 = new RepoAST.Submodule("baz", null); + const c = new RepoAST.Conflict(s0, s1, s2); + assert.deepEqual(c.ancestor, s0); + assert.deepEqual(c.our, s1); + assert.deepEqual(c.their, s2); + }); + it("equal", function () { + const Conflict = RepoAST.Conflict; + const cases = { + "same": { + lhs: new Conflict("foo", "bar", "baz"), + rhs: new Conflict("foo", "bar", "baz"), + expected: true, + }, + "diff ancestor": { + lhs: new Conflict("foo", "bar", "baz"), + rhs: new Conflict("food", "bar", "baz"), + expected: false, + }, + "diff ours": { + lhs: new Conflict("foo", "bar", "baz"), + rhs: new Conflict("foo", "bark", "baz"), + expected: false, + }, + "diff theirs": { + lhs: new Conflict("foo", "bar", "baz"), + rhs: new Conflict("foo", "bar", "bam"), + expected: false, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + assert.equal(c.lhs.equal(c.rhs), c.expected); + }); + }); }); describe("Commit", function () { @@ -166,6 +243,7 @@ describe("RepoAST", function () { const Commit = RepoAST.Commit; const Rebase = RepoAST.Rebase; const Merge = RepoAST.Merge; + const CherryPick = RepoAST.CherryPick; const Remote = RepoAST.Remote; const c1 = new Commit(); @@ -195,6 +273,8 @@ describe("RepoAST", function () { expected.openSubmodules : {}, erebase: ("rebase" in expected) ? expected.rebase : null, emerge: ("merge" in expected) ? expected.merge : null, + echerryPick: ("cherryPick" in expected) ? + expected.cherryPick : null, fails : fails, }; } @@ -209,6 +289,7 @@ describe("RepoAST", function () { currentBranchName: null, rebase: null, merge: null, + cherryPick: null, bare: false, }, undefined, @@ -238,6 +319,12 @@ describe("RepoAST", function () { commits: {"1": c1 }, head: "1", }, undefined, true), + "bad bare with cherry": m({ + bare: true, + cherryPick: new CherryPick("1", "1"), + commits: {"1": c1 }, + head: "1", + }, undefined, true), "branchCommit": m({ commits: {"1":c1, "2": cWithPar}, branches: {"master": new RepoAST.Branch("2", null) }, @@ -381,6 +468,17 @@ describe("RepoAST", function () { head: "1", index: { foo: new RepoAST.Submodule("z", "a") }, }, false), + "index with conflict": m({ + commits: { "1": c1}, + head: "1", + index: { foo: new RepoAST.Conflict("foo", "bar", "baz") }, + workdir: { foo: "bar" }, + }, { + commits: { "1": c1}, + head: "1", + index: { foo: new RepoAST.Conflict("foo", "bar", "baz") }, + workdir: { foo: "bar" }, + }, false), "workdir": m({ commits: { "1": c1}, head: "1", @@ -496,18 +594,18 @@ describe("RepoAST", function () { "bad rebase": m({ rebase: new Rebase("fff", "1", "1"), }, undefined, true), - "with merge": m({ + "with cherry": m({ commits: { "1": new Commit(), }, head: "1", - merge: new Merge("fff", "1", "1"), + cherryPick: new CherryPick( "1", "1"), }, { commits: { "1": new Commit(), }, head: "1", - merge: new Merge("fff", "1", "1"), + cherryPick: new CherryPick("1", "1"), }), "with merge specific commits": m({ commits: { @@ -524,8 +622,23 @@ describe("RepoAST", function () { head: "1", merge: new Merge("fff", "2", "2"), }), - "bad merge": m({ - merge: new Merge("fff", "1", "1"), + "with cherry-pick specific commits": m({ + commits: { + "1": new Commit(), + "2": new Commit(), + }, + head: "1", + cherryPick: new CherryPick("2", "2"), + }, { + commits: { + "1": new Commit(), + "2": new Commit(), + }, + head: "1", + cherryPick: new CherryPick("2", "2"), + }), + "bad cherry-pick": m({ + cherryPick: new CherryPick("1", "1"), }, undefined, true), }; Object.keys(cases).forEach(caseName => { @@ -550,6 +663,7 @@ describe("RepoAST", function () { assert.deepEqual(obj.openSubmodules, c.eopenSubmodules); assert.deepEqual(obj.rebase, c.erebase); assert.deepEqual(obj.merge, c.emerge); + assert.deepEqual(obj.cherryPick, c.echerryPick); assert.equal(obj.bare, c.ebare); if (c.input) { @@ -689,6 +803,7 @@ describe("RepoAST", function () { describe("AST.copy", function () { const Rebase = RepoAST.Rebase; const Merge = RepoAST.Merge; + const CherryPick = RepoAST.CherryPick; const base = new RepoAST({ commits: { "1": new RepoAST.Commit()}, branches: { "master": new RepoAST.Branch("1", null) }, @@ -699,6 +814,7 @@ describe("RepoAST", function () { workdir: { foo: "bar" }, rebase: new Rebase("hello", "1", "1"), merge: new Merge("hello", "1", "1"), + cherryPick: new CherryPick("1", "1"), bare: false, }); const newArgs = { @@ -712,6 +828,7 @@ describe("RepoAST", function () { workdir: { foo: "bar" }, rebase: new Rebase("hello world", "2", "2"), merge: new Merge("hello world", "2", "2"), + cherryPick: new CherryPick("2", "2"), bare: false, }; const cases = { @@ -730,6 +847,7 @@ describe("RepoAST", function () { workdir: {}, rebase: null, merge: null, + cherryPick: null, }, e: new RepoAST({ commits: { "1": new RepoAST.Commit()}, @@ -756,6 +874,7 @@ describe("RepoAST", function () { assert.deepEqual(obj.openSubmodules, c.e.openSubmodules); assert.deepEqual(obj.rebase, c.e.rebase); assert.deepEqual(obj.merge, c.e.merge); + assert.deepEqual(obj.cherryPick, c.e.cherryPick); assert.equal(obj.bare, c.e.bare); }); }); @@ -768,6 +887,7 @@ describe("RepoAST", function () { // together properly. const Commit = RepoAST.Commit; + const Conflict = RepoAST.Conflict; const c1 = new Commit({ changes: { foo: "bar" }}); const cases = { "no index": { @@ -781,6 +901,12 @@ describe("RepoAST", function () { index: { y: "z" }, expected: { foo: "bar", y: "z" }, }, + "ignore conflict": { + commits: { "1": c1}, + from: "1", + index: { y: "z", foo: new Conflict("foo", "bar", "bb") }, + expected: { foo: "bar", y: "z" }, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; diff --git a/node/test/util/repo_ast_test_util.js b/node/test/util/repo_ast_test_util.js index 2ec98276d..65413235f 100644 --- a/node/test/util/repo_ast_test_util.js +++ b/node/test/util/repo_ast_test_util.js @@ -353,4 +353,55 @@ Bmaster=1 origin/master`, })); }); }); + describe("mapCommits", function () { + const cases = { + "nothing to do": { + commits: {}, + commitMap: {}, + suffix: "foo", + expected: {}, + }, + "map one": { + commits: { "2": "1" }, + commitMap: { "1": "foo" }, + suffix: "bar", + expected: { "2": "foobar" }, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, function () { + const result = {}; + RepoASTTestUtil.mapCommits(result, + c.commits, + c.commitMap, + c.suffix); + assert.deepEqual(result, c.expected); + }); + }); + }); + describe("mapSubCommits", function () { + const cases = { + "nothing to do": { + subCommits: {}, + commitMap: {}, + expected: {}, + }, + "map one": { + subCommits: { "x": { "2": "1" } }, + commitMap: { "1": "fo" }, + expected: { "2": "fox" }, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, function () { + const result = {}; + RepoASTTestUtil.mapSubCommits(result, + c.subCommits, + c.commitMap); + assert.deepEqual(result, c.expected); + }); + }); + }); }); diff --git a/node/test/util/repo_ast_util.js b/node/test/util/repo_ast_util.js index be31a743b..de0cd1d29 100644 --- a/node/test/util/repo_ast_util.js +++ b/node/test/util/repo_ast_util.js @@ -36,7 +36,8 @@ const RepoAST = require("../../lib/util/repo_ast"); const RepoASTUtil = require("../../lib/util/repo_ast_util"); describe("RepoAstUtil", function () { - + const Conflict = RepoAST.Conflict; + const Submodule = RepoAST.Submodule; describe("assertEqualCommits", function () { const Commit = RepoAST.Commit; const cases = { @@ -131,9 +132,11 @@ describe("RepoAstUtil", function () { describe("assertEqualASTs", function () { const AST = RepoAST; + const Conflict = RepoAST.Conflict; const Commit = AST.Commit; const Rebase = AST.Rebase; const Merge = AST.Merge; + const CherryPick = AST.CherryPick; const Remote = AST.Remote; const Submodule = AST.Submodule; @@ -167,6 +170,7 @@ describe("RepoAstUtil", function () { openSubmodules: { y: anAST }, rebase: new Rebase("foo", "1", "1"), merge: new Merge("foo", "1", "1"), + cherryPick: new CherryPick("1", "1"), bare: false, }), expected: new AST({ @@ -181,6 +185,7 @@ describe("RepoAstUtil", function () { openSubmodules: { y: anAST }, rebase: new Rebase("foo", "1", "1"), merge: new Merge("foo", "1", "1"), + cherryPick: new CherryPick("1", "1"), bare: false, }), }, @@ -362,6 +367,33 @@ describe("RepoAstUtil", function () { }), fails: true, }, + "wrong file data": { + actual: new AST({ + index: { foo: "bar", }, + }), + expected: new AST({ + index: { foo: "baz", }, + }), + fails: true, + }, + "regex match data miss": { + actual: new AST({ + index: { foo: "bar", }, + }), + expected: new AST({ + index: { foo: "^^ar", }, + }), + fails: true, + }, + "regex match data hit": { + actual: new AST({ + index: { foo: "bar", }, + }), + expected: new AST({ + index: { foo: "^^ba", }, + }), + fails: false, + }, "bad workdir": { actual: new AST({ commits: { "1": aCommit}, @@ -567,6 +599,82 @@ describe("RepoAstUtil", function () { }), fails: true, }, + "missing cherry": { + actual: new AST({ + commits: { "1": aCommit}, + head: "1", + cherryPick: new CherryPick("1", "1"), + }), + expected: new AST({ + commits: { "1": aCommit}, + head: "1", + }), + fails: true, + }, + "unexpected cherry": { + actual: new AST({ + commits: { "1": aCommit}, + head: "1", + }), + expected: new AST({ + commits: { "1": aCommit}, + head: "1", + cherryPick: new CherryPick("1", "1"), + }), + fails: true, + }, + "wrong cherry": { + actual: new AST({ + commits: { "1": aCommit, "2": aCommit}, + head: "1", + branches: { master: new RepoAST.Branch("2", null), }, + cherryPick: new CherryPick("2", "1"), + }), + expected: new AST({ + commits: { "1": aCommit, "2": aCommit}, + head: "1", + branches: { master: new RepoAST.Branch("2", null), }, + cherryPick: new CherryPick("1", "1"), + }), + fails: true, + }, + "same Conflict": { + actual: new AST({ + index: { + "foo": new Conflict("foo", "bar", "baz"), + }, + workdir: { + "foo": "boo", + }, + }), + expected: new AST({ + index: { + "foo": new Conflict("foo", "bar", "baz"), + }, + workdir: { + "foo": "boo", + }, + }), + }, + "diff Conflict": { + actual: new AST({ + index: { + "foo": new Conflict("foo", "bar", "baz"), + }, + workdir: { + "foo": "boo", + }, + }), + expected: new AST({ + index: { + "foo": new Conflict("foo", "bar", "bam"), + }, + workdir: { + "foo": "boo", + }, + }), + fails: true, + }, }; Object.keys(cases).forEach((caseName) => { const c = cases[caseName]; @@ -587,6 +695,7 @@ describe("RepoAstUtil", function () { const Commit = RepoAST.Commit; const Rebase = RepoAST.Rebase; const Merge = RepoAST.Merge; + const CherryPick = RepoAST.CherryPick; const c1 = new Commit({ message: "foo" }); const cases = { "trivial": { i: new RepoAST(), m: {}, e: new RepoAST() }, @@ -870,6 +979,28 @@ describe("RepoAstUtil", function () { }, }), }, + "index conflicted submodule": { + i: new RepoAST({ + commits: { "1": c1 }, + head: "1", + index: { + baz: new Conflict(new Submodule("q", "1"), + new Submodule("r", "1"), + new Submodule("s", "1")), + }, + }), + m: { "1": "2"}, + u: { "q": "z", "r": "a", "s": "b" }, + e: new RepoAST({ + commits: { "2": c1 }, + head: "2", + index: { + baz: new Conflict(new Submodule("z", "2"), + new Submodule("a", "2"), + new Submodule("b", "2")), + }, + }), + }, "workdir, unchanged": { i: new RepoAST({ commits: { "1": c1 }, @@ -969,6 +1100,32 @@ describe("RepoAstUtil", function () { merge: new Merge("foo", "1", "1"), }), }, + "cherry-pick": { + i: new RepoAST({ + commits: { "1": c1 }, + head: "1", + cherryPick: new CherryPick("1", "1"), + }), + m: { "1": "2"}, + e: new RepoAST({ + commits: { "2": c1 }, + head: "2", + cherryPick: new CherryPick("2", "2"), + }), + }, + "cherry-pick unmapped": { + i: new RepoAST({ + commits: { "1": c1 }, + head: "1", + cherryPick: new CherryPick("1", "1"), + }), + m: {}, + e: new RepoAST({ + commits: { "1": c1 }, + head: "1", + cherryPick: new CherryPick("1", "1"), + }), + }, }; Object.keys(cases).forEach(caseName => { it(caseName, function () { diff --git a/node/test/util/repo_status.js b/node/test/util/repo_status.js index 012900248..afdef90a9 100644 --- a/node/test/util/repo_status.js +++ b/node/test/util/repo_status.js @@ -33,6 +33,7 @@ const assert = require("chai").assert; const Merge = require("../../lib/util/merge"); +const CherryPick = require("../../lib/util/cherry_pick"); const Rebase = require("../../lib/util/rebase"); const RepoStatus = require("../../lib/util/repo_status"); @@ -477,6 +478,7 @@ describe("RepoStatus", function () { submodules: {}, rebase: null, merge: null, + cherryPick: null, }; return Object.assign(result, args); } @@ -503,6 +505,7 @@ describe("RepoStatus", function () { }, rebase: new Rebase("foo", "1", "2"), merge: new Merge("baz", "2", "1"), + cherryPick: new CherryPick("2", "1"), }, e: m({ currentBranchName: "foo", @@ -517,6 +520,7 @@ describe("RepoStatus", function () { }, rebase: new Rebase("foo", "1", "2"), merge: new Merge("baz", "2", "1"), + cherryPick: new CherryPick("2", "1"), }), } }; @@ -533,6 +537,7 @@ describe("RepoStatus", function () { assert.deepEqual(result.submodules, c.e.submodules); assert.deepEqual(result.rebase, c.e.rebase); assert.deepEqual(result.merge, c.e.merge); + assert.deepEqual(result.cherryPick, c.e.cherryPick); }); }); @@ -858,6 +863,79 @@ describe("RepoStatus", function () { }); }); + describe("isConflicted", function () { + const cases = { + "trivial": { + input: new RepoStatus(), + expected: false, + }, + "with files and submodules": { + input: new RepoStatus({ + currentBranchName: "foo", + headCommit: "1", + staged: { bar: FILESTATUS.MODIFIED }, + workdir: { foo: FILESTATUS.ADDED }, + submodules: { + "a": new Submodule({ + commit: new Commit("1", "a"), + staged: new Index("1", "a", RELATION.SAME), + workdir: new Workdir(new RepoStatus({ + headCommit: "1", + }), RELATION.SAME), + }), + }, + }), + expected: false, + }, + "conflict in meta": { + input: new RepoStatus({ + currentBranchName: "foo", + headCommit: "1", + staged: { bar: new RepoStatus.Conflict(null, null, null) }, + workdir: { foo: FILESTATUS.ADDED }, + submodules: { + "a": new Submodule({ + commit: new Commit("1", "a"), + staged: new Index("1", "a", RELATION.SAME), + workdir: new Workdir(new RepoStatus({ + headCommit: "1", + }), RELATION.SAME), + }), + }, + }), + expected: true, + }, + "conflict in sub": { + input: new RepoStatus({ + currentBranchName: "foo", + headCommit: "1", + staged: { bar: FILESTATUS.MODIFIED }, + workdir: { foo: FILESTATUS.ADDED }, + submodules: { + "a": new Submodule({ + commit: new Commit("1", "a"), + staged: new Index("1", "a", RELATION.SAME), + workdir: new Workdir(new RepoStatus({ + staged: { + meh: new RepoStatus.Conflict(1, 1, 1), + }, + headCommit: "1", + }), RELATION.SAME), + }), + }, + }), + expected: true, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, function () { + const result = c.input.isConflicted(); + assert.equal(result, c.expected); + }); + }); + }); + describe("isDeepClean", function () { const cases = { "trivial": { @@ -963,6 +1041,7 @@ describe("RepoStatus", function () { workdir: { x: FILESTATUS.MODIFIED }, rebase: new Rebase("2", "4", "b"), merge: new Merge("hah", "1", "1"), + cherryPick: new CherryPick("1", "1"), }); const anotherStat = new RepoStatus({ currentBranchName: "fo", @@ -974,6 +1053,7 @@ describe("RepoStatus", function () { workdir: { x: FILESTATUS.ADDED }, rebase: new Rebase("a", "4", "b"), merge: new Merge("a", "2", "2"), + cherryPick: new CherryPick("a", "2"), }); it("simple, no args", function () { const newStat = stat.copy(); @@ -992,6 +1072,7 @@ describe("RepoStatus", function () { workdir: anotherStat.workdir, rebase: anotherStat.rebase, merge: anotherStat.merge, + cherryPick: anotherStat.cherryPick, }); assert.deepEqual(newStat, anotherStat); }); diff --git a/node/test/util/shorthand_parser_util.js b/node/test/util/shorthand_parser_util.js index e74275338..35d2e09b9 100644 --- a/node/test/util/shorthand_parser_util.js +++ b/node/test/util/shorthand_parser_util.js @@ -110,6 +110,7 @@ describe("ShorthandParserUtil", function () { }); describe("parseRepoShorthandRaw", function () { const Commit = RepoAST.Commit; + const Conflict = RepoAST.Conflict; const Submodule = RepoAST.Submodule; function m(args) { let result = { @@ -425,6 +426,26 @@ describe("ShorthandParserUtil", function () { index: { x: new Submodule("/x", "1") }, }), }, + "index with conflict": { + i: "S:I *a=x*y*S/x:2,b=q", + e: m({ + type: "S", + index: { + a: new Conflict("x", "y", new Submodule("/x", "2")), + b: "q", + } + }), + }, + "index with conflict and nulls": { + i: "S:I *a=*~*,b=q", + e: m({ + type: "S", + index: { + a: new Conflict("", null, ""), + b: "q", + } + }), + }, "workdir change": { i: "S:W x=y", e: m({ @@ -613,6 +634,20 @@ describe("ShorthandParserUtil", function () { merge: null, }), }, + "cherry-pick": { + i: "S:P1,2", + e: m({ + type: "S", + cherryPick: new RepoAST.CherryPick("1", "2"), + }), + }, + "cherry-pick null": { + i: "S:P", + e: m({ + type: "S", + cherryPick: null, + }), + }, "new submodule": { i: "S:I x=Sfoo:;Ox", e: m({ @@ -650,6 +685,7 @@ describe("ShorthandParserUtil", function () { assert.deepEqual(r.openSubmodules, e.openSubmodules); assert.deepEqual(r.rebase, e.rebase); assert.deepEqual(r.merge, e.merge); + assert.deepEqual(r.cherryPick, e.cherryPick); }); }); }); @@ -827,6 +863,12 @@ describe("ShorthandParserUtil", function () { merge: new RepoAST.Merge("foo", "1", "1"), }), }, + "cherry-pick": { + i: "S:P1,1", + e: S.copy({ + cherryPick: new RepoAST.CherryPick("1", "1"), + }), + }, }; Object.keys(cases).forEach(caseName => { it(caseName, function () { @@ -1349,6 +1391,59 @@ x=S:Mfoo,8,9`, }), } }, + "missing commits in cherry-pick": { + i: ` +a=B:C8-1;C9-1;Bmaster=8;Bfoo=9| +x=S:P8,9`, + e: { + a: B.copy({ + commits: { + "1": new Commit({ + changes: { + "README.md": "hello world" + }, + message: "the first commit", + }), + "8": new Commit({ + parents: ["1"], + changes: { "8": "8" }, + message: "message\n", + }), + "9": new Commit({ + parents: ["1"], + changes: { "9": "9" }, + message: "message\n", + }), + }, + branches: { + master: new RepoAST.Branch("8", null), + foo: new RepoAST.Branch("9", null), + }, + head: "8", + }), + x: S.copy({ + commits: { + "1": new Commit({ + changes: { + "README.md": "hello world" + }, + message: "the first commit", + }), + "8": new Commit({ + parents: ["1"], + changes: { "8": "8" }, + message: "message\n", + }), + "9": new Commit({ + parents: ["1"], + changes: { "9": "9" }, + message: "message\n", + }), + }, + cherryPick: new RepoAST.CherryPick("8", "9"), + }), + } + }, "new open sub": { i: "a=B|x=S:I s=Sa:;Os", e: { diff --git a/node/test/util/status_util.js b/node/test/util/status_util.js index d45712f9f..629839a81 100644 --- a/node/test/util/status_util.js +++ b/node/test/util/status_util.js @@ -33,7 +33,9 @@ const assert = require("chai").assert; const co = require("co"); const path = require("path"); +const NodeGit = require("nodegit"); +const CherryPick = require("../../lib/util/cherry_pick"); const Merge = require("../../lib/util/merge"); const Rebase = require("../../lib/util/rebase"); const RepoAST = require("../../lib/util/repo_ast"); @@ -42,10 +44,13 @@ const RepoStatus = require("../../lib/util/repo_status"); const StatusUtil = require("../../lib/util/status_util"); const SubmoduleUtil = require("../../lib/util/submodule_util"); const SubmoduleConfigUtil = require("../../lib/util/submodule_config_util"); +const UserError = require("../../lib/util/user_error"); // test utilities describe("StatusUtil", function () { + const FILEMODE = NodeGit.TreeEntry.FILEMODE; + const BLOB = FILEMODE.BLOB; const FILESTATUS = RepoStatus.FILESTATUS; const RELATION = RepoStatus.Submodule.COMMIT_RELATION; const Submodule = RepoStatus.Submodule; @@ -107,6 +112,7 @@ describe("StatusUtil", function () { workdir: { y: RepoStatus.FILESTATUS.ADDED }, rebase: new Rebase("foo", "1", "1"), merge: new Merge("foo", "1", "1"), + cherryPick: new CherryPick("1", "1"), }), commitMap: { "1": "3"}, urlMap: {}, @@ -117,6 +123,7 @@ describe("StatusUtil", function () { workdir: { y: RepoStatus.FILESTATUS.ADDED }, rebase: new Rebase("foo", "3", "3"), merge: new Merge("foo", "3", "3"), + cherryPick: new CherryPick("3", "3"), }), }, "with a sub": { @@ -480,6 +487,62 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, })); }); + describe("readConflicts", function () { + const Conflict = RepoStatus.Conflict; + const FILEMODE = NodeGit.TreeEntry.FILEMODE; + const BLOB = FILEMODE.BLOB; + const cases = { + "trivial": { + state: "S", + expected: {}, + }, + "a conflict": { + state: "S:I *README.md=a*b*c,foo=bar", + expected: { + "README.md": new Conflict(BLOB, BLOB, BLOB), + }, + }, + "missing ancestor": { + state: "S:I *README.md=~*a*c", + expected: { + "README.md": new Conflict(null, BLOB, BLOB), + }, + }, + "missing our": { + state: "S:I *README.md=a*~*c", + expected: { + "README.md": new Conflict(BLOB, null, BLOB), + }, + }, + "missing their": { + state: "S:I *README.md=a*a*~", + expected: { + "README.md": new Conflict(BLOB, BLOB, null), + }, + }, + "submodule": { + state: "S:I *README.md=a*a*S:1", + expected: { + "README.md": new Conflict(BLOB, BLOB, FILEMODE.COMMIT), + }, + }, + "ignore submodule sha conflict": { + state: "S:I *README.md=a*S:1*S:1", + expected: {}, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(c.state); + const repo = written.repo; + const index = yield repo.index(); + const result = StatusUtil.readConflicts(index); + assert.deepEqual(result, c.expected); + })); + }); + }); + describe("getRepoStatus", function () { // The logic for reading individual files is tested by `getChanges`, so // we don't need to do exhaustive testing on that here. @@ -513,6 +576,14 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, merge: new Merge("hi\n", "2", "3"), }), }, + "cherry-pick": { + state: "x=S:C2-1;C3-1;Bfoo=3;Bmaster=2;P2,3", + expected: new RepoStatus({ + headCommit: "2", + currentBranchName: "master", + cherryPick: new CherryPick("2", "3"), + }), + }, "staged change": { state: "x=S:I README.md=whoohoo", options: { @@ -774,6 +845,16 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, }, }), }, + "conflict": { + state: "x=S:I *foo=~*ff*~", + expected: new RepoStatus({ + currentBranchName: "master", + headCommit: "1", + staged: { + foo: new RepoStatus.Conflict(null, BLOB, null), + }, + }), + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; @@ -794,4 +875,50 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, })); }); }); + + describe("ensureReady", function () { + const cases = { + "ready": { + input: new RepoStatus(), + fails: false, + }, + "rebase": { + input: new RepoStatus({ + rebase: new Rebase("foo", "bart", "baz"), + }), + fails: true, + }, + "merge": { + input: new RepoStatus({ + merge: new Merge("foo", "bart", "baz"), + }), + fails: true, + }, + "cherry-pick": { + input: new RepoStatus({ + cherryPick: new CherryPick("foo", "bart"), + }), + fails: true, + }, + }; + Object.keys(cases).forEach(caseName => { + it(caseName, function () { + const c = cases[caseName]; + let exception; + try { + StatusUtil.ensureReady(c.input); + } catch (e) { + exception = e; + } + if (undefined === exception) { + assert.equal(c.fails, false); + } else { + if (!(exception instanceof UserError)) { + throw exception; + } + assert.equal(c.fails, true); + } + }); + }); + }); }); diff --git a/node/test/util/submodule_config_util.js b/node/test/util/submodule_config_util.js index 0433cdc60..0a513164f 100644 --- a/node/test/util/submodule_config_util.js +++ b/node/test/util/submodule_config_util.js @@ -798,5 +798,17 @@ foo }); }); }); + it("writeUrls", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const index = yield repo.index(); + yield SubmoduleConfigUtil.writeUrls(repo, index, { + foo: "/bar" + }); + const fromIndex = + yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + assert.deepEqual(fromIndex, { + foo: "/bar" + }); + })); }); diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index 253142210..f54c4d653 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -371,27 +371,52 @@ describe("SubmoduleUtil", function () { })); }); - describe("getSubmoduleChanges", function () { + describe("getSubmoduleChangesFromDiff", function () { const cases = { "trivial": { state: "S", from: "1", result: {}, + allowMetaChanges: true, + }, + "trivial, no meta": { + state: "S", + from: "1", + result: {}, + allowMetaChanges: false, + fails: true, }, "changed something else": { state: "S:C2-1 README.md=foo;H=2", from: "2", result: {}, + allowMetaChanges: true, + }, + "changed something in meta, not allowed": { + state: "S:C2-1 README.md=foo;H=2", + from: "2", + result: {}, + allowMetaChanges: false, + fails: true, }, "removed something else": { state: "S:C2-1 README.md;H=2", from: "2", result: {}, + allowMetaChanges: true, + }, + "removed in meta, not allowed": { + state: "S:C2-1 README.md;H=2", + from: "2", + result: {}, + allowMetaChanges: false, + fails: true, }, "not on current commit": { state: "S:C2-1 x=Sa:1;H=2", from: "1", result: {}, + allowMetaChanges: true, }, "added one": { state: "S:C2-1 x=Sa:1;H=2", @@ -399,6 +424,7 @@ describe("SubmoduleUtil", function () { result: { "x": new SubmoduleChange(null, "1"), }, + allowMetaChanges: false, }, "added two": { state: "S:C2-1 a=Sa:1,x=Sa:1;H=2", @@ -407,6 +433,7 @@ describe("SubmoduleUtil", function () { a: new SubmoduleChange(null, "1"), x: new SubmoduleChange(null, "1"), }, + allowMetaChanges: true, }, "changed one": { state: "S:C3-2 a=Sa:2;C2-1 a=Sa:1,x=Sa:1;H=3", @@ -414,11 +441,13 @@ describe("SubmoduleUtil", function () { result: { a: new SubmoduleChange("1", "2"), }, + allowMetaChanges: true, }, "changed url": { state: "S:C3-2 a=Sb:1;C2-1 a=Sa:1,x=Sa:1;H=3", from: "3", result: {}, + allowMetaChanges: true, }, "changed and added": { state: "S:C3-2 a=Sa:2,c=Sa:2;C2-1 a=Sa:1,x=Sa:1;H=3", @@ -427,13 +456,15 @@ describe("SubmoduleUtil", function () { a: new SubmoduleChange("1", "2"), c: new SubmoduleChange(null, "2"), }, + allowMetaChanges: true, }, "removed one": { - state: "S:C3-2 a=;C2-1 a=Sa:1,x=Sa:1;H=3", + state: "S:C3-2 a;C2-1 a=Sa:1,x=Sa:1;H=3", from: "3", result: { a: new SubmoduleChange("1", null), }, + allowMetaChanges: false, }, "added and removed": { state: "S:C3-2 a,c=Sa:2;C2-1 a=Sa:1,x=Sa:1;H=3", @@ -442,6 +473,85 @@ describe("SubmoduleUtil", function () { c: new SubmoduleChange(null, "2"), a: new SubmoduleChange("1", null), }, + allowMetaChanges: true, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(c.state); + const repo = written.repo; + const fromSha = written.oldCommitMap[c.from]; + const fromId = NodeGit.Oid.fromString(fromSha); + const commit = yield repo.getCommit(fromId); + let parentTree = null; + const parents = yield commit.getParents(); + if (0 !== parents.length) { + parentTree = yield parents[0].getTree(); + } + const tree = yield commit.getTree(); + const diff = yield NodeGit.Diff.treeToTree(repo, + parentTree, + tree, + null); + let changes; + let exception; + try { + changes = yield SubmoduleUtil.getSubmoduleChangesFromDiff( + diff, + c.allowMetaChanges); + } + catch (e) { + exception = e; + } + const shouldFail = c.fails || false; + if (undefined === exception) { + assert.equal(false, shouldFail); + } + else { + if (!(exception instanceof UserError)) { + throw exception; + } + assert.equal(true, shouldFail); + return; // RETURN + } + + const commitMap = written.commitMap; + + // map the logical commits in the expected results to the + // actual commit ids + + Object.keys(changes).forEach(name => { + const change = changes[name]; + assert.instanceOf(change, SubmoduleChange); + const oldSha = change.oldSha && commitMap[change.oldSha]; + const newSha = change.newSha && commitMap[change.newSha]; + changes[name] = new SubmoduleChange(oldSha, newSha); + }); + assert.deepEqual(changes, c.result); + })); + }); + }); + + describe("getSubmoduleChanges", function () { + // We know this is implemented in terms of + // `getSubmoduleChangesFromDiff`, so we just need to verify that it's + // hooked up correctly. + + const cases = { + "trivial": { + state: "S", + from: "1", + fails: true, + allowMetaChanges: false, + }, + "added one": { + state: "S:C2-1 x=Sa:1;H=2", + from: "2", + result: { + "x": new SubmoduleChange(null, "1"), + }, + allowMetaChanges: false, }, }; Object.keys(cases).forEach(caseName => { @@ -452,8 +562,28 @@ describe("SubmoduleUtil", function () { const fromSha = written.oldCommitMap[c.from]; const fromId = NodeGit.Oid.fromString(fromSha); const commit = yield repo.getCommit(fromId); - const changes = - yield SubmoduleUtil.getSubmoduleChanges(repo, commit); + let changes; + let exception; + try { + changes = yield SubmoduleUtil.getSubmoduleChanges( + repo, + commit, + c.allowMetaChanges); + } + catch (e) { + exception = e; + } + const shouldFail = c.fails || false; + if (undefined === exception) { + assert.equal(false, shouldFail); + } + else { + if (!(exception instanceof UserError)) { + throw exception; + } + assert.equal(true, shouldFail); + return; // RETURN + } const commitMap = written.commitMap; @@ -477,16 +607,48 @@ describe("SubmoduleUtil", function () { state: "S:C2-1 foo=Sa:1;H=2", commit: "2", expected: { foo: new Submodule("a", "1") }, + names: null, + }, + "two": { + state: "S:C2-1 foo=Sa:1,bar=Sa:1;H=2", + commit: "2", + expected: { + foo: new Submodule("a", "1"), + bar: new Submodule("a", "1"), + }, + names: null, + }, + "no names": { + state: "S:C2-1 foo=Sa:1,bar=Sa:1;H=2", + commit: "2", + expected: {}, + names: [], + }, + "bad name": { + state: "S:C2-1 foo=Sa:1,bar=Sa:1;H=2", + commit: "2", + expected: {}, + names: ["whoo"], + }, + "good name": { + state: "S:C2-1 foo=Sa:1,bar=Sa:1;H=2", + commit: "2", + expected: { + bar: new Submodule("a", "1"), + }, + names: ["bar"], }, "from later commit": { state: "S:C2-1 x=S/a:1;C3-2 x=S/a:2;H=3", commit: "3", expected: { x: new Submodule("/a", "2") }, + names: null, }, "none": { state: "S:Cu 1=1;Bu=u", commit: "u", expected: {}, + names: null, }, }; Object.keys(cases).forEach(caseName => { @@ -497,8 +659,9 @@ describe("SubmoduleUtil", function () { const mappedCommitSha = written.oldCommitMap[c.commit]; const commit = yield repo.getCommit(mappedCommitSha); const result = yield SubmoduleUtil.getSubmodulesForCommit( - repo, - commit); + repo, + commit, + c.names); let mappedResult = {}; Object.keys(result).forEach((name) => { const resultSub = result[name]; diff --git a/node/test/util/write_repo_ast_util.js b/node/test/util/write_repo_ast_util.js index 7f4b02d47..0089b6395 100644 --- a/node/test/util/write_repo_ast_util.js +++ b/node/test/util/write_repo_ast_util.js @@ -343,10 +343,13 @@ S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q", S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q;H=3", }, "with in-progress merge": "S:Mhello,1,1", + "with in-progress cherry-pick": "S:P1,1", "headless": { input: new RepoAST(), expected: new RepoAST(), }, + "conflict": "S:I *README.md=aa*bb*cc;W README.md=yyy", + "submodule conflict": "S:I *README.md=aa*S:1*cc;W README.md=yyy", }; Object.keys(cases).forEach(caseName => { @@ -396,6 +399,8 @@ S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q;H=3", "cloned": "a=B|x=Ca", "pathed tracking branch": "a=B:Bfoo/bar=1|x=Ca:Bfoo/bar=1 origin/foo/bar", + "open submodule conflict": + "a=B|x=U:I *README.md=aa*S:1*cc;W README.md=yyy;Os", }; Object.keys(cases).forEach(caseName => { const input = cases[caseName]; From 30e3ed264525bbeb09bd926d671ab4d83a50f8b9 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 9 Mar 2018 14:59:32 -0500 Subject: [PATCH 058/402] Stop using unnecessary `stateCleanup` after cherry Addresses: https://github.com/twosigma/git-meta/issues/224 --- node/lib/util/cherry_pick_util.js | 1 - 1 file changed, 1 deletion(-) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 90f723ffa..9aa5c5684 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -382,7 +382,6 @@ const finish = co.wrap(function *(repo, commit) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); - repo.stateCleanup(); const defaultSig = repo.defaultSignature(); const metaCommit = yield repo.createCommitOnHead([], defaultSig, From fe1bc606c8ac4381478eed0679be129c000f89e0 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 7 Mar 2018 17:06:42 -0500 Subject: [PATCH 059/402] Fix add to stage conflicted files `git meta add` was not staging files with conflicts because they didn't show up in my status object under the workdir. --- node/lib/util/add.js | 10 ++++++++++ node/test/util/add.js | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/node/lib/util/add.js b/node/lib/util/add.js index 0374d869a..d12a5b099 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -83,6 +83,16 @@ exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update) { return index.addByPath(filename); } }); + + // Add conflicted files. + + const staged = subStat.workdir.status.staged; + yield Object.keys(staged).map(co.wrap(function *(filename) { + const change = staged[filename]; + if (change instanceof RepoStatus.Conflict) { + yield index.addByPath(filename); + } + })); yield index.write(); } })); diff --git a/node/test/util/add.js b/node/test/util/add.js index 6df2eb6ac..d0ae5ff74 100644 --- a/node/test/util/add.js +++ b/node/test/util/add.js @@ -146,6 +146,11 @@ a=B|x=S:C2-1 a/b=Sa:1;Oa/b W x/y/z=a,x/r/z=b;Bmaster=2`, expected: "x=U:Os I README.md", paths: [""], }, + "sub with conflict": { + initial: "a=B|x=U:Os I *README.md=a*b*c!W README.md=foo", + paths: ["s"], + expected: "x=E:Os I README.md=foo", + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From a83831d126d89d83f8c7498d5a427ddeae660b18 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Mon, 12 Mar 2018 16:58:19 -0400 Subject: [PATCH 060/402] Stop `reset --hard` if will remove dirty open subs In this case, libgit2 will delete the submodule's directory, throwing away all changes. See https://github.com/twosigma/git-meta/issues/509 I do not want to do this check when resetting to abort cherry-pick -- we started out clean before picking and it could be pretty common that we've added a submodule that we want to get rid of when we abort -- but I don't want to add a weird flag that will exist just for this workaround. So, I went ahead and added a `MERGE` reset type, that will eventually do a real `git reset --merge` but for the time being is just like `HARD` except that it does not do this check. --- node/lib/util/cherry_pick_util.js | 2 +- node/lib/util/reset.js | 70 ++++++++++++++++++++++++++++--- node/test/util/reset.js | 70 ++++++++++++++++++++++++++++++- 3 files changed, 133 insertions(+), 9 deletions(-) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 9aa5c5684..12f463f24 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -569,7 +569,7 @@ exports.abort = co.wrap(function *(repo) { throw new UserError("No cherry-pick in progress."); } const commit = yield repo.getCommit(cherry.originalHead); - yield Reset.reset(repo, commit, Reset.TYPE.HARD); + yield Reset.reset(repo, commit, Reset.TYPE.MERGE); yield CherryPickFileUtil.cleanCherryPick(repo.path()); console.log("Cherry-pick aborted."); }); diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index 58be2f453..7e0b79b45 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -32,6 +32,7 @@ const assert = require("chai").assert; const co = require("co"); +const colors = require("colors"); const NodeGit = require("nodegit"); const DoWorkQueue = require("../util/do_work_queue"); @@ -46,6 +47,7 @@ const TYPE = { SOFT: "soft", MIXED: "mixed", HARD: "hard", + MERGE: "merge", }; Object.freeze(TYPE); exports.TYPE = TYPE; @@ -60,10 +62,59 @@ function getType(type) { case TYPE.SOFT : return NodeGit.Reset.TYPE.SOFT; case TYPE.MIXED: return NodeGit.Reset.TYPE.MIXED; case TYPE.HARD : return NodeGit.Reset.TYPE.HARD; + + // TODO: real implementation of `reset --merge`. For now, this behaves + // just like `HARD` except that we ignore the check for modified open + // submodules. + + case TYPE.MERGE: return NodeGit.Reset.TYPE.HARD; } assert(false, `Bad type: ${type}`); } +/** + * Throw a `UserError` if any open submodule as listed by the specified + * `opener` has unstaged changes and does not exist in the specified `shas`. + * + * @param {Open.Opener} opener + * @param {Object} shas map from submodule path to SHA + */ +exports.validateClean = co.wrap(function *(opener, shas) { + assert.instanceOf(opener, Open.Opener); + assert.isObject(shas); + + const openSubs = Array.from(yield opener.getOpenSubs()); + const badSubs = []; + const checkSub = co.wrap(function *(name) { + if (name in shas) { + // If it exists in `shas` we don't need to check it + return; // RETURN + } + const repo = yield opener.getSubrepo(name); + const status = yield StatusUtil.getRepoStatus(repo, { + showMetaChanges: true, + }); + if (!status.isWorkdirClean(true)) { + badSubs.push(name); + } + }); + yield DoWorkQueue.doInParallel(openSubs, checkSub); + if (0 !== badSubs.length) { + badSubs.sort(); + let errorMessage = `\ +The following submodules with unstaged will be deleted by this reset: + +`; + for (let name of badSubs) { + errorMessage += ` ${colors.red(name)}\n`; + } + errorMessage += ` +Please stage these changes or close the submodules before proceeding. +`; + throw new UserError(errorMessage); + } +}); + /** * Change the `HEAD` commit to the specified `commit` in the specified `repo`, * unstaging any staged changes. Reset all open submodule in the same way to @@ -99,12 +150,6 @@ exports.reset = co.wrap(function *(repo, commit, type) { const resetType = getType(type); - // First, reset the meta-repo. - - yield SubmoduleUtil.cacheSubmodules(repo, () => { - return NodeGit.Reset.reset(repo, commit, resetType); - }); - // Make a list of submodules to reset, including all that have been changed // between HEAD and 'commit', and all that are open. @@ -115,6 +160,19 @@ exports.reset = co.wrap(function *(repo, commit, type) { const shas = yield SubmoduleUtil.getSubmoduleShasForCommit(repo, pathsToReset, commit); + + // If doing a HARD reset, work around the problem where libgit2 may throw + // away unstaged changes in open submodules. See + // https://github.com/twosigma/git-meta/issues/509 + + if (TYPE.HARD === type) { + yield exports.validateClean(opener, shas); + } + + yield SubmoduleUtil.cacheSubmodules(repo, () => { + return NodeGit.Reset.reset(repo, commit, resetType); + }); + const index = yield repo.index(); const resetSubmodule = co.wrap(function *(name) { const change = changedSubs[name]; diff --git a/node/test/util/reset.js b/node/test/util/reset.js index 001321cb0..39c310b38 100644 --- a/node/test/util/reset.js +++ b/node/test/util/reset.js @@ -30,13 +30,61 @@ */ "use strict"; -const co = require("co"); -const path = require("path"); +const assert = require("chai").assert; +const co = require("co"); +const path = require("path"); +const Open = require("../../lib/util/open"); const Reset = require("../../lib/util/reset"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const UserError = require("../../lib/util/user_error"); describe("reset", function () { + describe("validateClean", function () { + const cases = { + "trivial": { + state: "x=S", + shas: {}, + fails: false, + }, + "open and clean": { + state: "a=B|x=U:Os", + shas: {}, + fails: false, + }, + "open, dirty, but existent": { + state: "a=B|x=U:Os W README.md=8888", + shas: { s: "3" }, + fails: false, + }, + "open, dirty, and gone": { + state: "a=B|x=U:Os W README.md=8888", + shas: { t: "3" }, + fails: true, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const w = yield RepoASTTestUtil.createMultiRepos(c.state); + const repo = w.repos.x; + const opener = new Open.Opener(repo, null); + let exception; + try { + yield Reset.validateClean(opener, c.shas); + } catch (e) { + exception = e; + } + if (undefined === exception) { + assert.equal(false, c.fails); + } else { + if (!(exception instanceof UserError) || !c.fails) { + throw exception; + } + } + })); + }); + }); describe("reset", function () { // We are deferring the actual reset logic to NodeGit, so we are not @@ -123,6 +171,24 @@ a=B:Ca-1;Bmaster=a|x=U:C3-2 s=Sa:a;Bmaster=3;Bf=3;Os`, type: TYPE.SOFT, expected: "x=E:Os H=1!I a=a;Bmaster=2", }, + "hard, submodule with changes should refuse": { + initial: `a=B|x=U:Os W README.md=888`, + to: "1", + type: TYPE.HARD, + fails: true, + }, + "merge should do as HARD but not refuse": { + initial: `a=B|x=U:Os W README.md=888`, + to: "1", + type: TYPE.MERGE, + expected: "x=S", + }, + "soft, submodule with changes should not refuse": { + initial: `a=B|x=U:Os W README.md=888;Bfoo=2`, + to: "1", + expected: "x=E:Bmaster=1;I s=Sa:1", + type: TYPE.SOFT, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From 073f1c767bbc810c038d744fb70b8936155f94eb Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 13 Mar 2018 22:11:08 -0400 Subject: [PATCH 061/402] Support for reading and writing sequencer states Addreses task: https://github.com/twosigma/git-meta/projects/2#card-8117068 --- node/lib/util/sequencer_state.js | 210 +++++++++++++++++ node/lib/util/sequencer_state_util.js | 260 +++++++++++++++++++++ node/test/util/sequencer_state.js | 237 +++++++++++++++++++ node/test/util/sequencer_state_util.js | 312 +++++++++++++++++++++++++ 4 files changed, 1019 insertions(+) create mode 100644 node/lib/util/sequencer_state.js create mode 100644 node/lib/util/sequencer_state_util.js create mode 100644 node/test/util/sequencer_state.js create mode 100644 node/test/util/sequencer_state_util.js diff --git a/node/lib/util/sequencer_state.js b/node/lib/util/sequencer_state.js new file mode 100644 index 000000000..eaa917bab --- /dev/null +++ b/node/lib/util/sequencer_state.js @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const deeper = require("deeper"); + +const TYPE = { + CHERRY_PICK: "CHERRY_PICK", + MERGE: "MERGE", + REBASE: "REBASE", +}; + +/** + * This module defines the `SequencerState` value-semantic type. + */ + +/** + * @class CommitAndRef + * + * This class describes a commit and optionally the ref it came from. + */ +class CommitAndRef { + /** + * Create a new `CommitAndRef` object. + * + * @param {String} sha + * @param {String|null} ref + */ + constructor(sha, ref) { + assert.isString(sha); + if (null !== ref) { + assert.isString(ref); + } + this.d_sha = sha; + this.d_ref = ref; + + Object.freeze(this); + } + + /** + * @property {String} sha the unique identifier for this commit + */ + get sha() { + return this.d_sha; + } + + /** + * @property {String|null} ref + * + * If the commit was referenced by a ref, this is its name. + */ + get ref() { + return this.d_ref; + } + + /** + * Return true if the specified `rhs` represents the same value as this + * object. Two `CommitAndRef` objects represet the same value if they have + * the same `sha` and `ref` properties. + * + * @param {CommitAndRef} rhs + * @return {Bool} + */ + equal(rhs) { + assert.instanceOf(rhs, CommitAndRef); + return this.d_sha === rhs.d_sha && this.d_ref === rhs.d_ref; + } +} + +CommitAndRef.prototype.toString = function () { + let result = `CommitAndRef(sha=${this.d_sha}`; + if (null !== this.d_ref) { + result += `, ref=${this.d_ref}`; + } + return result + ")"; +}; + +/** + * @class SequencerState + * + * This class represents the state of an in-progress sequence operation such as + * a merge, cherry-pick, or rebase. + */ +class SequencerState { + /** + * Create a new `SequencerState` object. The behavior is undefined unless + * `0 <= currentLength` and `commits.length > currentCommit`. + * + * @param {Object} properties + * @param {TYPE} properties.type + * @param {CommitAndRef} properties.originalHead + * @param {CommitAndRef} properties.target + * @param {[String]} properties.commits + * @param {Number} properties.currentCommit + */ + constructor(properties) { + assert.isString(properties.type); + assert.property(TYPE, properties.type); + assert.instanceOf(properties.originalHead, CommitAndRef); + assert.instanceOf(properties.target, CommitAndRef); + assert.isArray(properties.commits); + assert.isNumber(properties.currentCommit); + assert(0 <= properties.currentCommit); + assert(properties.commits.length > properties.currentCommit); + + this.d_type = properties.type; + this.d_originalHead = properties.originalHead; + this.d_target = properties.target; + this.d_commits = properties.commits; + this.d_currentCommit = properties.currentCommit; + + Object.freeze(this); + } + + /** + * @property {TYPE} the type of operation in progress + */ + get type() { + return this.d_type; + } + + /** + * @property {CommitAndRef} originalHead + * what HEAD pointed to when the operation started + */ + get originalHead() { + return this.d_originalHead; + } + + /** + * @property {CommitAndRef} target + * the commit that was the target of the operation + */ + get target() { + return this.d_target; + } + + /** + * @property {[String]} commits the sequence of commits to operate on + */ + get commits() { + return this.d_commits; + } + + /** + * @property {Number} currentCommit index of the current commit + */ + get currentCommit() { + return this.d_currentCommit; + } + + /** + * Return true if the specified `rhs` represents the same value as this + * `SequencerState` object and false otherwise. Two `SequencerState` + * objects represent the same value if they have the same `type`, + * `originalHead`, `target`, `commits`, and `currentCommit` properties. + * + * @param {SequencerState} rhs + * @return {Bool} + */ + equal(rhs) { + assert.instanceOf(rhs, SequencerState); + return this.d_type === rhs.d_type && + this.d_originalHead.equal(rhs.d_originalHead) && + this.d_target.equal(rhs.d_target) && + deeper(this.d_commits, rhs.d_commits) && + this.d_currentCommit === rhs.d_currentCommit; + } +} + +SequencerState.prototype.toString = function () { + return `\ +SequencerState(type=${this.d_type}, originalHead=${this.d_originalHead}, \ +target=${this.d_target}, commits=${JSON.stringify(this.d_commits)}, \ +currentCommit=${this.d_currentCommit})`; +}; + +SequencerState.TYPE = TYPE; +SequencerState.CommitAndRef = CommitAndRef; + +module.exports = SequencerState; diff --git a/node/lib/util/sequencer_state_util.js b/node/lib/util/sequencer_state_util.js new file mode 100644 index 000000000..616796632 --- /dev/null +++ b/node/lib/util/sequencer_state_util.js @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const fs = require("fs-promise"); +const path = require("path"); +const rimraf = require("rimraf"); + +const SequencerState = require("./sequencer_state"); + +const CommitAndRef = SequencerState.CommitAndRef; + +/** + * This module contains methods for accessing, reading, and rendering + * `SequencerState` objects on disk. The disk structure is (note, all files + * are eol-terminated): + * + * .git/meta_sequencer/TYPE -- Single line containing the type, e.g. + * "REBASE". + * /ORIGINAL_HEAD -- SHA and optional ref name of what was + * -- on head. The first line is the SHA. + * If there is one line, there is no + * ref name. If there are two lines, + * the second is the ref name. + * /TARGET -- SHA and optional ref name of the + * -- commit to rebase onto, merge, etc. + * The format is same as ORIGINAL_HEAD. + * /COMMITS -- List of commits to rebase, + * cherry-pick, etc., one per line. + * /CURRENT_COMMIT -- Single line containing the index of + * current commit in COMMITS to operate + * on. + */ + +const SEQUENCER_DIR = "meta_sequencer"; +const TYPE_FILE = "TYPE"; +const ORIGINAL_HEAD_FILE = "ORIGINAL_HEAD"; +const TARGET_FILE = "TARGET"; +const COMMITS_FILE = "COMMITS"; +const CURRENT_COMMIT_FILE = "CURRENT_COMMIT"; + +/** + * Return the contents of the file in the sequencer directory from the + * specified `gitDir` having the specified `name`, or null if the file cannot + * be read. + * + * @param {String} gitDir + * @param {String} name + * @return {String|null} + */ +exports.readFile = co.wrap(function *(gitDir, name) { + assert.isString(gitDir); + assert.isString(name); + const filePath = path.join(gitDir, SEQUENCER_DIR, name); + try { + return yield fs.readFile(filePath, "utf8"); + } + catch (e) { + return null; + } +}); + +/** + * Read the `CommitAndRef` object from the specified `fileName` in the + * sequencer director in the specified `gitDir` if it exists, or null if it + * does not. The format + * + * @param {String} gitDir + * @param {String} fileName + * @return {CommitAndRef|null} + */ +exports.readCommitAndRef = co.wrap(function *(gitDir, fileName) { + assert.isString(gitDir); + assert.isString(fileName); + const content = yield exports.readFile(gitDir, fileName); + if (null !== content) { + const lines = content.split("\n"); + lines.pop(); + const numLines = lines.length; + if (1 === numLines || 2 === numLines) { + const ref = 2 === numLines ? lines[1] : null; + return new CommitAndRef(lines[0], ref); + } + } + return null; +}); + +/** + * Return the array of commit stored in the specified `gitDir`, or null if + * the file is missing or malformed. + * + * @param {String} gitDir + * @return {[String]|null} + */ +exports.readCommits = co.wrap(function *(gitDir) { + assert.isString(gitDir); + const content = yield exports.readFile(gitDir, COMMITS_FILE); + if (null !== content) { + const lines = content.split("\n"); + const nonEmpty = lines.filter(line => line.length > 0); + if (0 !== nonEmpty.length) { + return nonEmpty; + } + } + return null; +}); + +/** + * Return the index of the current commit stored in the specified `gitDir`, or + * null if the file is missing, malformed, or the index is out-of-range. It is + * out-of-range if it is >= the specified `numCommits`, which is the size of + * the array of commit operations stored in the sequencer in `gitDir`, e.g., if + * there is 1 commit operation, the only valid index is 0. + * + * @param {String} gitDir + * @param {Number} numCommits + * @return {Number} + */ +exports.readCurrentCommit = co.wrap(function *(gitDir, numCommits) { + assert.isString(gitDir); + assert.isNumber(numCommits); + const content = yield exports.readFile(gitDir, CURRENT_COMMIT_FILE); + if (null !== content) { + const lines = content.split("\n"); + if (0 !== lines.length) { + const index = Number.parseInt(lines[0]); + if (!Number.isNaN(index) && index < numCommits) { + return index; + } + } + } + return null; +}); + +/** + * Return the sequencer state if it exists in the specified `gitDir`, or null + * if it is missing or malformed. + * TODO: emit diagnostic when malformed? + * + * @param {String} gitDir + * @return {SequencerState|null} + */ +exports.readSequencerState = co.wrap(function *(gitDir) { + assert.isString(gitDir); + + const typeContent = yield exports.readFile(gitDir, TYPE_FILE); + if (null === typeContent) { + return null; // RETURN + } + const typeLines = typeContent.split("\n"); + if (2 !== typeLines.length) { + return null; // RETURN + } + const type = typeLines[0]; + if (!(type in SequencerState.TYPE)) { + return null; // RETURN + } + const original = yield exports.readCommitAndRef(gitDir, + ORIGINAL_HEAD_FILE); + if (null === original) { + return null; // RETURN + } + const target = yield exports.readCommitAndRef(gitDir, TARGET_FILE); + if (null === target) { + return null; // RETURN + } + const commits = yield exports.readCommits(gitDir); + if (null === commits) { + return null; // RETURN + } + const currentCommit = yield exports.readCurrentCommit(gitDir, + commits.length); + if (null === currentCommit) { + return null; // RETURN + } + return new SequencerState({ + type: type, + originalHead: original, + target: target, + commits: commits, + currentCommit: currentCommit + }); +}); + +/** + * Remote the sequencer directory and all its content in the specified + * `gitDir`, or do nothing if this directory doesn't exist. + * + * @param {String} gitDir + */ +exports.cleanSequencerState = co.wrap(function *(gitDir) { + assert.isString(gitDir); + const root = path.join(gitDir, SEQUENCER_DIR); + const promise = new Promise(callback => { + return rimraf(root, {}, callback); + }); + yield promise; +}); + +const writeCommitAndRef = co.wrap(function *(dir, name, commitAndRef) { + const filePath = path.join(dir, name); + let content = commitAndRef.sha + "\n"; + if (null !== commitAndRef.ref) { + content += commitAndRef.ref + "\n"; + } + yield fs.writeFile(filePath, content); +}); + +/** + * Clear out any existing sequencer and write the specified `state` to the + * specified `gitDir`. + * + * @param {String} gitDir + * @param {SequencerState} state + */ +exports.writeSequencerState = co.wrap(function *(gitDir, state) { + assert.isString(gitDir); + assert.instanceOf(state, SequencerState); + + yield exports.cleanSequencerState(gitDir); + const root = path.join(gitDir, SEQUENCER_DIR); + yield fs.mkdir(root); + yield fs.writeFile(path.join(root, TYPE_FILE), state.type + "\n"); + yield writeCommitAndRef(root, ORIGINAL_HEAD_FILE, state.originalHead); + yield writeCommitAndRef(root, TARGET_FILE, state.target); + const commitsContent = state.commits.join("\n"); + yield fs.writeFile(path.join(root, COMMITS_FILE), commitsContent); + yield fs.writeFile(path.join(root, CURRENT_COMMIT_FILE), + "" + state.currentCommit); +}); diff --git a/node/test/util/sequencer_state.js b/node/test/util/sequencer_state.js new file mode 100644 index 000000000..f6cb0d78c --- /dev/null +++ b/node/test/util/sequencer_state.js @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; + +const SequencerState = require("../../lib/util/sequencer_state"); + +describe("SequencerState", function () { + +const TYPE = SequencerState.TYPE; +const CommitAndRef = SequencerState.CommitAndRef; + describe("CommitAndRef", function () { + it("breath", function () { + const withRef = new CommitAndRef("foo", "bar"); + assert.isFrozen(withRef); + assert.equal(withRef.sha, "foo"); + assert.equal(withRef.ref, "bar"); + + const noRef = new CommitAndRef("wee", null); + assert.equal(noRef.sha, "wee"); + assert.isNull(noRef.ref); + }); + describe("equal", function () { + const cases = { + "same": { + lhs: new CommitAndRef("a", "b"), + rhs: new CommitAndRef("a", "b"), + expected: true, + }, + "diff sha": { + lhs: new CommitAndRef("a", "b"), + rhs: new CommitAndRef("b", "b"), + expected: false, + }, + "diff ref": { + lhs: new CommitAndRef("a", null), + rhs: new CommitAndRef("a", "b"), + expected: false, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, function () { + const result = c.lhs.equal(c.rhs); + assert.equal(result, c.expected); + }); + }); + }); + }); + describe("toString", function () { + it("with ref", function () { + const input = new CommitAndRef("foo", "bar"); + const result = "" + input; + assert.equal(result, "CommitAndRef(sha=foo, ref=bar)"); + }); + it("no ref", function () { + const input = new CommitAndRef("foo", null); + const result = "" + input; + assert.equal(result, "CommitAndRef(sha=foo)"); + }); + }); + it("breathe", function () { + const original = new CommitAndRef("a", "foo"); + const target = new CommitAndRef("c", "bar"); + const seq = new SequencerState({ + type: TYPE.MERGE, + originalHead: original, + target: target, + commits: ["3"], + currentCommit: 0 + }); + assert.isFrozen(seq); + assert.equal(seq.type, TYPE.MERGE); + assert.deepEqual(seq.originalHead, original); + assert.deepEqual(seq.target, target); + assert.deepEqual(seq.commits, ["3"]); + assert.equal(seq.currentCommit, 0); + }); + describe("equal", function () { + const cnr0 = new CommitAndRef("a", "foo"); + const cnr1 = new CommitAndRef("b", "foo"); + const cases = { + "same": { + lhs: new SequencerState({ + type: TYPE.MERGE, + originalHead: cnr0, + target: cnr1, + commits: ["1", "2", "3"], + currentCommit: 1, + }), + rhs: new SequencerState({ + type: TYPE.MERGE, + originalHead: cnr0, + target: cnr1, + commits: ["1", "2", "3"], + currentCommit: 1, + }), + expected: true, + }, + "different type": { + lhs: new SequencerState({ + type: TYPE.MERGE, + originalHead: cnr0, + target: cnr1, + commits: ["1", "2", "3"], + currentCommit: 1, + }), + rhs: new SequencerState({ + type: TYPE.REBASE, + originalHead: cnr0, + target: cnr1, + commits: ["1", "2", "3"], + currentCommit: 1, + }), + expected: false, + }, + "different original head": { + lhs: new SequencerState({ + type: TYPE.MERGE, + originalHead: cnr1, + target: cnr1, + commits: ["1", "2", "3"], + currentCommit: 1, + }), + rhs: new SequencerState({ + type: TYPE.MERGE, + originalHead: cnr0, + target: cnr1, + commits: ["1", "2", "3"], + currentCommit: 1, + }), + expected: false, + }, + "different target": { + lhs: new SequencerState({ + type: TYPE.MERGE, + originalHead: cnr0, + target: cnr1, + commits: ["1", "2", "3"], + currentCommit: 1, + }), + rhs: new SequencerState({ + type: TYPE.MERGE, + originalHead: cnr0, + target: cnr0, + commits: ["1", "2", "3"], + currentCommit: 1, + }), + expected: false, + }, + "different commits": { + lhs: new SequencerState({ + type: TYPE.MERGE, + originalHead: cnr0, + target: cnr1, + commits: ["1", "2", "3"], + currentCommit: 1, + }), + rhs: new SequencerState({ + type: TYPE.MERGE, + originalHead: cnr0, + target: cnr1, + commits: ["3", "2", "1"], + currentCommit: 1, + }), + expected: false, + }, + "different current commit": { + lhs: new SequencerState({ + type: TYPE.MERGE, + originalHead: cnr0, + target: cnr1, + commits: ["1", "2", "3"], + currentCommit: 0, + }), + rhs: new SequencerState({ + type: TYPE.MERGE, + originalHead: cnr0, + target: cnr1, + commits: ["1", "2", "3"], + currentCommit: 1, + }), + expected: false, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, function () { + const result = c.lhs.equal(c.rhs); + assert.equal(result, c.expected); + }); + }); + }); + it("toString", function () { + const input = new SequencerState({ + type: TYPE.REBASE, + originalHead: new CommitAndRef("a", null), + target: new CommitAndRef("b", null), + commits: ["1"], + currentCommit: 0, + }); + const result = "" + input; + assert.equal(result, + `\ +SequencerState(type=REBASE, originalHead=CommitAndRef(sha=a), \ +target=CommitAndRef(sha=b), commits=["1"], currentCommit=0)`); + }); +}); diff --git a/node/test/util/sequencer_state_util.js b/node/test/util/sequencer_state_util.js new file mode 100644 index 000000000..cbd030e99 --- /dev/null +++ b/node/test/util/sequencer_state_util.js @@ -0,0 +1,312 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const fs = require("fs-promise"); +const mkdirp = require("mkdirp"); +const path = require("path"); + +const SequencerState = require("../../lib//util/sequencer_state"); +const SequencerStateUtil = require("../../lib//util/sequencer_state_util"); +const TestUtil = require("../../lib/util/test_util"); + +const CommitAndRef = SequencerState.CommitAndRef; + +describe("SequencerStateUtil", function () { +describe("readFile", function () { + it("exists", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "foo"), "1234\n"); + const result = yield SequencerStateUtil.readFile(gitDir, "foo"); + assert.equal(result, "1234\n"); + })); + it("missing", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const result = yield SequencerStateUtil.readFile(gitDir, "foo"); + assert.isNull(result); + })); +}); +describe("readCommitAndRef", function () { + it("nothing", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const result = yield SequencerStateUtil.readCommitAndRef(gitDir, + "foo"); + assert.isNull(result); + })); + it("just a sha", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "foo"), "1234\n"); + const result = yield SequencerStateUtil.readCommitAndRef(gitDir, + "foo"); + assert.instanceOf(result, CommitAndRef); + assert.equal(result.sha, "1234"); + assert.isNull(result.ref); + })); + it("sha and a ref", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "foo"), "12\n34\n"); + const result = yield SequencerStateUtil.readCommitAndRef(gitDir, + "foo"); + assert.instanceOf(result, CommitAndRef); + assert.equal(result.sha, "12"); + assert.equal(result.ref, "34"); + })); + it("too few", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "foo"), "12"); + const result = yield SequencerStateUtil.readCommitAndRef(gitDir, + "foo"); + assert.isNull(result); + })); + it("too many", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "foo"), "1\n2\n3\n"); + const result = yield SequencerStateUtil.readCommitAndRef(gitDir, + "foo"); + assert.isNull(result); + })); +}); +describe("readCommits", function () { + it("got commits", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "COMMITS"), "1\n2\n3\n"); + const result = yield SequencerStateUtil.readCommits(gitDir); + assert.deepEqual(result, ["1", "2", "3"]); + })); + it("missing commits file", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const result = yield SequencerStateUtil.readCommits(gitDir); + assert.isNull(result); + })); + it("no commits in file", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "COMMITS"), ""); + const result = yield SequencerStateUtil.readCommits(gitDir); + assert.isNull(result); + })); +}); +describe("readCurrentCommit", function () { + it("good", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "CURRENT_COMMIT"), "1\n"); + const result = yield SequencerStateUtil.readCurrentCommit(gitDir, 2); + assert.equal(result, 1); + })); + it("missing", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const result = yield SequencerStateUtil.readCurrentCommit(gitDir, 2); + assert.isNull(result); + })); + it("no lines", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "CURRENT_COMMIT"), ""); + const result = yield SequencerStateUtil.readCurrentCommit(gitDir, 2); + assert.isNull(result); + })); + it("non", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "CURRENT_COMMIT"), "x\n"); + const result = yield SequencerStateUtil.readCurrentCommit(gitDir, 2); + assert.isNull(result); + })); + it("bad index", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "CURRENT_COMMIT"), "2\n"); + const result = yield SequencerStateUtil.readCurrentCommit(gitDir, 2); + assert.isNull(result); + })); +}); +describe("readSequencerState", function () { + it("good state", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "TYPE"), + SequencerState.TYPE.MERGE + "\n"); + yield fs.writeFile(path.join(fileDir, "ORIGINAL_HEAD"), "24\n"); + yield fs.writeFile(path.join(fileDir, "TARGET"), "12\n34\n"); + yield fs.writeFile(path.join(fileDir, "COMMITS"), "1\n2\n3\n"); + yield fs.writeFile(path.join(fileDir, "CURRENT_COMMIT"), "1\n"); + const result = yield SequencerStateUtil.readSequencerState(gitDir); + assert.instanceOf(result, SequencerState); + assert.equal(result.type, SequencerState.TYPE.MERGE); + assert.equal(result.originalHead.sha, "24"); + assert.isNull(result.originalHead.ref); + assert.equal(result.target.sha, "12"); + assert.equal(result.target.ref, "34"); + assert.deepEqual(result.commits, ["1", "2", "3"]); + assert.equal(result.currentCommit, 1); + })); + it("bad type", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "ORIGINAL_HEAD"), "24\n"); + yield fs.writeFile(path.join(fileDir, "TARGET"), "12\n34\n"); + yield fs.writeFile(path.join(fileDir, "COMMITS"), "1\n2\n3\n"); + yield fs.writeFile(path.join(fileDir, "CURRENT_COMMIT"), "1\n"); + const result = yield SequencerStateUtil.readSequencerState(gitDir); + assert.isNull(result); + })); + it("wrong type", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "TYPE"), "foo\n"); + yield fs.writeFile(path.join(fileDir, "ORIGINAL_HEAD"), "24\n"); + yield fs.writeFile(path.join(fileDir, "TARGET"), "12\n34\n"); + yield fs.writeFile(path.join(fileDir, "COMMITS"), "1\n2\n3\n"); + yield fs.writeFile(path.join(fileDir, "CURRENT_COMMIT"), "1\n"); + const result = yield SequencerStateUtil.readSequencerState(gitDir); + assert.isNull(result); + })); + it("bad head", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "TYPE"), + SequencerState.TYPE.MERGE + "\n"); + yield fs.writeFile(path.join(fileDir, "TARGET"), "12\n34\n"); + yield fs.writeFile(path.join(fileDir, "COMMITS"), "1\n2\n3\n"); + yield fs.writeFile(path.join(fileDir, "CURRENT_COMMIT"), "1\n"); + const result = yield SequencerStateUtil.readSequencerState(gitDir); + assert.isNull(result); + })); + it("bad target", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "TYPE"), + SequencerState.TYPE.MERGE + "\n"); + yield fs.writeFile(path.join(fileDir, "ORIGINAL_HEAD"), "24\n"); + yield fs.writeFile(path.join(fileDir, "COMMITS"), "1\n2\n3\n"); + yield fs.writeFile(path.join(fileDir, "CURRENT_COMMIT"), "1\n"); + const result = yield SequencerStateUtil.readSequencerState(gitDir); + assert.isNull(result); + })); + it("bad commits", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "TYPE"), + SequencerState.TYPE.MERGE + "\n"); + yield fs.writeFile(path.join(fileDir, "ORIGINAL_HEAD"), "24\n"); + yield fs.writeFile(path.join(fileDir, "TARGET"), "12\n34\n"); + yield fs.writeFile(path.join(fileDir, "CURRENT_COMMIT"), "1\n"); + const result = yield SequencerStateUtil.readSequencerState(gitDir); + assert.isNull(result); + })); + it("bad commits length", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "TYPE"), + SequencerState.TYPE.MERGE + "\n"); + yield fs.writeFile(path.join(fileDir, "ORIGINAL_HEAD"), "24\n"); + yield fs.writeFile(path.join(fileDir, "TARGET"), "12\n34\n"); + yield fs.writeFile(path.join(fileDir, "COMMITS"), "1\n2\n3\n"); + yield fs.writeFile(path.join(fileDir, "CURRENT_COMMIT"), "4\n"); + const result = yield SequencerStateUtil.readSequencerState(gitDir); + assert.isNull(result); + })); + it("bad current commit", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "TYPE"), + SequencerState.TYPE.MERGE + "\n"); + yield fs.writeFile(path.join(fileDir, "ORIGINAL_HEAD"), "24\n"); + yield fs.writeFile(path.join(fileDir, "TARGET"), "12\n34\n"); + yield fs.writeFile(path.join(fileDir, "COMMITS"), "1\n2\n3\n"); + const result = yield SequencerStateUtil.readSequencerState(gitDir); + assert.isNull(result); + })); +}); +describe("cleanSequencerState", function () { + it("breathe", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + const filePath = path.join(fileDir, "TYPE"); + yield fs.writeFile(filePath, SequencerState.TYPE.MERGE); + yield fs.readFile(filePath, "utf8"); + yield SequencerStateUtil.cleanSequencerState(gitDir); + let gone = false; + try { + yield fs.readFile(filePath, "utf8"); + } catch (e) { + gone = true; + } + assert(gone); + })); +}); +describe("writeSequencerState", function () { + it("breathe", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const original = new CommitAndRef("a", null); + const target = new CommitAndRef("b", "c"); + const initial = new SequencerState({ + type: SequencerState.TYPE.REBASE, + originalHead: original, + target: target, + commits: ["1", "2"], + currentCommit: 0 + }); + yield SequencerStateUtil.writeSequencerState(gitDir, initial); + const read = yield SequencerStateUtil.readSequencerState(gitDir); + assert.deepEqual(read, initial); + })); +}); +}); From 7cf170fcd05779f53f079b6b63b2007f74d0ddc6 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 16 Mar 2018 14:23:04 -0400 Subject: [PATCH 062/402] make close limit parallelism to prevent OOM --- node/lib/util/close_util.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/node/lib/util/close_util.js b/node/lib/util/close_util.js index fee1047e8..d3047868c 100644 --- a/node/lib/util/close_util.js +++ b/node/lib/util/close_util.js @@ -36,6 +36,7 @@ const co = require("co"); const colors = require("colors"); const DeinitUtil = require("../util/deinit_util"); +const DoWorkQueue = require("../util/do_work_queue"); const Hook = require("../util/hook"); const StatusUtil = require("../util/status_util"); const SubmoduleUtil = require("../util/submodule_util"); @@ -74,7 +75,7 @@ exports.close = co.wrap(function *(repo, cwd, paths, force) { let errorMessage = ""; let subsClosedSuccessfully = []; - const closers = subsToClose.map(co.wrap(function *(name) { + const closer = co.wrap(function *(name) { const sub = subStats[name]; if (undefined === sub || null === sub.workdir) { return; // RETURN @@ -97,8 +98,8 @@ Pass ${colors.magenta("--force")} to close it anyway. } yield DeinitUtil.deinit(repo, name); subsClosedSuccessfully.push(name); - })); - yield closers; + }); + yield DoWorkQueue.doInParallel(subsToClose, closer); // Run post-close-submodule hook with submodules which closed successfully. yield Hook.execHook("post-close-submodule", subsClosedSuccessfully); From d520bc4b7daa9dcaa19f9a3e812851ba8467ad56 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 16 Mar 2018 14:32:04 -0400 Subject: [PATCH 063/402] a few more limits on parallelism for operations that use libgit2 --- node/lib/util/commit.js | 3 +-- node/lib/util/git_util.js | 7 +++---- node/lib/util/submodule_util.js | 13 +++++++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 87ad4e0df..2739672e3 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -510,8 +510,7 @@ exports.commit = co.wrap(function *(metaRepo, } }); - const subCommitters = Object.keys(submodules).map(commitSubmodule); - yield subCommitters; + yield DoWorkQueue.doInParallel(Object.keys(submodules), commitSubmodule); const result = { metaCommit: null, diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index ad32f6a6f..ce9c1abdf 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -42,7 +42,8 @@ const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); -const UserError = require("../util/user_error"); +const DoWorkQueue = require("../util/do_work_queue"); +const UserError = require("../util/user_error"); /** * If the directory identified by the specified `dir` contains a ".git" @@ -636,9 +637,7 @@ exports.listUnpushedCommits = co.wrap(function *(repo, remote, commit) { } }); - const refCheckers = refs.map(checkRef); - - yield refCheckers; + yield DoWorkQueue.doInParallel(refs, checkRef); // If we found no results (no branches for 'remote', return a list // containing 'commit' and all its history. diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index f6e88f053..1eae56b73 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -41,6 +41,7 @@ const NodeGit = require("nodegit"); const fs = require("fs-promise"); const path = require("path"); +const DoWorkQueue = require("../util/do_work_queue"); const GitUtil = require("./git_util"); const Submodule = require("./submodule"); const SubmoduleChange = require("./submodule_change"); @@ -130,7 +131,7 @@ exports.getSubmoduleShasForCommit = // believes is the proper commit for that submodule. const tree = yield commit.getTree(); - const shaGetters = submoduleNames.map(co.wrap(function *(name) { + const shaGetter = co.wrap(function *(name) { try { const entry = yield tree.entryByPath(name); return entry.sha(); @@ -138,8 +139,8 @@ exports.getSubmoduleShasForCommit = catch (e) { return null; } - })); - const shas = yield shaGetters; + }); + const shas = yield DoWorkQueue.doInParallel(submoduleNames, shaGetter); let result = {}; for (let i = 0; i < submoduleNames.length; ++i) { const sha = shas[i]; @@ -298,7 +299,7 @@ exports.getSubmoduleRepos = co.wrap(function *(repo) { const openSet = new Set(openArray); const submoduleNames = yield exports.getSubmoduleNames(repo); - const openers = submoduleNames.map(co.wrap(function *(name) { + const opener = co.wrap(function *(name) { const isVisible = openSet.has(name); if (!isVisible) { return null; @@ -308,8 +309,8 @@ exports.getSubmoduleRepos = co.wrap(function *(repo) { name: name, repo: subRepo, }; - })); - const repos = yield openers; + }); + const repos = yield DoWorkQueue.doInParallel(submoduleNames, opener); return repos.filter(x => x !== null); }); From 76e5cb767031b3d969dbda7c732374c8f5bd60a8 Mon Sep 17 00:00:00 2001 From: Zokir Tiliaev Date: Thu, 22 Mar 2018 20:41:40 -0400 Subject: [PATCH 064/402] Change to root git directory before executing hooks --- node/lib/util/hook.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/node/lib/util/hook.js b/node/lib/util/hook.js index c019e0ca6..d752ee54c 100644 --- a/node/lib/util/hook.js +++ b/node/lib/util/hook.js @@ -35,6 +35,7 @@ const ChildProcess = require("child-process-promise"); const co = require("co"); const path = require("path"); const GitUtil = require("../util/git_util"); +const process = require("process"); /** * Run git-meta hook with given hook name. @@ -46,10 +47,12 @@ const GitUtil = require("../util/git_util"); exports.execHook = co.wrap(function *(name, args=[]) { assert.isString(name); - const hookPath = path.join(GitUtil.getRootGitDirectory(), ".git/hooks"); + const rootDirectory = GitUtil.getRootGitDirectory(); + const hookPath = path.join(rootDirectory, ".git/hooks"); const absPath = path.resolve(hookPath, name); try { + process.chdir(rootDirectory); const result = yield ChildProcess.execFile(absPath, args); return result.stdout; } catch (e) { From ff91fab0b84b95e41299d42851cafbb8d1acefa2 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 23 Mar 2018 09:41:17 -0400 Subject: [PATCH 065/402] Added `SequencerStateUtil.mapCommits` Translates shas contained in a `SequencerState` based on a mapping, for use in testing. --- node/lib/util/sequencer_state_util.js | 35 +++++++++++++++++ node/test/util/sequencer_state_util.js | 53 ++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/node/lib/util/sequencer_state_util.js b/node/lib/util/sequencer_state_util.js index 616796632..9e32be42b 100644 --- a/node/lib/util/sequencer_state_util.js +++ b/node/lib/util/sequencer_state_util.js @@ -258,3 +258,38 @@ exports.writeSequencerState = co.wrap(function *(gitDir, state) { yield fs.writeFile(path.join(root, CURRENT_COMMIT_FILE), "" + state.currentCommit); }); + +/** + * Return a new `SequencerState` object having the same value as the specified + * `sequencer`, but with each commit sha it contains replaced with the value it + * mapts to in the specified `commitMap`. The behavior is undefined unless + * every commit sha is mapped. + * + * @param {SequencerState} sequencer + * @param {Object} commitMap sha to sha + * @return {Sequencer} + */ +exports.mapCommits = function (sequencer, commitMap) { + assert.instanceOf(sequencer, SequencerState); + assert.isObject(commitMap); + + function map(sha) { + assert.property(commitMap, sha); + return commitMap[sha]; + } + + function mapCommitAndRef(old) { + return new CommitAndRef(map(old.sha), old.ref); + } + + const newOriginal = mapCommitAndRef(sequencer.originalHead); + const newTarget = mapCommitAndRef(sequencer.target); + const newCommits = sequencer.commits.map(map); + return new SequencerState({ + type: sequencer.type, + originalHead: newOriginal, + target: newTarget, + currentCommit: sequencer.currentCommit, + commits: newCommits, + }); +}; diff --git a/node/test/util/sequencer_state_util.js b/node/test/util/sequencer_state_util.js index cbd030e99..ac64cdc2d 100644 --- a/node/test/util/sequencer_state_util.js +++ b/node/test/util/sequencer_state_util.js @@ -309,4 +309,57 @@ describe("writeSequencerState", function () { assert.deepEqual(read, initial); })); }); +describe("mapCommits", function () { + const cases = { + "just one to map": { + sequencer: new SequencerState({ + type: SequencerState.TYPE.REBASE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("1", "foo"), + currentCommit: 0, + commits: ["1"], + }), + commitMap: { + "1": "2", + }, + expected: new SequencerState({ + type: SequencerState.TYPE.REBASE, + originalHead: new CommitAndRef("2", null), + target: new CommitAndRef("2", "foo"), + currentCommit: 0, + commits: ["2"], + }), + }, + "multiple": { + sequencer: new SequencerState({ + type: SequencerState.TYPE.REBASE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("2", "foo"), + currentCommit: 0, + commits: ["1", "3"], + }), + commitMap: { + "1": "2", + "2": "4", + "3": "8", + }, + expected: new SequencerState({ + type: SequencerState.TYPE.REBASE, + originalHead: new CommitAndRef("2", null), + target: new CommitAndRef("4", "foo"), + currentCommit: 0, + commits: ["2", "8"], + }), + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, function () { + const result = SequencerStateUtil.mapCommits(c.sequencer, + c.commitMap); + assert.instanceOf(result, SequencerState); + assert.deepEqual(result, c.expected); + }); + }); +}); }); From e1ce38558f5e2286add9d0c8589c155866bd8cb9 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 15 Mar 2018 16:54:48 -0400 Subject: [PATCH 066/402] Test support for `SequencerState` Including: * support in `RepoAST` * shorthand parser * `ReadRepoASTUtil` * `WriteRepoASTUtil` See card: https://github.com/twosigma/git-meta/projects/2#card-8117137 --- node/lib/util/read_repo_ast_util.js | 11 +++ node/lib/util/repo_ast.js | 66 ++++++++++----- node/lib/util/repo_ast_util.js | 39 +++++++++ node/lib/util/shorthand_parser_util.js | 81 +++++++++++++++++- node/lib/util/write_repo_ast_util.js | 8 ++ node/test/util/read_repo_ast_util.js | 65 +++++++++++++++ node/test/util/repo_ast.js | 102 +++++++++++++++++++++++ node/test/util/repo_ast_util.js | 105 ++++++++++++++++++++++++ node/test/util/shorthand_parser_util.js | 81 ++++++++++++++++++ node/test/util/write_repo_ast_util.js | 1 + 10 files changed, 539 insertions(+), 20 deletions(-) diff --git a/node/lib/util/read_repo_ast_util.js b/node/lib/util/read_repo_ast_util.js index 32a452c1f..d7d52ea01 100644 --- a/node/lib/util/read_repo_ast_util.js +++ b/node/lib/util/read_repo_ast_util.js @@ -46,6 +46,7 @@ const CherryPickFileUtil = require("./cherry_pick_file_util"); const RepoAST = require("./repo_ast"); const RebaseFileUtil = require("./rebase_file_util"); const MergeFileUtil = require("./merge_file_util"); +const SequencerStateUtil = require("./sequencer_state_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); /** @@ -556,6 +557,15 @@ exports.readRAST = co.wrap(function *(repo) { yield loadCommit(NodeGit.Oid.fromString(cherryPick.picked)); } + const sequencer = yield SequencerStateUtil.readSequencerState(repo.path()); + + if (null !== sequencer) { + yield loadCommit(NodeGit.Oid.fromString(sequencer.originalHead.sha)); + yield loadCommit(NodeGit.Oid.fromString(sequencer.target.sha)); + yield sequencer.commits.map( + sha => loadCommit(NodeGit.Oid.fromString(sha))); + } + return new RepoAST({ commits: commits, branches: branchTargets, @@ -570,6 +580,7 @@ exports.readRAST = co.wrap(function *(repo) { rebase: rebase, merge: merge, cherryPick: cherryPick, + sequencerState: sequencer, bare: bare, }); }); diff --git a/node/lib/util/repo_ast.js b/node/lib/util/repo_ast.js index 553a3ec55..fa3fb8b85 100644 --- a/node/lib/util/repo_ast.js +++ b/node/lib/util/repo_ast.js @@ -37,9 +37,10 @@ const assert = require("chai").assert; const deeper = require("deeper"); const deepCopy = require("deepcopy"); -const Rebase = require("./rebase"); -const Merge = require("./merge"); -const CherryPick = require("./cherry_pick"); +const Rebase = require("./rebase"); +const Merge = require("./merge"); +const CherryPick = require("./cherry_pick"); +const SequencerState = require("./sequencer_state"); /** * @class {Branch} @@ -380,21 +381,22 @@ class AST { * - if 'bare', `index` and `workdir` are empty, and `rebase` is null * - any conflicted path in the index has a value specified in the workdir * - * @param {Object} args - * @param {Object} [args.commits] - * @param {Object} [args.branches] - * @param {Object} [args.refs] - * @param {String|null} [args.head] - * @param {Boolean} [args.bare] - * @param {String|null} [args.currentBranchName] - * @param {Object} [args.remotes] - * @param {Object} [args.index] - * @param {Object} [args.workdir] - * @param {Object} [args.notes] - * @param {Object} [args.openSubmodules] - * @param {Rebase} [args.rebase] - * @param {Merge} [args.merge] - * @param {CherryPick} [args.cherryPick] + * @param {Object} args + * @param {Object} [args.commits] + * @param {Object} [args.branches] + * @param {Object} [args.refs] + * @param {String|null} [args.head] + * @param {Boolean} [args.bare] + * @param {String|null} [args.currentBranchName] + * @param {Object} [args.remotes] + * @param {Object} [args.index] + * @param {Object} [args.workdir] + * @param {Object} [args.notes] + * @param {Object} [args.openSubmodules] + * @param {Rebase} [args.rebase] + * @param {Merge} [args.merge] + * @param {CherryPick} [args.cherryPick] + * @param {SequencerState} [args.sequencerState] */ constructor(args) { if (undefined === args) { @@ -585,6 +587,22 @@ in commit ${id}.`); } } + this.d_sequencerState = null; + if ("sequencerState" in args) { + const sequencerState = args.sequencerState; + if (null !== sequencerState) { + assert.instanceOf(sequencerState, SequencerState); + assert.isFalse(this.d_bare); + checkAndTraverse(sequencerState.originalHead.sha, + "original head of sequencer"); + checkAndTraverse(sequencerState.target.sha, + "target commit of sequencer"); + sequencerState.commits.forEach( + sha => checkAndTraverse(sha, "sequencer commit")); + this.d_sequencerState = sequencerState; + } + } + // Validate that all commits have been reached. for (let key in commits) { @@ -771,6 +789,13 @@ in commit ${id}.`); return this.d_cherryPick; } + /** + * @property {SequencerState} null unless a sequence operation is ongoing + */ + get sequencerState() { + return this.d_sequencerState; + } + /** * Accumulate the specified `changes` into the specified `dest` map. A * non-null value in `changes` overrides any existing value in `dest`; a @@ -832,7 +857,9 @@ in commit ${id}.`); rebase: ("rebase" in args) ? args.rebase : this.d_rebase, merge: ("merge" in args) ? args.merge : this.d_merge, cherryPick: ("cherryPick" in args) ? - args.cherryPick : this.cherryPick, + args.cherryPick : this.d_cherryPick, + sequencerState: ("sequencerState" in args) ? + args.sequencerState: this.d_sequencerState, bare: ("bare" in args) ? args.bare : this.d_bare, }); } @@ -919,5 +946,6 @@ AST.Rebase = Rebase; AST.Merge = Merge; AST.CherryPick = CherryPick; AST.Remote = Remote; +AST.SequencerState = SequencerState; AST.Submodule = Submodule; module.exports = AST; diff --git a/node/lib/util/repo_ast_util.js b/node/lib/util/repo_ast_util.js index ecce55a96..4c38b3d84 100644 --- a/node/lib/util/repo_ast_util.js +++ b/node/lib/util/repo_ast_util.js @@ -41,6 +41,9 @@ const deeper = require("deeper"); const RepoAST = require("../util/repo_ast"); +const Sequencer = RepoAST.SequencerState; +const CommitAndRef = Sequencer.CommitAndRef; + // Begin module-local methods /** @@ -527,6 +530,23 @@ Expected cherry pick to be ${actual.cherryPick} but got \ ${expected.cherryPick}`); } + // Check sequencer + + if (null === actual.sequencerState && null !== expected.sequencerState) { + result.push("Missing sequencer."); + } + else if (null !== actual.sequencerState && + null === expected.sequencerState) { + result.push("Unexpected sequencer."); + } + else if (null !== actual.sequencerState && + !actual.sequencerState.equal(expected.sequencerState)) { + result.push(`\ +Expected sequencer to be ${actual.sequencerState} but got \ +${expected.sequencerState}`); + } + + return result; } @@ -694,6 +714,10 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { }); } + function mapCommitAndRef(car) { + return new CommitAndRef(mapCommitId(car.sha), car.ref); + } + // Copy and transform commit map. Have to transform the key (commit id) // and the commits themselves which also contain commit ids. @@ -777,6 +801,20 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { mapCommitId(cherryPick.picked)); } + let sequencer = ast.sequencerState; + if (null !== sequencer) { + const original = mapCommitAndRef(sequencer.originalHead); + const target = mapCommitAndRef(sequencer.target); + const commits = sequencer.commits.map(mapCommitId); + sequencer = new Sequencer({ + type: sequencer.type, + originalHead: original, + target: target, + commits: commits, + currentCommit: sequencer.currentCommit, + }); + } + return ast.copy({ commits: commits, branches: branches, @@ -791,6 +829,7 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { rebase: rebase, merge: merge, cherryPick: cherryPick, + sequencerState: sequencer, }); }; diff --git a/node/lib/util/shorthand_parser_util.js b/node/lib/util/shorthand_parser_util.js index 35ad7b2f7..741348f8e 100644 --- a/node/lib/util/shorthand_parser_util.js +++ b/node/lib/util/shorthand_parser_util.js @@ -31,9 +31,12 @@ "use strict"; const assert = require("chai").assert; + const RepoAST = require("../util/repo_ast"); const RepoASTUtil = require("../util/repo_ast_util"); +const SequencerState = RepoAST.SequencerState; + /** * @module {ShorthandParserUtil} * @@ -57,7 +60,8 @@ const RepoASTUtil = require("../util/repo_ast_util"); * base repo type = 'S' | 'B' | ('C') | 'A' | 'N' * override = | | | | * | | | | - * | | | + * | | | | + * * head = 'H='| nothing means detached * nothing = * commit = + @@ -79,6 +83,10 @@ const RepoASTUtil = require("../util/repo_ast_util"); * rebase = E,, * merge = M,, * cherry-pick = P, + * sequencer = Q' '' '' ' + * ' '(',')* + * sequencer type = C | M | R + * commit and ref = commit':'[] * index = I [,]* * workdir = W [,]* * open submodule = 'O'[' '('!')*] @@ -190,6 +198,10 @@ const RepoASTUtil = require("../util/repo_ast_util"); * as conflicted, having a base content of 'a', * "our" content as 'b', and "their" content as * 'c'. + * QR 1:refs/heads/master 8: 1 3,5,1 + * -- A sequencer is in progress that is a + * -- rebase. When the rebase started, HEAD + * -- was on `master`. * * Note that the "clone' type may not be used with single-repo ASTs, and the * url must map to the name of another repo. A cloned repository has the @@ -330,6 +342,24 @@ function copyOverrides(dest, source) { } } +/** + * Return the `CommitAndRef` object encoded in the specified `str`. + * @param {String} str + * @return {CommitAndRef} + */ +exports.parseCommitAndRef = function (str) { + const parts = str.split(":"); + assert.equal(2, parts.length, `malformed commit and ref: ${str}`); + const ref = "" === parts[1] ? null : parts[1]; + return new SequencerState.CommitAndRef(parts[0], ref); +}; + +const sequencerTypes = { + C: SequencerState.TYPE.CHERRY_PICK, + M: SequencerState.TYPE.MERGE, + R: SequencerState.TYPE.REBASE, +}; + /** * Return the result of merging the data in the specified `baseAST` with data * returned from `parseRepoShorthandRaw` into an object suitable for passing to @@ -357,6 +387,7 @@ function prepareASTArguments(baseAST, rawRepo) { rebase: baseAST.rebase, merge: baseAST.merge, cherryPick: baseAST.cherryPick, + sequencerState: baseAST.sequencerState, bare: baseAST.bare, }; @@ -481,6 +512,10 @@ function prepareASTArguments(baseAST, rawRepo) { resultArgs.cherryPick = rawRepo.cherryPick; } + if ("sequencerState" in rawRepo) { + resultArgs.sequencerState = rawRepo.sequencerState; + } + return resultArgs; } @@ -560,6 +595,7 @@ function parseOverrides(shorthand, begin, end, delimiter) { let rebase; let merge; let cherryPick; + let sequencer; /** * Parse a set of changes from the specified `begin` character to the @@ -966,6 +1002,40 @@ function parseOverrides(shorthand, begin, end, delimiter) { cherryPick = new RepoAST.CherryPick(parts[0], parts[1]); } + /** + * Parse the sequencer definition beginning at the specified `begin` and + * terminating at the specified `end`. + * + * @param {Number} begin + * @param {Number} end + */ + function parseSequencer(begin, end) { + if (begin === end) { + sequencer = null; + return; // RETURN + } + const sequencerDef = shorthand.substr(begin, end - begin); + const parts = sequencerDef.split(" "); + assert.equal(parts.length, + 5, + `Wrong number of sequencer parts in ${sequencerDef}`); + assert.property(sequencerTypes, parts[0], "sequencer type"); + const type = sequencerTypes[parts[0]]; + const originalHead = exports.parseCommitAndRef(parts[1]); + const target = exports.parseCommitAndRef(parts[2]); + const currentCommit = Number.parseInt(parts[3]); + assert(!Number.isNaN(currentCommit), + `bad current commit: ${currentCommit}`); + const commits = parts[4].split(","); + assert.notEqual(0, commits.length, `Bad commits: ${parts[4]}`); + sequencer = new SequencerState({ + type: type, + originalHead: originalHead, + target: target, + currentCommit: currentCommit, + commits: commits, + }); + } /** * Parse the override beginning at the specified `begin` and finishing at * the specified `end`. @@ -992,6 +1062,7 @@ function parseOverrides(shorthand, begin, end, delimiter) { case "W": return parseWorkdir; case "O": return parseOpenSubmodule; case "E": return parseRebase; + case "Q": return parseSequencer; default: assert.isNull(`Invalid override ${override}.`); break; @@ -1039,6 +1110,9 @@ function parseOverrides(shorthand, begin, end, delimiter) { if (undefined !== cherryPick) { result.cherryPick = cherryPick; } + if (undefined !== sequencer) { + result.sequencerState = sequencer; + } return result; } @@ -1303,6 +1377,11 @@ exports.parseMultiRepoShorthand = function (shorthand, existingRepos) { includeCommit(resultArgs.cherryPick.originalHead); includeCommit(resultArgs.cherryPick.picked); } + if (resultArgs.sequencerState) { + includeCommit(resultArgs.sequencerState.originalHead.sha); + includeCommit(resultArgs.sequencerState.target.sha); + resultArgs.sequencerState.commits.forEach(includeCommit); + } for (let remoteName in resultArgs.remotes) { const remote = resultArgs.remotes[remoteName]; for (let branch in remote.branches) { diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index f69910584..1d89b78cb 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -52,6 +52,7 @@ const MergeFileUtil = require("./merge_file_util"); const RebaseFileUtil = require("./rebase_file_util"); const RepoAST = require("./repo_ast"); const RepoASTUtil = require("./repo_ast_util"); +const SequencerStateUtil = require("./sequencer_state_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); const TestUtil = require("./test_util"); const TreeUtil = require("./tree_util"); @@ -491,6 +492,13 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { yield CherryPickFileUtil.writeCherryPick(repo.path(), cherryPick); } + // Write out sequencer state if there is one. + const sequencer = ast.sequencerState; + if (null !== sequencer) { + const mapped = SequencerStateUtil.mapCommits(sequencer, commitMap); + yield SequencerStateUtil.writeSequencerState(repo.path(), mapped); + } + // Set up the index. We render the current commit and apply the index // on top of it. diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index 23653d93f..9542e454a 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -46,9 +46,13 @@ const Rebase = require("../../lib/util/rebase"); const RepoAST = require("../../lib/util/repo_ast"); const ReadRepoASTUtil = require("../../lib/util/read_repo_ast_util"); const RepoASTUtil = require("../../lib/util/repo_ast_util"); +const SequencerState = require("../../lib/util/sequencer_state"); +const SequencerStateUtil = require("../../lib/util/sequencer_state_util"); const SubmoduleConfigUtil = require("../../lib/util/submodule_config_util"); const TestUtil = require("../../lib/util/test_util"); +const CommitAndRef = SequencerState.CommitAndRef; + // Test utilities /** @@ -1546,6 +1550,67 @@ describe("readRAST", function () { assert.deepEqual(actualCherryPick, cherryPick); })); + it("sequencer", co.wrap(function *() { + // Start out with a base repo having two branches, "master", and "foo", + // foo having one commit on top of master. + + const start = yield repoWithCommit(); + const r = start.repo; + + // Switch to master + + yield r.checkoutBranch("master"); + + const head = yield r.getHeadCommit(); + const sha = head.id().tostrS(); + + const sequencer = new SequencerState({ + type: SequencerState.TYPE.REBASE, + originalHead: new CommitAndRef(sha, "foo"), + target: new CommitAndRef(sha, "bar"), + currentCommit: 0, + commits: [sha], + }); + + const original = yield ReadRepoASTUtil.readRAST(r); + const expected = original.copy({ + sequencerState: sequencer, + }); + + yield SequencerStateUtil.writeSequencerState(r.path(), sequencer); + + const actual = yield ReadRepoASTUtil.readRAST(r); + + RepoASTUtil.assertEqualASTs(actual, expected); + })); + + it("sequencer - unreachable", co.wrap(function *() { + const r = yield TestUtil.createSimpleRepository(); + r.detachHead(); + const second = yield TestUtil.generateCommit(r); + const third = yield TestUtil.generateCommit(r); + const fourth = yield TestUtil.generateCommit(r); + + // Then begin a cherry-pick. + + const sequencer = new SequencerState({ + type: SequencerState.TYPE.REBASE, + originalHead: new CommitAndRef(second.id().tostrS(), "foo"), + target: new CommitAndRef(third.id().tostrS(), "bar"), + currentCommit: 0, + commits: [fourth.id().tostrS()], + }); + + yield SequencerStateUtil.writeSequencerState(r.path(), sequencer); + + // Remove the branches, making the commits reachable only from the + // rebase. + + const ast = yield ReadRepoASTUtil.readRAST(r); + const actualSequencer = ast.sequencerState; + assert.deepEqual(actualSequencer, sequencer); + })); + it("add subs again", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); let expected = yield astFromSimpleRepo(repo); diff --git a/node/test/util/repo_ast.js b/node/test/util/repo_ast.js index f23568d1c..abdb644da 100644 --- a/node/test/util/repo_ast.js +++ b/node/test/util/repo_ast.js @@ -35,6 +35,9 @@ const assert = require("chai").assert; const RepoAST = require("../../lib/util/repo_ast"); describe("RepoAST", function () { +const SequencerState = RepoAST.SequencerState; +const CommitAndRef = SequencerState.CommitAndRef; +const REBASE = SequencerState.TYPE.REBASE; describe("Branch", function () { it("breath", function () { @@ -275,6 +278,8 @@ describe("RepoAST", function () { emerge: ("merge" in expected) ? expected.merge : null, echerryPick: ("cherryPick" in expected) ? expected.cherryPick : null, + esequencerState: ("sequencerState" in expected) ? + expected.sequencerState : null, fails : fails, }; } @@ -290,6 +295,7 @@ describe("RepoAST", function () { rebase: null, merge: null, cherryPick: null, + sequencerState: null, bare: false, }, undefined, @@ -325,6 +331,18 @@ describe("RepoAST", function () { commits: {"1": c1 }, head: "1", }, undefined, true), + "bad bare with sequencer": m({ + bare: true, + sequencerState: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("1", null), + commits: ["1"], + currentCommit: 0, + }), + commits: {"1": c1 }, + head: "1", + }, undefined, true), "branchCommit": m({ commits: {"1":c1, "2": cWithPar}, branches: {"master": new RepoAST.Branch("2", null) }, @@ -607,6 +625,35 @@ describe("RepoAST", function () { head: "1", cherryPick: new CherryPick("1", "1"), }), + "with sequencer state": m({ + commits: { + "1": new Commit(), + "2": new Commit(), + "3": new Commit(), + }, + head: "1", + sequencerState: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("3", null), + target: new CommitAndRef("3", null), + commits: ["2", "1"], + currentCommit: 1, + }), + }, { + commits: { + "1": new Commit(), + "2": new Commit(), + "3": new Commit(), + }, + head: "1", + sequencerState: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("3", null), + target: new CommitAndRef("3", null), + commits: ["2", "1"], + currentCommit: 1, + }), + }), "with merge specific commits": m({ commits: { "1": new Commit(), @@ -637,9 +684,47 @@ describe("RepoAST", function () { head: "1", cherryPick: new CherryPick("2", "2"), }), + "with sequencer specific commits": m({ + commits: { + "1": new Commit(), + "2": new Commit(), + "3": new Commit(), + }, + head: "1", + sequencerState: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("3", null), + target: new CommitAndRef("3", null), + commits: ["2", "1"], + currentCommit: 1, + }), + }, { + commits: { + "1": new Commit(), + "2": new Commit(), + "3": new Commit(), + }, + head: "1", + sequencerState: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("3", null), + target: new CommitAndRef("3", null), + commits: ["2", "1"], + currentCommit: 1, + }), + }), "bad cherry-pick": m({ cherryPick: new CherryPick("1", "1"), }, undefined, true), + "bad sequencer": m({ + sequencerState: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("foo", null), + target: new CommitAndRef("bar", null), + commits: ["2", "1"], + currentCommit: 1, + }), + }, undefined, true), }; Object.keys(cases).forEach(caseName => { it(caseName, function () { @@ -664,6 +749,7 @@ describe("RepoAST", function () { assert.deepEqual(obj.rebase, c.erebase); assert.deepEqual(obj.merge, c.emerge); assert.deepEqual(obj.cherryPick, c.echerryPick); + assert.deepEqual(obj.sequencerState, c.esequencerState); assert.equal(obj.bare, c.ebare); if (c.input) { @@ -815,6 +901,13 @@ describe("RepoAST", function () { rebase: new Rebase("hello", "1", "1"), merge: new Merge("hello", "1", "1"), cherryPick: new CherryPick("1", "1"), + sequencerState: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("1", null), + commits: ["1"], + currentCommit: 0, + }), bare: false, }); const newArgs = { @@ -829,6 +922,13 @@ describe("RepoAST", function () { rebase: new Rebase("hello world", "2", "2"), merge: new Merge("hello world", "2", "2"), cherryPick: new CherryPick("2", "2"), + sequencerState: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("2", "refs/heads/master"), + target: new CommitAndRef("2", null), + commits: ["2"], + currentCommit: 0, + }), bare: false, }; const cases = { @@ -848,6 +948,7 @@ describe("RepoAST", function () { rebase: null, merge: null, cherryPick: null, + sequencerState: null, }, e: new RepoAST({ commits: { "1": new RepoAST.Commit()}, @@ -875,6 +976,7 @@ describe("RepoAST", function () { assert.deepEqual(obj.rebase, c.e.rebase); assert.deepEqual(obj.merge, c.e.merge); assert.deepEqual(obj.cherryPick, c.e.cherryPick); + assert.deepEqual(obj.sequencerState, c.e.sequencerState); assert.equal(obj.bare, c.e.bare); }); }); diff --git a/node/test/util/repo_ast_util.js b/node/test/util/repo_ast_util.js index de0cd1d29..6c8e55fbf 100644 --- a/node/test/util/repo_ast_util.js +++ b/node/test/util/repo_ast_util.js @@ -37,6 +37,9 @@ const RepoASTUtil = require("../../lib/util/repo_ast_util"); describe("RepoAstUtil", function () { const Conflict = RepoAST.Conflict; + const Sequencer = RepoAST.SequencerState; + const CommitAndRef = Sequencer.CommitAndRef; + const MERGE = Sequencer.TYPE.MERGE; const Submodule = RepoAST.Submodule; describe("assertEqualCommits", function () { const Commit = RepoAST.Commit; @@ -171,6 +174,13 @@ describe("RepoAstUtil", function () { rebase: new Rebase("foo", "1", "1"), merge: new Merge("foo", "1", "1"), cherryPick: new CherryPick("1", "1"), + sequencerState: new Sequencer({ + type: MERGE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("1", "foo/bar"), + commits: ["1"], + currentCommit: 0, + }), bare: false, }), expected: new AST({ @@ -186,6 +196,13 @@ describe("RepoAstUtil", function () { rebase: new Rebase("foo", "1", "1"), merge: new Merge("foo", "1", "1"), cherryPick: new CherryPick("1", "1"), + sequencerState: new Sequencer({ + type: MERGE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("1", "foo/bar"), + commits: ["1"], + currentCommit: 0, + }), bare: false, }), }, @@ -638,6 +655,69 @@ describe("RepoAstUtil", function () { }), fails: true, }, + "missing sequencer": { + actual: new AST({ + commits: { "1": aCommit}, + head: "1", + sequencerState: new Sequencer({ + type: MERGE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("1", "foo/bar"), + commits: ["1"], + currentCommit: 0, + }), + }), + expected: new AST({ + commits: { "1": aCommit}, + head: "1", + }), + fails: true, + }, + "unexpected sequencer": { + actual: new AST({ + commits: { "1": aCommit}, + head: "1", + }), + expected: new AST({ + commits: { "1": aCommit}, + head: "1", + sequencerState: new Sequencer({ + type: MERGE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("1", "foo/bar"), + commits: ["1"], + currentCommit: 0, + }), + }), + fails: true, + }, + "wrong sequencer": { + actual: new AST({ + commits: { "1": aCommit, "2": aCommit}, + head: "1", + branches: { master: new RepoAST.Branch("2", null), }, + sequencerState: new Sequencer({ + type: MERGE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("1", "foo/bar"), + commits: ["1"], + currentCommit: 0, + }), + }), + expected: new AST({ + commits: { "1": aCommit, "2": aCommit}, + head: "1", + branches: { master: new RepoAST.Branch("2", null), }, + sequencerState: new Sequencer({ + type: MERGE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("1", "foo/baz"), + commits: ["1"], + currentCommit: 0, + }), + }), + fails: true, + }, "same Conflict": { actual: new AST({ index: { @@ -1126,6 +1206,31 @@ describe("RepoAstUtil", function () { cherryPick: new CherryPick("1", "1"), }), }, + "sequencer": { + i: new RepoAST({ + commits: { "1": c1 }, + head: "1", + sequencerState: new Sequencer({ + type: MERGE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("1", "foo/bar"), + commits: ["1"], + currentCommit: 0, + }), + }), + m: { "1": "2"}, + e: new RepoAST({ + commits: { "2": c1 }, + head: "2", + sequencerState: new Sequencer({ + type: MERGE, + originalHead: new CommitAndRef("2", null), + target: new CommitAndRef("2", "foo/bar"), + commits: ["2"], + currentCommit: 0, + }), + }), + }, }; Object.keys(cases).forEach(caseName => { it(caseName, function () { diff --git a/node/test/util/shorthand_parser_util.js b/node/test/util/shorthand_parser_util.js index 35d2e09b9..bc84b3e58 100644 --- a/node/test/util/shorthand_parser_util.js +++ b/node/test/util/shorthand_parser_util.js @@ -37,6 +37,28 @@ const RepoASTUtil = require("../../lib/util/repo_ast_util"); const ShorthandParserUtil = require("../../lib/util/shorthand_parser_util"); describe("ShorthandParserUtil", function () { + const SequencerState = RepoAST.SequencerState; + const CommitAndRef = SequencerState.CommitAndRef; + describe("parseCommitAndRef", function () { + const cases = { + "without ref": { + input: "foo:", + expected: new CommitAndRef("foo", null), + }, + "with ref": { + input: "bar:baz", + expected: new CommitAndRef("bar", "baz"), + }, + }; + Object.keys(cases).forEach(caseName => { + it(caseName, function () { + const c = cases[caseName]; + const result = ShorthandParserUtil.parseCommitAndRef(c.input); + assert.instanceOf(result, CommitAndRef); + assert.deepEqual(result, c.expected); + }); + }); + }); describe("findSeparator", function () { const cases = { missing: { @@ -648,6 +670,52 @@ describe("ShorthandParserUtil", function () { cherryPick: null, }), }, + "sequencer null": { + i: "S:Q", + e: m({ + type: "S", + sequencerState: null, + }), + }, + "sequencer with cherry": { + i: "S:QC 1:foo 3: 2 a,b,c", + e: m({ + type: "S", + sequencerState: new SequencerState({ + type: SequencerState.TYPE.CHERRY_PICK, + originalHead: new CommitAndRef("1", "foo"), + target: new CommitAndRef("3", null), + currentCommit: 2, + commits: ["a", "b", "c"], + }), + }), + }, + "sequencer with merge": { + i: "S:QM 1:foo 3: 2 a,b,c", + e: m({ + type: "S", + sequencerState: new SequencerState({ + type: SequencerState.TYPE.MERGE, + originalHead: new CommitAndRef("1", "foo"), + target: new CommitAndRef("3", null), + currentCommit: 2, + commits: ["a", "b", "c"], + }), + }), + }, + "sequencer with rebase": { + i: "S:QR 1:foo 3: 2 a,b,c", + e: m({ + type: "S", + sequencerState: new SequencerState({ + type: SequencerState.TYPE.REBASE, + originalHead: new CommitAndRef("1", "foo"), + target: new CommitAndRef("3", null), + currentCommit: 2, + commits: ["a", "b", "c"], + }), + }), + }, "new submodule": { i: "S:I x=Sfoo:;Ox", e: m({ @@ -686,6 +754,7 @@ describe("ShorthandParserUtil", function () { assert.deepEqual(r.rebase, e.rebase); assert.deepEqual(r.merge, e.merge); assert.deepEqual(r.cherryPick, e.cherryPick); + assert.deepEqual(r.sequencerState, e.sequencerState); }); }); }); @@ -869,6 +938,18 @@ describe("ShorthandParserUtil", function () { cherryPick: new RepoAST.CherryPick("1", "1"), }), }, + "sequencer": { + i: "S:QM 1:foo 1: 0 1", + e: S.copy({ + sequencerState: new SequencerState({ + type: SequencerState.TYPE.MERGE, + originalHead: new CommitAndRef("1", "foo"), + target: new CommitAndRef("1", null), + currentCommit: 0, + commits: ["1"], + }), + }), + }, }; Object.keys(cases).forEach(caseName => { it(caseName, function () { diff --git a/node/test/util/write_repo_ast_util.js b/node/test/util/write_repo_ast_util.js index 0089b6395..76490e9c8 100644 --- a/node/test/util/write_repo_ast_util.js +++ b/node/test/util/write_repo_ast_util.js @@ -344,6 +344,7 @@ S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q;H=3", }, "with in-progress merge": "S:Mhello,1,1", "with in-progress cherry-pick": "S:P1,1", + "with in-progress sequencer": "S:QR 1:foo 1:bar 0 1", "headless": { input: new RepoAST(), expected: new RepoAST(), From 9c0c6d12de9317d52627ce9994645cd0e6c008eb Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 23 Mar 2018 10:48:29 -0400 Subject: [PATCH 067/402] Add `SequencerState` to `RepoStatus` --- node/lib/util/repo_status.js | 22 ++++++++++++++++-- node/test/util/repo_status.js | 43 +++++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/node/lib/util/repo_status.js b/node/lib/util/repo_status.js index 2daa42ae5..0e222acbd 100644 --- a/node/lib/util/repo_status.js +++ b/node/lib/util/repo_status.js @@ -34,6 +34,7 @@ const assert = require("chai").assert; const Rebase = require("./rebase"); const Merge = require("./merge"); const CherryPick = require("./cherry_pick"); +const SequencerState = require("./sequencer_state"); /** * This modules defines the type `RepoStatus`, used to describe modifications @@ -448,6 +449,8 @@ class RepoStatus { * @param {Rebase} [args.rebase] rebase, if one is in progress * @param {Merge} [args.merge] merge, if one is in progress * @param {CherryPick} [args.cherryPick] cherry-pick, if one is in progress + * @param {SequencerState} + * [args.sequencerState] state of sequencer */ constructor(args) { if (undefined === args) { @@ -464,6 +467,7 @@ class RepoStatus { this.d_rebase = null; this.d_merge = null; this.d_cherryPick = null; + this.d_sequencerState = null; if ("currentBranchName" in args) { if (null !== args.currentBranchName) { @@ -516,7 +520,6 @@ class RepoStatus { } this.d_merge = merge; } - if ("cherryPick" in args) { const cherryPick = args.cherryPick; if (null !== cherryPick) { @@ -524,6 +527,13 @@ class RepoStatus { } this.d_cherryPick = cherryPick; } + if ("sequencerState" in args) { + const sequencerState = args.sequencerState; + if (null !== sequencerState) { + assert.instanceOf(sequencerState, SequencerState); + } + this.d_sequencerState = sequencerState; + } Object.freeze(this); } @@ -703,12 +713,18 @@ class RepoStatus { } /** - * @property {CherryPick} cherryPick if~null, state of cherryPick + * @property {CherryPick} cherryPick if ~null, state of cherryPick */ get cherryPick() { return this.d_cherryPick; } + /** + * @property {SequencerState} sequencerState if ~null, state of sequencer + */ + get sequencerState() { + return this.d_sequencerState; + } /** * Return a new `RepoStatus` object having the same value as this one, but * with replacing properties defined in the specified `args`. @@ -736,6 +752,8 @@ class RepoStatus { merge: ("merge" in args) ? args.merge : this.d_merge, cherryPick: ("cherryPick" in args) ? args.cherryPick : this.d_cherryPick, + sequencerState: ("sequencerState" in args) ? + args.sequencerState : this.d_sequencerState, }); } } diff --git a/node/test/util/repo_status.js b/node/test/util/repo_status.js index afdef90a9..ebe90482c 100644 --- a/node/test/util/repo_status.js +++ b/node/test/util/repo_status.js @@ -32,12 +32,16 @@ const assert = require("chai").assert; -const Merge = require("../../lib/util/merge"); -const CherryPick = require("../../lib/util/cherry_pick"); -const Rebase = require("../../lib/util/rebase"); -const RepoStatus = require("../../lib/util/repo_status"); +const Merge = require("../../lib/util/merge"); +const CherryPick = require("../../lib/util/cherry_pick"); +const Rebase = require("../../lib/util/rebase"); +const RepoStatus = require("../../lib/util/repo_status"); +const SequencerState = require("../../lib/util/sequencer_state"); describe("RepoStatus", function () { +const CommitAndRef = SequencerState.CommitAndRef; +const MERGE = SequencerState.TYPE.MERGE; + const FILESTATUS = RepoStatus.FILESTATUS; const Submodule = RepoStatus.Submodule; const Commit = Submodule.Commit; @@ -479,6 +483,7 @@ describe("RepoStatus", function () { rebase: null, merge: null, cherryPick: null, + sequencerState: null, }; return Object.assign(result, args); } @@ -506,6 +511,13 @@ describe("RepoStatus", function () { rebase: new Rebase("foo", "1", "2"), merge: new Merge("baz", "2", "1"), cherryPick: new CherryPick("2", "1"), + sequencerState: new SequencerState({ + type: MERGE, + originalHead: new CommitAndRef("foo", null), + target: new CommitAndRef("bar", "baz"), + commits: ["2", "1"], + currentCommit: 1, + }), }, e: m({ currentBranchName: "foo", @@ -521,6 +533,13 @@ describe("RepoStatus", function () { rebase: new Rebase("foo", "1", "2"), merge: new Merge("baz", "2", "1"), cherryPick: new CherryPick("2", "1"), + sequencerState: new SequencerState({ + type: MERGE, + originalHead: new CommitAndRef("foo", null), + target: new CommitAndRef("bar", "baz"), + commits: ["2", "1"], + currentCommit: 1, + }), }), } }; @@ -538,6 +557,7 @@ describe("RepoStatus", function () { assert.deepEqual(result.rebase, c.e.rebase); assert.deepEqual(result.merge, c.e.merge); assert.deepEqual(result.cherryPick, c.e.cherryPick); + assert.deepEqual(result.sequencerState, c.e.sequencerState); }); }); @@ -1042,6 +1062,13 @@ describe("RepoStatus", function () { rebase: new Rebase("2", "4", "b"), merge: new Merge("hah", "1", "1"), cherryPick: new CherryPick("1", "1"), + sequencerState: new SequencerState({ + type: MERGE, + originalHead: new CommitAndRef("foo", null), + target: new CommitAndRef("bar", "baz"), + commits: ["2", "1"], + currentCommit: 1, + }), }); const anotherStat = new RepoStatus({ currentBranchName: "fo", @@ -1054,6 +1081,13 @@ describe("RepoStatus", function () { rebase: new Rebase("a", "4", "b"), merge: new Merge("a", "2", "2"), cherryPick: new CherryPick("a", "2"), + sequencerState: new SequencerState({ + type: MERGE, + originalHead: new CommitAndRef("foo", null), + target: new CommitAndRef("flim", "flam"), + commits: ["3", "4"], + currentCommit: 1, + }), }); it("simple, no args", function () { const newStat = stat.copy(); @@ -1073,6 +1107,7 @@ describe("RepoStatus", function () { rebase: anotherStat.rebase, merge: anotherStat.merge, cherryPick: anotherStat.cherryPick, + sequencerState: anotherStat.sequencerState, }); assert.deepEqual(newStat, anotherStat); }); From 27c39ddde172035b16f92c8824fc89f502bad119 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 23 Mar 2018 10:48:44 -0400 Subject: [PATCH 068/402] Support for printing SequencerState in RepoStatus --- node/lib/util/print_status_util.js | 38 +++++++++++++++++ node/test/util/print_status_util.js | 65 +++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/node/lib/util/print_status_util.js b/node/lib/util/print_status_util.js index cba073ba7..fbb33795b 100644 --- a/node/lib/util/print_status_util.js +++ b/node/lib/util/print_status_util.js @@ -43,6 +43,7 @@ const path = require("path"); const GitUtil = require("./git_util"); const Merge = require("./merge"); const CherryPick = require("./cherry_pick"); +const SequencerState = require("./sequencer_state"); const Rebase = require("./rebase"); const RepoStatus = require("./repo_status"); @@ -397,6 +398,39 @@ A cherry-pick is in progress. `; } +/** + * Return the command to which the specified sequencer `type` corresponds. + * + * @param {SequencerState.TYPE} type + * @return {String} + */ +exports.getSequencerCommand = function (type) { + const TYPE = SequencerState.TYPE; + switch (type) { + case TYPE.CHERRY_PICK: return "cherry-pick"; + case TYPE.MERGE: return "merge"; + case TYPE.REBASE: return "rebase"; + default: assert(false, "shouldn't get here"); break; + } +}; + +/** + * Return a message describing the specified `sequencer`. + * + * @param {SequencerState} sequencer + * @return {String} + */ +exports.printSequencer = function (sequencer) { + assert.instanceOf(sequencer, SequencerState); + const command = exports.getSequencerCommand(sequencer.type); + return `\ +A ${command} is in progress. + (after resolving conflicts mark the corrected paths + with 'git meta add', then run "git meta ${command} --continue") + (use "git meta ${command} --abort" to check out the original branch) +`; +}; + /** * Return a message describing the state of the current branch in the specified * `status`. @@ -440,6 +474,10 @@ exports.printRepoStatus = function (status, cwd) { result += printCherryPick(status.cherryPick); } + if (null !== status.sequencerState) { + result += exports.printSequencer(status.sequencerState); + } + let changes = ""; const fileStatuses = exports.accumulateStatus(status); const staged = fileStatuses.staged; diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js index 3de16dbf1..fdab3dd37 100644 --- a/node/test/util/print_status_util.js +++ b/node/test/util/print_status_util.js @@ -39,8 +39,11 @@ const Merge = require("../../lib/util/merge"); const CherryPick = require("../../lib/util/cherry_pick"); const RepoStatus = require("../../lib/util/repo_status"); const PrintStatusUtil = require("../../lib/util/print_status_util"); +const SequencerState = require("../../lib/util/sequencer_state"); describe("PrintStatusUtil", function () { + const CommitAndRef = SequencerState.CommitAndRef; + const TYPE = SequencerState.TYPE; const FILEMODE = NodeGit.TreeEntry.FILEMODE; const BLOB = FILEMODE.BLOB; const FILESTATUS = RepoStatus.FILESTATUS; @@ -661,6 +664,48 @@ describe("PrintStatusUtil", function () { }); }); + describe("getSequencerCommand", function () { + const cases = { + "merge": { + input: TYPE.MERGE, + expected: "merge", + }, + "rebase": { + input: TYPE.REBASE, + expected: "rebase", + }, + "cherry-pick": { + input: TYPE.CHERRY_PICK, + expected: "cherry-pick", + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, function () { + const result = PrintStatusUtil.getSequencerCommand(c.input); + assert.equal(result, c.expected); + }); + }); + }); + + it("printSequencer", function () { + const state = new SequencerState({ + type: TYPE.MERGE, + originalHead: new CommitAndRef("foo", null), + target: new CommitAndRef("bar", "baz"), + commits: ["2", "1"], + currentCommit: 1, + }); + const expected = `\ +A merge is in progress. + (after resolving conflicts mark the corrected paths + with 'git meta add', then run "git meta merge --continue") + (use "git meta merge --abort" to check out the original branch) +`; + const result = PrintStatusUtil.printSequencer(state); + assert.deepEqual(result.split("\n"), expected.split("\n")); + }); + describe("printCurrentBranch", function () { const cases = { "normal": { @@ -731,6 +776,26 @@ A cherry-pick is in progress. with 'git meta add', then run "git meta cherry-pick --continue") (use "git meta cherry-pick --abort" to check out the original branch) nothing to commit, working tree clean +`, + }, + "sequencer": { + input: new RepoStatus({ + currentBranchName: "master", + sequencerState: new SequencerState({ + type: TYPE.REBASE, + originalHead: new CommitAndRef("foo", null), + target: new CommitAndRef("bar", "baz"), + commits: ["2", "1"], + currentCommit: 1, + }), + }), + exact: `\ +On branch ${colors.green("master")}. +A rebase is in progress. + (after resolving conflicts mark the corrected paths + with 'git meta add', then run "git meta rebase --continue") + (use "git meta rebase --abort" to check out the original branch) +nothing to commit, working tree clean `, }, "rebase": { From f50706fe9cc4091f80a0710cfee2c701c08959af Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 23 Mar 2018 14:38:26 -0400 Subject: [PATCH 069/402] Better documented sequencer shorthand example --- node/lib/util/shorthand_parser_util.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/node/lib/util/shorthand_parser_util.js b/node/lib/util/shorthand_parser_util.js index 741348f8e..8b1911d48 100644 --- a/node/lib/util/shorthand_parser_util.js +++ b/node/lib/util/shorthand_parser_util.js @@ -198,10 +198,15 @@ const SequencerState = RepoAST.SequencerState; * as conflicted, having a base content of 'a', * "our" content as 'b', and "their" content as * 'c'. - * QR 1:refs/heads/master 8: 1 3,5,1 + * QR a:refs/heads/master q: 1 c,d,a * -- A sequencer is in progress that is a * -- rebase. When the rebase started, HEAD - * -- was on `master`. + * -- was on `master` pointing to commit "a". The + * -- commit being rebased to is "q", and it was + * -- not referenced through a ref name. The + * -- list of commits to be rebased are "c", "d", + * -- and "a", and we're currently on the + * -- second commit, at index 1: "d". * * Note that the "clone' type may not be used with single-repo ASTs, and the * url must map to the name of another repo. A cloned repository has the From dde2d0e146525faf9efa2dc9a53fcbfdf11edb59 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 23 Mar 2018 11:56:14 -0400 Subject: [PATCH 070/402] Support for SequencerState in StatusUtil --- node/lib/util/print_status_util.js | 2 +- node/lib/util/status_util.js | 15 +++++++++++ node/test/util/status_util.js | 43 ++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/node/lib/util/print_status_util.js b/node/lib/util/print_status_util.js index fbb33795b..1aa9da459 100644 --- a/node/lib/util/print_status_util.js +++ b/node/lib/util/print_status_util.js @@ -410,7 +410,7 @@ exports.getSequencerCommand = function (type) { case TYPE.CHERRY_PICK: return "cherry-pick"; case TYPE.MERGE: return "merge"; case TYPE.REBASE: return "rebase"; - default: assert(false, "shouldn't get here"); break; + default: assert(false, `unhandled sequencer type: ${type}`); } }; diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 2547f9464..79d8cb65b 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -50,6 +50,7 @@ const PrintStatusUtil = require("./print_status_util"); const Rebase = require("./rebase"); const RebaseFileUtil = require("./rebase_file_util"); const RepoStatus = require("./repo_status"); +const SequencerStateUtil = require("./sequencer_state_util"); const SubmoduleUtil = require("./submodule_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); const UserError = require("./user_error"); @@ -207,6 +208,10 @@ exports.remapRepoStatus = function (status, commitMap, urlMap) { cherryPick: status.cherryPick === null ? null : remapCherryPick(status.cherryPick, commitMap), + sequencerState: status.sequencerState === null ? + null : + SequencerStateUtil.mapCommits(status.sequencerState, + commitMap), }); }; @@ -496,6 +501,8 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { args.merge = yield MergeFileUtil.readMerge(repo.path()); args.cherryPick = yield CherryPickFileUtil.readCherryPick(repo.path()); + args.sequencerState = + yield SequencerStateUtil.readSequencerState(repo.path()); if (options.showMetaChanges && !repo.isBare()) { const head = yield repo.getHeadCommit(); @@ -663,4 +670,12 @@ Before proceeding, you must complete the cherry-pick in progress (by running Please resolve outstanding conflicts before proceeding: ${PrintStatusUtil.printRepoStatus(status, "")}`); } + if (null !== status.sequencerState) { + const command = + PrintStatusUtil.getSequencerCommand(status.sequencerState.type); + throw new UserError(`\ +Before proceeding, you must complete the ${command} in progress (by running +'git meta ${command} --continue') or abort it (by running +'git meta ${command} --abort').`); + } }; diff --git a/node/test/util/status_util.js b/node/test/util/status_util.js index 629839a81..4b1d2d594 100644 --- a/node/test/util/status_util.js +++ b/node/test/util/status_util.js @@ -41,6 +41,7 @@ const Rebase = require("../../lib/util/rebase"); const RepoAST = require("../../lib/util/repo_ast"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); const RepoStatus = require("../../lib/util/repo_status"); +const SequencerState = require("../../lib/util/sequencer_state"); const StatusUtil = require("../../lib/util/status_util"); const SubmoduleUtil = require("../../lib/util/submodule_util"); const SubmoduleConfigUtil = require("../../lib/util/submodule_config_util"); @@ -49,6 +50,8 @@ const UserError = require("../../lib/util/user_error"); // test utilities describe("StatusUtil", function () { + const CommitAndRef = SequencerState.CommitAndRef; + const TYPE = SequencerState.TYPE; const FILEMODE = NodeGit.TreeEntry.FILEMODE; const BLOB = FILEMODE.BLOB; const FILESTATUS = RepoStatus.FILESTATUS; @@ -113,6 +116,13 @@ describe("StatusUtil", function () { rebase: new Rebase("foo", "1", "1"), merge: new Merge("foo", "1", "1"), cherryPick: new CherryPick("1", "1"), + sequencerState: new SequencerState({ + type: TYPE.MERGE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("1", "baz"), + commits: ["1"], + currentCommit: 0, + }), }), commitMap: { "1": "3"}, urlMap: {}, @@ -124,6 +134,13 @@ describe("StatusUtil", function () { rebase: new Rebase("foo", "3", "3"), merge: new Merge("foo", "3", "3"), cherryPick: new CherryPick("3", "3"), + sequencerState: new SequencerState({ + type: TYPE.MERGE, + originalHead: new CommitAndRef("3", null), + target: new CommitAndRef("3", "baz"), + commits: ["3"], + currentCommit: 0, + }), }), }, "with a sub": { @@ -584,6 +601,20 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, cherryPick: new CherryPick("2", "3"), }), }, + "sequencer": { + state: "x=S:C2-1;C3-1;Bfoo=3;Bmaster=2;QM 1: 2:foo 1 2,3", + expected: new RepoStatus({ + headCommit: "2", + currentBranchName: "master", + sequencerState: new SequencerState({ + type: TYPE.MERGE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("2", "foo"), + commits: ["2", "3"], + currentCommit: 1, + }), + }), + }, "staged change": { state: "x=S:I README.md=whoohoo", options: { @@ -900,6 +931,18 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, }), fails: true, }, + "sequencer": { + input: new RepoStatus({ + sequencerState: new SequencerState({ + type: TYPE.MERGE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("1", "baz"), + commits: ["1"], + currentCommit: 0, + }), + }), + fails: true, + }, }; Object.keys(cases).forEach(caseName => { it(caseName, function () { From 1e1b53439f2a92586d35064f005ef191ac6cfd2e Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 23 Mar 2018 16:56:52 -0400 Subject: [PATCH 071/402] Move merge_util to use sequencer Addresses note: https://github.com/twosigma/git-meta/projects/2#card-8117756 --- node/lib/util/merge.js | 86 ------------------ node/lib/util/merge_file_util.js | 111 ----------------------- node/lib/util/merge_util.js | 104 ++++++++++++++------- node/lib/util/print_status_util.js | 21 ----- node/lib/util/read_repo_ast_util.js | 9 -- node/lib/util/repo_ast.js | 23 ----- node/lib/util/repo_ast_util.js | 35 +------ node/lib/util/repo_status.js | 18 ---- node/lib/util/sequencer_state.js | 21 ++++- node/lib/util/sequencer_state_util.js | 10 +- node/lib/util/shorthand_parser_util.js | 64 ++++--------- node/lib/util/status_util.js | 36 +------- node/lib/util/write_repo_ast_util.js | 14 --- node/test/util/merge.js | 46 ---------- node/test/util/merge_file_util.js | 86 ------------------ node/test/util/merge_util.js | 45 ++++----- node/test/util/print_status_util.js | 15 --- node/test/util/read_repo_ast_util.js | 51 ----------- node/test/util/repo_ast.js | 58 ------------ node/test/util/repo_ast_util.js | 116 +----------------------- node/test/util/repo_status.js | 8 -- node/test/util/sequencer_state.js | 28 +++++- node/test/util/sequencer_state_util.js | 41 +++++++++ node/test/util/shorthand_parser_util.js | 88 +++--------------- node/test/util/status_util.js | 17 ---- node/test/util/write_repo_ast_util.js | 2 +- 26 files changed, 227 insertions(+), 926 deletions(-) delete mode 100644 node/lib/util/merge.js delete mode 100644 node/lib/util/merge_file_util.js delete mode 100644 node/test/util/merge.js delete mode 100644 node/test/util/merge_file_util.js diff --git a/node/lib/util/merge.js b/node/lib/util/merge.js deleted file mode 100644 index 9390c18d4..000000000 --- a/node/lib/util/merge.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2017, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -const assert = require("chai").assert; - -/** - * This module defines the `Merge` value-semantic type. - */ - -/** - * @class Merge - * - * This class represents the state of a merge. - */ -class Merge { - - /** - * Create a new `Merge` object. - * - * @param {String} message - * @param {String} originalHead - * @param {String} mergeHead - */ - constructor(message, originalHead, mergeHead) { - assert.isString(message); - assert.isString(originalHead); - assert.isString(mergeHead); - - this.d_message = message; - this.d_originalHead = originalHead; - this.d_mergeHead = mergeHead; - Object.freeze(this); - } - - /** - * @property {String} message commit message started with the merge - */ - get message() { - return this.d_message; - } - - /** - * @property {String} originalHead HEAD commit when merge started - */ - get originalHead() { - return this.d_originalHead; - } - - /** - * @property {String} mergeHead target commit of merge - */ - get mergeHead() { - return this.d_mergeHead; - } -} - -module.exports = Merge; diff --git a/node/lib/util/merge_file_util.js b/node/lib/util/merge_file_util.js deleted file mode 100644 index 78bd10385..000000000 --- a/node/lib/util/merge_file_util.js +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) 2017, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -const assert = require("chai").assert; -const co = require("co"); -const fs = require("fs-promise"); -const path = require("path"); -const rimraf = require("rimraf"); - -const Merge = require("./merge"); - -/** - * This module contains methods for accessing files that pertain to in-progress - * merges. - */ - -const metaMergeDir = "META_MERGE"; -const messageFile = "MSG"; -const originalHeadFile = "ORIG_HEAD"; -const mergeHeadFile = "MERGE_HEAD"; - -/** - * Return the `Merge` object in the specified `gitDir`, if one exists, or null - * if there is no merge in progress. - * - * @param {String} gitDir - * @return {String|null} - */ -exports.readMerge = co.wrap(function *(gitDir) { - assert.isString(gitDir); - let message; - let originalHead; - let mergeHead; - const root = path.join(gitDir, metaMergeDir); - try { - message = yield fs.readFile(path.join(root, messageFile), - "utf8"); - originalHead = yield fs.readFile(path.join(root, originalHeadFile), - "utf8"); - mergeHead = yield fs.readFile(path.join(root, mergeHeadFile), "utf8"); - } - catch (e) { - return null; - } - return new Merge(message, - originalHead.split("\n")[0], - mergeHead.split("\n")[0]); -}); - -/** - * Write the specified `merge` to the specified `gitDir`. The behavior is - * undefined if there is already a merge recorded in `gitDir`. - * - * @param {String} gitDir - * @param {Merge} merge - */ -exports.writeMerge = co.wrap(function *(gitDir, merge) { - assert.isString(gitDir); - assert.instanceOf(merge, Merge); - - const root = path.join(gitDir, metaMergeDir); - yield fs.mkdir(root); - yield fs.writeFile(path.join(root, messageFile), merge.message); - yield fs.writeFile(path.join(root, originalHeadFile), - merge.originalHead + "\n"); - yield fs.writeFile(path.join(root, mergeHeadFile), - merge.mergeHead + "\n"); -}); - -/** - * Remove files related to a meta merge. - * - * @param {String} gitDir - */ -exports.cleanMerge = co.wrap(function *(gitDir) { - assert.isString(gitDir); - const root = path.join(gitDir, metaMergeDir); - const promise = new Promise(callback => { - return rimraf(root, {}, callback); - }); - yield promise; -}); diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 2c4b46360..ff157a1a8 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -41,15 +41,48 @@ const Commit = require("./commit"); const DeinitUtil = require("./deinit_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); -const Merge = require("./merge"); -const MergeFileUtil = require("./merge_file_util"); const Open = require("./open"); const RepoStatus = require("./repo_status"); +const SequencerState = require("./sequencer_state"); +const SequencerStateUtil = require("./sequencer_state_util"); const StatusUtil = require("./status_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); const SubmoduleUtil = require("./submodule_util"); const UserError = require("./user_error"); +const CommitAndRef = SequencerState.CommitAndRef; +const MERGE = SequencerState.TYPE.MERGE; + +/** + * If there is a sequencer with a merge in the specified `path` return it, + * otherwise, return null. + * + * @param {String} path + * @return {String|null} + */ +const getSequencerIfMerge = co.wrap(function *(path) { + const seq = yield SequencerStateUtil.readSequencerState(path); + if (null !== seq && MERGE === seq.type) { + return seq; + } + return null; +}); + +/** + * If there is a sequencer with a merge in the specified `path` return it, + * otherwise, throw a `UserError` indicating that there is no merge. + * + * @param {String} path + * @return {String} + */ +const checkForMerge = co.wrap(function *(path) { + const seq = yield getSequencerIfMerge(path); + if (null === seq) { + throw new UserError("No merge in progress."); + } + return seq; +}); + /** * @enum {MODE} * Flags to describe what type of merge to do. @@ -377,11 +410,15 @@ ${colors.green(subSha)}.`); // Abort if conflicted. if (subIndex.hasConflicts()) { - const merge = new Merge(message, - subHead.id().tostrS(), - subCommit.id().tostrS()); - yield MergeFileUtil.writeMerge(subRepo.path(), merge); - + const seq = new SequencerState({ + type: MERGE, + originalHead: new CommitAndRef(subHead.id().tostrS(), null), + target: new CommitAndRef(subCommit.id().tostrS(), null), + currentCommit: 0, + commits: [subCommit.id().tostrS()], + message: message, + }); + yield SequencerStateUtil.writeSequencerState(subRepo.path(), seq); errorMessage += `Submodule ${colors.red(path)} is conflicted:\n`; const entries = subIndex.entries(); for (let i = 0; i < entries.length; ++i) { @@ -433,11 +470,17 @@ ${colors.green(subSha)}.`); if ("" !== errorMessage) { // We're about to fail due to conflict. First, record that there is a // merge in progress so that we can continue or abort it later. - - const merge = new Merge(message, - head.id().tostrS(), - commit.id().tostrS()); - yield MergeFileUtil.writeMerge(repo.path(), merge); + // TODO: some day when we make use of it, write the ref name for HEAD + + const seq = new SequencerState({ + type: MERGE, + originalHead: new CommitAndRef(head.id().tostrS(), null), + target: new CommitAndRef(commit.id().tostrS(), null), + currentCommit: 0, + commits: [commit.id().tostrS()], + message: message, + }); + yield SequencerStateUtil.writeSequencerState(repo.path(), seq); throw new UserError(errorMessage); } @@ -500,16 +543,11 @@ const checkForConflicts = function (index) { */ exports.continue = co.wrap(function *(repo) { assert.instanceOf(repo, NodeGit.Repository); - const result = { metaCommit: null, submoduleCommits: {}, }; - const merge = yield MergeFileUtil.readMerge(repo.path()); - if (null === merge) { - throw new UserError("No merge in progress."); - } - + const seq = yield checkForMerge(repo.path()); const index = yield repo.index(); checkForConflicts(index); @@ -518,7 +556,7 @@ exports.continue = co.wrap(function *(repo) { // conflicts. We validated in `checkForConflicts` that there are no "real" // conflicts. - console.log(`Continuing with merge of ${colors.green(merge.mergeHead)}.`); + console.log(`Continuing with merge of ${colors.green(seq.target.sha)}.`); let errorMessage = ""; @@ -531,8 +569,8 @@ exports.continue = co.wrap(function *(repo) { return; // RETURN } const sig = subRepo.defaultSignature(); - const subMerge = yield MergeFileUtil.readMerge(subRepo.path()); - if (null === subMerge) { + const subSeq = yield getSequencerIfMerge(subRepo.path()); + if (null === subSeq) { // There is no merge in this submodule, but if there are staged // changes we need to make a commit. @@ -543,7 +581,7 @@ exports.continue = co.wrap(function *(repo) { const id = yield subRepo.createCommitOnHead([], sig, sig, - merge.message); + seq.message); result.submoduleCommits[subPath] = id.tostrS(); } } @@ -552,15 +590,15 @@ exports.continue = co.wrap(function *(repo) { // Continue it and then clean up the merge. const head = yield subRepo.getHeadCommit(); - const mergeHead = yield subRepo.getCommit(subMerge.mergeHead); + const mergeHead = yield subRepo.getCommit(subSeq.target.sha); const treeId = yield subIndex.writeTreeTo(subRepo); const id = yield subRepo.createCommit("HEAD", sig, sig, - subMerge.message, + subSeq.message, treeId, [head, mergeHead]); - yield MergeFileUtil.cleanMerge(subRepo.path()); + yield SequencerStateUtil.cleanSequencerState(subRepo.path()); result.submoduleCommits[subPath] = id.tostrS(); } yield index.addByPath(subPath); @@ -568,7 +606,6 @@ exports.continue = co.wrap(function *(repo) { }); const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo); yield DoWorkQueue.doInParallel(openSubs, continueSub); - yield index.write(); if ("" !== errorMessage) { @@ -578,16 +615,16 @@ exports.continue = co.wrap(function *(repo) { const sig = repo.defaultSignature(); const head = yield repo.getHeadCommit(); - const mergeHead = yield repo.getCommit(merge.mergeHead); + const mergeHead = yield repo.getCommit(seq.target.sha); const metaCommit = yield repo.createCommit("HEAD", sig, sig, - merge.message, + seq.message, treeId, [head, mergeHead]); console.log( `Finished with merge commit ${colors.green(metaCommit.tostrS())}`); - yield MergeFileUtil.cleanMerge(repo.path()); + yield SequencerStateUtil.cleanSequencerState(repo.path()); result.metaCommit = metaCommit.tostrS(); return result; }); @@ -607,10 +644,7 @@ const resetMerge = co.wrap(function *(repo) { exports.abort = co.wrap(function *(repo) { assert.instanceOf(repo, NodeGit.Repository); - const merge = yield MergeFileUtil.readMerge(repo.path()); - if (null === merge) { - throw new UserError("No merge in progress."); - } + yield checkForMerge(repo.path()); const head = yield repo.getHeadCommit(repo); const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo); @@ -636,12 +670,12 @@ exports.abort = co.wrap(function *(repo) { NodeGit.Reset.TYPE.SOFT); yield resetMerge(subRepo); } - yield MergeFileUtil.cleanMerge(subRepo.path()); + yield SequencerStateUtil.cleanSequencerState(subRepo.path()); yield index.addByPath(subName); }); yield DoWorkQueue.doInParallel(openSubs, abortSub); yield index.conflictCleanup(); yield index.write(); yield resetMerge(repo); - yield MergeFileUtil.cleanMerge(repo.path()); + yield SequencerStateUtil.cleanSequencerState(repo.path()); }); diff --git a/node/lib/util/print_status_util.js b/node/lib/util/print_status_util.js index 1aa9da459..1504040e3 100644 --- a/node/lib/util/print_status_util.js +++ b/node/lib/util/print_status_util.js @@ -41,7 +41,6 @@ const colors = require("colors/safe"); const path = require("path"); const GitUtil = require("./git_util"); -const Merge = require("./merge"); const CherryPick = require("./cherry_pick"); const SequencerState = require("./sequencer_state"); const Rebase = require("./rebase"); @@ -366,22 +365,6 @@ You are currently rebasing branch '${rebase.headName}' on '${shortSha}'. `; }; -/** - * Return a message describing the specified `merge`. - * - * @param {Merge} - * @return {String} - */ -function printMerge(merge) { - assert.instanceOf(merge, Merge); - return `\ -A merge is in progress. - (after resolving conflicts mark the corrected paths - with 'git meta add', then run "git meta merge --continue") - (use "git meta merge --abort" to check out the original branch) -`; -} - /** * Return a message describing the specified `cherryPick`. * @@ -466,10 +449,6 @@ exports.printRepoStatus = function (status, cwd) { result += exports.printCurrentBranch(status); - if (null !== status.merge) { - result += printMerge(status.merge); - } - if (null !== status.cherryPick) { result += printCherryPick(status.cherryPick); } diff --git a/node/lib/util/read_repo_ast_util.js b/node/lib/util/read_repo_ast_util.js index d7d52ea01..1ceac9551 100644 --- a/node/lib/util/read_repo_ast_util.js +++ b/node/lib/util/read_repo_ast_util.js @@ -45,7 +45,6 @@ const path = require("path"); const CherryPickFileUtil = require("./cherry_pick_file_util"); const RepoAST = require("./repo_ast"); const RebaseFileUtil = require("./rebase_file_util"); -const MergeFileUtil = require("./merge_file_util"); const SequencerStateUtil = require("./sequencer_state_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -543,13 +542,6 @@ exports.readRAST = co.wrap(function *(repo) { yield loadCommit(NodeGit.Oid.fromString(rebase.onto)); } - const merge = yield MergeFileUtil.readMerge(repo.path()); - - if (null !== merge) { - yield loadCommit(NodeGit.Oid.fromString(merge.originalHead)); - yield loadCommit(NodeGit.Oid.fromString(merge.mergeHead)); - } - const cherryPick = yield CherryPickFileUtil.readCherryPick(repo.path()); if (null !== cherryPick) { @@ -578,7 +570,6 @@ exports.readRAST = co.wrap(function *(repo) { workdir: workdir, openSubmodules: openSubmodules, rebase: rebase, - merge: merge, cherryPick: cherryPick, sequencerState: sequencer, bare: bare, diff --git a/node/lib/util/repo_ast.js b/node/lib/util/repo_ast.js index fa3fb8b85..c3fa5b7c9 100644 --- a/node/lib/util/repo_ast.js +++ b/node/lib/util/repo_ast.js @@ -38,7 +38,6 @@ const deeper = require("deeper"); const deepCopy = require("deepcopy"); const Rebase = require("./rebase"); -const Merge = require("./merge"); const CherryPick = require("./cherry_pick"); const SequencerState = require("./sequencer_state"); @@ -394,7 +393,6 @@ class AST { * @param {Object} [args.notes] * @param {Object} [args.openSubmodules] * @param {Rebase} [args.rebase] - * @param {Merge} [args.merge] * @param {CherryPick} [args.cherryPick] * @param {SequencerState} [args.sequencerState] */ @@ -562,18 +560,6 @@ in commit ${id}.`); } } - this.d_merge = null; - if ("merge" in args) { - const merge = args.merge; - if (null !== merge) { - assert.instanceOf(merge, Merge); - assert.isFalse(this.d_bare); - checkAndTraverse(merge.originalHead, "original head of merge"); - checkAndTraverse(merge.mergeHead, "merge head"); - this.d_merge = merge; - } - } - this.d_cherryPick = null; if ("cherryPick" in args) { const cherryPick = args.cherryPick; @@ -775,13 +761,6 @@ in commit ${id}.`); return this.d_rebase; } - /** - * @property {Merge} null unless a merge is in progress - */ - get merge() { - return this.d_merge; - } - /** * @property {CherryPick} null unless a cherry-pick is in progress */ @@ -855,7 +834,6 @@ in commit ${id}.`); openSubmodules: ("openSubmodules" in args) ? args.openSubmodules : this.d_openSubmodules, rebase: ("rebase" in args) ? args.rebase : this.d_rebase, - merge: ("merge" in args) ? args.merge : this.d_merge, cherryPick: ("cherryPick" in args) ? args.cherryPick : this.d_cherryPick, sequencerState: ("sequencerState" in args) ? @@ -943,7 +921,6 @@ AST.Branch = Branch; AST.Commit = Commit; AST.Conflict = Conflict; AST.Rebase = Rebase; -AST.Merge = Merge; AST.CherryPick = CherryPick; AST.Remote = Remote; AST.SequencerState = SequencerState; diff --git a/node/lib/util/repo_ast_util.js b/node/lib/util/repo_ast_util.js index 4c38b3d84..ce0664c48 100644 --- a/node/lib/util/repo_ast_util.js +++ b/node/lib/util/repo_ast_util.js @@ -489,32 +489,6 @@ ${colorExp(expected.rebase.onto)} but got ${colorAct(actual.rebase.onto)}.`); } } - // Check merge - - if (null === actual.merge && null !== expected.merge) { - result.push("Missing merge."); - } - else if (null !== actual.merge && null === expected.merge) { - result.push("Unexpected merge."); - } - else if (null !== actual.merge) { - if (actual.merge.message !== expected.merge.message) { - result.push(`Expected ${colorBad("merge message")} to be \ -${colorExp(expected.merge.message)} but got \ -${colorAct(actual.merge.message)}.`); - } - if (actual.merge.originalHead !== expected.merge.originalHead) { - result.push(`Expected ${colorBad("merge original head")} to be \ -${colorExp(expected.merge.originalHead)} but got \ -${colorAct(actual.merge.originalHead)}.`); - } - if (actual.merge.mergeHead !== expected.merge.mergeHead) { - result.push(`Expected ${colorBad("merge head")} to be \ -${colorExp(expected.merge.mergeHead)} but got \ -${colorAct(actual.merge.mergeHead)}.`); - } - } - // Check cherry-pick if (null === actual.cherryPick && null !== expected.cherryPick) { @@ -787,13 +761,6 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { mapCommitId(rebase.onto)); } - let merge = ast.merge; - if (null !== merge) { - merge = new RepoAST.Merge(merge.message, - mapCommitId(merge.originalHead), - mapCommitId(merge.mergeHead)); - } - let cherryPick = ast.cherryPick; if (null !== cherryPick) { cherryPick = new RepoAST.CherryPick( @@ -812,6 +779,7 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { target: target, commits: commits, currentCommit: sequencer.currentCommit, + message: sequencer.message, }); } @@ -827,7 +795,6 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { workdir: mapChanges(ast.workdir), openSubmodules: openSubmodules, rebase: rebase, - merge: merge, cherryPick: cherryPick, sequencerState: sequencer, }); diff --git a/node/lib/util/repo_status.js b/node/lib/util/repo_status.js index 0e222acbd..9b21a7b88 100644 --- a/node/lib/util/repo_status.js +++ b/node/lib/util/repo_status.js @@ -32,7 +32,6 @@ const assert = require("chai").assert; const Rebase = require("./rebase"); -const Merge = require("./merge"); const CherryPick = require("./cherry_pick"); const SequencerState = require("./sequencer_state"); @@ -447,7 +446,6 @@ class RepoStatus { * @param {Object} [args.submodules] map from name to `Submodule` * @param {Object} [args.workdir] map from name to `FILESTATUS` * @param {Rebase} [args.rebase] rebase, if one is in progress - * @param {Merge} [args.merge] merge, if one is in progress * @param {CherryPick} [args.cherryPick] cherry-pick, if one is in progress * @param {SequencerState} * [args.sequencerState] state of sequencer @@ -465,7 +463,6 @@ class RepoStatus { this.d_workdir = {}; this.d_submodules = {}; this.d_rebase = null; - this.d_merge = null; this.d_cherryPick = null; this.d_sequencerState = null; @@ -513,13 +510,6 @@ class RepoStatus { this.d_rebase = rebase; } - if ("merge" in args) { - const merge = args.merge; - if (null !== merge) { - assert.instanceOf(merge, Merge); - } - this.d_merge = merge; - } if ("cherryPick" in args) { const cherryPick = args.cherryPick; if (null !== cherryPick) { @@ -705,13 +695,6 @@ class RepoStatus { return this.d_rebase; } - /** - * @property {merge} merge if non-null, state of in-progress merge - */ - get merge() { - return this.d_merge; - } - /** * @property {CherryPick} cherryPick if ~null, state of cherryPick */ @@ -749,7 +732,6 @@ class RepoStatus { args.submodules: this.d_submodules, workdir: ("workdir" in args) ? args.workdir : this.d_workdir, rebase: ("rebase" in args) ? args.rebase : this.d_rebase, - merge: ("merge" in args) ? args.merge : this.d_merge, cherryPick: ("cherryPick" in args) ? args.cherryPick : this.d_cherryPick, sequencerState: ("sequencerState" in args) ? diff --git a/node/lib/util/sequencer_state.js b/node/lib/util/sequencer_state.js index eaa917bab..b9974fe89 100644 --- a/node/lib/util/sequencer_state.js +++ b/node/lib/util/sequencer_state.js @@ -121,6 +121,7 @@ class SequencerState { * @param {CommitAndRef} properties.target * @param {[String]} properties.commits * @param {Number} properties.currentCommit + * @param {String|null} [properties.message] */ constructor(properties) { assert.isString(properties.type); @@ -132,6 +133,14 @@ class SequencerState { assert(0 <= properties.currentCommit); assert(properties.commits.length > properties.currentCommit); + this.d_message = null; + if ("message" in properties) { + if (null !== properties.message) { + assert.isString(properties.message); + this.d_message = properties.message; + } + } + this.d_type = properties.type; this.d_originalHead = properties.originalHead; this.d_target = properties.target; @@ -178,6 +187,13 @@ class SequencerState { return this.d_currentCommit; } + /** + * @property {String|null} message commit message to be used + */ + get message() { + return this.d_message; + } + /** * Return true if the specified `rhs` represents the same value as this * `SequencerState` object and false otherwise. Two `SequencerState` @@ -193,7 +209,8 @@ class SequencerState { this.d_originalHead.equal(rhs.d_originalHead) && this.d_target.equal(rhs.d_target) && deeper(this.d_commits, rhs.d_commits) && - this.d_currentCommit === rhs.d_currentCommit; + this.d_currentCommit === rhs.d_currentCommit && + this.d_message === rhs.d_message; } } @@ -201,7 +218,7 @@ SequencerState.prototype.toString = function () { return `\ SequencerState(type=${this.d_type}, originalHead=${this.d_originalHead}, \ target=${this.d_target}, commits=${JSON.stringify(this.d_commits)}, \ -currentCommit=${this.d_currentCommit})`; +currentCommit=${this.d_currentCommit}, msg=${this.d_message})`; }; SequencerState.TYPE = TYPE; diff --git a/node/lib/util/sequencer_state_util.js b/node/lib/util/sequencer_state_util.js index 9e32be42b..0f8c77c59 100644 --- a/node/lib/util/sequencer_state_util.js +++ b/node/lib/util/sequencer_state_util.js @@ -60,6 +60,7 @@ const CommitAndRef = SequencerState.CommitAndRef; * /CURRENT_COMMIT -- Single line containing the index of * current commit in COMMITS to operate * on. + * /MESSAGE -- commit message file, if any */ const SEQUENCER_DIR = "meta_sequencer"; @@ -68,6 +69,7 @@ const ORIGINAL_HEAD_FILE = "ORIGINAL_HEAD"; const TARGET_FILE = "TARGET"; const COMMITS_FILE = "COMMITS"; const CURRENT_COMMIT_FILE = "CURRENT_COMMIT"; +const MESSAGE_FILE = "MESSAGE"; /** * Return the contents of the file in the sequencer directory from the @@ -203,12 +205,14 @@ exports.readSequencerState = co.wrap(function *(gitDir) { if (null === currentCommit) { return null; // RETURN } + const message = yield exports.readFile(gitDir, MESSAGE_FILE); return new SequencerState({ type: type, originalHead: original, target: target, commits: commits, - currentCommit: currentCommit + currentCommit: currentCommit, + message: message, }); }); @@ -257,6 +261,9 @@ exports.writeSequencerState = co.wrap(function *(gitDir, state) { yield fs.writeFile(path.join(root, COMMITS_FILE), commitsContent); yield fs.writeFile(path.join(root, CURRENT_COMMIT_FILE), "" + state.currentCommit); + if (null !== state.message) { + yield fs.writeFile(path.join(root, MESSAGE_FILE), state.message); + } }); /** @@ -291,5 +298,6 @@ exports.mapCommits = function (sequencer, commitMap) { target: newTarget, currentCommit: sequencer.currentCommit, commits: newCommits, + message: sequencer.message, }); }; diff --git a/node/lib/util/shorthand_parser_util.js b/node/lib/util/shorthand_parser_util.js index 8b1911d48..e3ae4e3d7 100644 --- a/node/lib/util/shorthand_parser_util.js +++ b/node/lib/util/shorthand_parser_util.js @@ -60,7 +60,7 @@ const SequencerState = RepoAST.SequencerState; * base repo type = 'S' | 'B' | ('C') | 'A' | 'N' * override = | | | | * | | | | - * | | | | + * | | | * * head = 'H='| nothing means detached * nothing = @@ -81,10 +81,9 @@ const SequencerState = RepoAST.SequencerState; * [' '=[](',\s*'=[])*] * note = N =message * rebase = E,, - * merge = M,, * cherry-pick = P, - * sequencer = Q' '' '' ' - * ' '(',')* + * sequencer = Q[message'#']' '' ' + * ' '' '(',')* * sequencer type = C | M | R * commit and ref = commit':'[] * index = I [,]* @@ -189,9 +188,6 @@ const SequencerState = RepoAST.SequencerState; * -- file `x` to be `y`. * S:Emaster,1,2 -- rebase in progress started on "master", * original head "1", onto commit "2" - * S:Mmerging commit,1,2 -- merge in progress started with commit - * message "merging commit" - * original head "1", merge head "2" * S:CP1,2 -- cherry-pick in progress started with * original head "1", picking commit "2" * S:I *foo=a*b*c -- The file 'foo' is flagged in the index @@ -207,6 +203,9 @@ const SequencerState = RepoAST.SequencerState; * -- list of commits to be rebased are "c", "d", * -- and "a", and we're currently on the * -- second commit, at index 1: "d". + * Qmerge of 'foo'#M a:refs/heads/master q: 1 c,d,a + * -- similar to above but a merge with a saved + * -- commit message: "merge of 'foo'" * * Note that the "clone' type may not be used with single-repo ASTs, and the * url must map to the name of another repo. A cloned repository has the @@ -390,7 +389,6 @@ function prepareASTArguments(baseAST, rawRepo) { workdir: baseAST.workdir, openSubmodules: baseAST.openSubmodules, rebase: baseAST.rebase, - merge: baseAST.merge, cherryPick: baseAST.cherryPick, sequencerState: baseAST.sequencerState, bare: baseAST.bare, @@ -505,12 +503,6 @@ function prepareASTArguments(baseAST, rawRepo) { resultArgs.rebase = rawRepo.rebase; } - // Override merge if provided - - if ("merge" in rawRepo) { - resultArgs.merge = rawRepo.merge; - } - // Override cherry-pick if provided if ("cherryPick" in rawRepo) { @@ -598,7 +590,6 @@ function parseOverrides(shorthand, begin, end, delimiter) { let notes = {}; let openSubmodules = {}; let rebase; - let merge; let cherryPick; let sequencer; @@ -967,26 +958,6 @@ function parseOverrides(shorthand, begin, end, delimiter) { rebase = new RepoAST.Rebase(parts[0], parts[1], parts[2]); } - /** - * Parse the merge definition beginning at the specified `begin` and - * terminating at the specified `end`. - * - * @param {Number} begin - * @param {Number} end - */ - function parseMerge(begin, end) { - if (begin === end) { - merge = null; - return; // RETURN - } - const mergeDef = shorthand.substr(begin, end - begin); - const parts = mergeDef.split(","); - assert.equal(parts.length, - 3, - `Wrong number of merge parts in ${mergeDef}`); - merge = new RepoAST.Merge(parts[0], parts[1], parts[2]); - } - /** * Parse the cherry-pick definition beginning at the specified `begin` and * terminating at the specified `end`. @@ -1020,7 +991,19 @@ function parseOverrides(shorthand, begin, end, delimiter) { return; // RETURN } const sequencerDef = shorthand.substr(begin, end - begin); - const parts = sequencerDef.split(" "); + const allParts = sequencerDef.split("#"); + let message = null; + let mainPart; + if (1 === allParts.length) { + mainPart = allParts[0]; + } else { + assert.equal(2, + allParts.length, + `Malformed sequencer ${sequencerDef}`); + message = allParts[0]; + mainPart = allParts[1]; + } + const parts = mainPart.split(" "); assert.equal(parts.length, 5, `Wrong number of sequencer parts in ${sequencerDef}`); @@ -1039,6 +1022,7 @@ function parseOverrides(shorthand, begin, end, delimiter) { target: target, currentCommit: currentCommit, commits: commits, + message: message, }); } /** @@ -1061,7 +1045,6 @@ function parseOverrides(shorthand, begin, end, delimiter) { case "H": return parseHeadOverride; case "R": return parseRemote; case "I": return parseIndex; - case "M": return parseMerge; case "P": return parseCherryPick; case "N": return parseNote; case "W": return parseWorkdir; @@ -1109,9 +1092,6 @@ function parseOverrides(shorthand, begin, end, delimiter) { if (undefined !== rebase) { result.rebase = rebase; } - if (undefined !== merge) { - result.merge = merge; - } if (undefined !== cherryPick) { result.cherryPick = cherryPick; } @@ -1374,10 +1354,6 @@ exports.parseMultiRepoShorthand = function (shorthand, existingRepos) { includeCommit(resultArgs.rebase.originalHead); includeCommit(resultArgs.rebase.onto); } - if (resultArgs.merge) { - includeCommit(resultArgs.merge.originalHead); - includeCommit(resultArgs.merge.mergeHead); - } if (resultArgs.cherryPick) { includeCommit(resultArgs.cherryPick.originalHead); includeCommit(resultArgs.cherryPick.picked); diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 79d8cb65b..03164db2e 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -44,8 +44,6 @@ const CherryPick = require("./cherry_pick"); const CherryPickFileUtil = require("./cherry_pick_file_util"); const DiffUtil = require("./diff_util"); const GitUtil = require("./git_util"); -const Merge = require("./merge"); -const MergeFileUtil = require("./merge_file_util"); const PrintStatusUtil = require("./print_status_util"); const Rebase = require("./rebase"); const RebaseFileUtil = require("./rebase_file_util"); @@ -121,29 +119,6 @@ function remapRebase(rebase, commitMap) { return new Rebase(rebase.headName, originalHead, onto); } -/** - * Return a new `Merge` object having the same value as the specified `merge` - * but with commit shas being replaced by commits in the specified `commitMap`. - * - * @param {Merge} merge - * @param {Object} commitMap from sha to sha - * @return {Merge} - */ -function remapMerge(merge, commitMap) { - assert.instanceOf(merge, Merge); - assert.isObject(commitMap); - - let originalHead = merge.originalHead; - let mergeHead = merge.mergeHead; - if (originalHead in commitMap) { - originalHead = commitMap[originalHead]; - } - if (mergeHead in commitMap) { - mergeHead = commitMap[mergeHead]; - } - return new Merge(merge.message, originalHead, mergeHead); -} - /** * Return a new `CherryPick` object having the same value as the specified * `cherryPick` but with commit shas being replaced by commits in the specified @@ -203,8 +178,6 @@ exports.remapRepoStatus = function (status, commitMap, urlMap) { workdir: status.workdir, rebase: status.rebase === null ? null : remapRebase(status.rebase, commitMap), - merge: status.merge === null ? null : remapMerge(status.merge, - commitMap), cherryPick: status.cherryPick === null ? null : remapCherryPick(status.cherryPick, commitMap), @@ -499,7 +472,6 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { args.rebase = rebase; } - args.merge = yield MergeFileUtil.readMerge(repo.path()); args.cherryPick = yield CherryPickFileUtil.readCherryPick(repo.path()); args.sequencerState = yield SequencerStateUtil.readSequencerState(repo.path()); @@ -639,7 +611,7 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { /** * Throw a `UserError` unless the specified `status` reflects a repository in a * normal, ready state, that is, it does not have any conflicts or in-progress - * operations such as a rebase, merge, or cherry-pick. Adjust output paths to + * operations such as a rebase or cherry-pick. Adjust output paths to * be relative to the specified `cwd`. * * @param {RepoStatus} status @@ -652,12 +624,6 @@ exports.ensureReady = function (status) { Before proceeding, you must complete the rebase in progress (by running 'git meta rebase --continue') or abort it (by running 'git meta rebase --abort').`); - } - if (null !== status.merge) { - throw new UserError(`\ -Before proceeding, you must complete the merge in progress (by running -'git meta merge --continue') or abort it (by running -'git meta merge --abort').`); } if (null !== status.cherryPick) { throw new UserError(`\ diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index 1d89b78cb..837dffa64 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -48,7 +48,6 @@ const path = require("path"); const CherryPickFileUtil = require("./cherry_pick_file_util"); const ConflictUtil = require("./conflict_util"); const DoWorkQueue = require("./do_work_queue"); -const MergeFileUtil = require("./merge_file_util"); const RebaseFileUtil = require("./rebase_file_util"); const RepoAST = require("./repo_ast"); const RepoASTUtil = require("./repo_ast_util"); @@ -467,19 +466,6 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { indexHead = rebase.onto; } - // Write out the merge info, if it exists. - - if (null !== ast.merge) { - // Map commits - - const originalSha = commitMap[ast.merge.originalHead]; - const mergeSha = commitMap[ast.merge.mergeHead]; - const merge = new RepoAST.Merge(ast.merge.message, - originalSha, - mergeSha); - yield MergeFileUtil.writeMerge(repo.path(), merge); - } - // Write out the cherry-pick info, if it exists. if (null !== ast.cherryPick) { diff --git a/node/test/util/merge.js b/node/test/util/merge.js deleted file mode 100644 index e68d16b30..000000000 --- a/node/test/util/merge.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2017, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -const assert = require("chai").assert; - -const Merge = require("../../lib/util/merge"); - -describe("Merge", function () { - it("breath", function () { - const m = new Merge("foo", "bar", "baz"); - assert.instanceOf(m, Merge); - assert.isFrozen(m); - assert.equal(m.message, "foo"); - assert.equal(m.originalHead, "bar"); - assert.equal(m.mergeHead, "baz"); - }); -}); diff --git a/node/test/util/merge_file_util.js b/node/test/util/merge_file_util.js deleted file mode 100644 index 01eea9fc0..000000000 --- a/node/test/util/merge_file_util.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2017, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -const assert = require("chai").assert; -const co = require("co"); -const fs = require("fs-promise"); -const path = require("path"); - -const MergeFileUtil = require("../../lib//util/merge_file_util"); -const Merge = require("../../lib//util/merge"); -const TestUtil = require("../../lib/util/test_util"); - -describe("MergeFileUtil", function () { - describe("readMerge", function () { - it("found", co.wrap(function *() { - const tempDir = yield TestUtil.makeTempDir(); - const gitDir = path.join(tempDir, "META_MERGE"); - yield fs.mkdir(gitDir); - yield fs.writeFile(path.join(gitDir, "MSG"), "hello world\n"); - yield fs.writeFile(path.join(gitDir, "ORIG_HEAD"), "123\n"); - yield fs.writeFile(path.join(gitDir, "MERGE_HEAD"), "456\n"); - const result = yield MergeFileUtil.readMerge(tempDir); - assert.deepEqual(result, new Merge("hello world\n", "123", "456")); - })); - it("not found", co.wrap(function *() { - const tempDir = yield TestUtil.makeTempDir(); - const gitDir = path.join(tempDir, "META_MERGE"); - yield fs.mkdir(gitDir); - yield fs.writeFile(path.join(gitDir, "MSG"), "hello world\n"); - yield fs.writeFile(path.join(gitDir, "ORIG_HEAD"), "123\n"); - const result = yield MergeFileUtil.readMerge(tempDir); - assert.isNull(result); - })); - }); - it("writeMerge", co.wrap(function *() { - const tempDir = yield TestUtil.makeTempDir(); - const merge = new Merge("foo\n", "bar", "baz"); - yield MergeFileUtil.writeMerge(tempDir, merge); - const root = path.join(tempDir, "META_MERGE"); - const message = yield fs.readFile(path.join(root, "MSG"), "utf8"); - assert.equal(message, "foo\n"); - const originalHead = yield fs.readFile(path.join(root, "ORIG_HEAD"), - "utf8"); - assert.equal(originalHead, "bar\n"); - const mergeHead = yield fs.readFile(path.join(root, "MERGE_HEAD"), - "utf8"); - assert.equal(mergeHead, "baz\n"); - })); - it("cleanMerge", co.wrap(function *() { - const tempDir = yield TestUtil.makeTempDir(); - yield MergeFileUtil.writeMerge(tempDir, - new Merge("hey", "there", "all")); - yield MergeFileUtil.cleanMerge(tempDir); - const result = yield MergeFileUtil.readMerge(tempDir); - assert.isNull(result); - })); -}); diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index aa9fe9777..55d7ebb2e 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -202,7 +202,7 @@ x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, fails: true, }, "already a merge in progress": { - initial: "x=S:Mhia,1,1", + initial: "x=S:Qhia#M 1: 1: 0 1", fromCommit: "1", fails: true, }, @@ -306,14 +306,14 @@ x=S:C2-1 s=Sa:1;C3-1 t=Sa:1;Bmaster=2;Bfoo=3`, a=B|b=B| x=S:C2-1 s=Sa:1;C3-1 s=Sb:1;Bmaster=2;Bfoo=3`, fromCommit: "3", - expected: "x=E:Mmessage\n,2,3", + expected: "x=E:Qmessage\n#M 2: 3: 0 3", fails: true, }, "conflict in meta": { initial: "x=S:C2-1 foo=bar;C3-1 foo=baz;Bmaster=2;Bfoo=3", fromCommit: "3", expected: ` -x=E:Mmessage\n,2,3;I *foo=~*bar*baz;W foo=<<<<<<< ours +x=E:Qmessage\n#M 2: 3: 0 3;I *foo=~*bar*baz;W foo=<<<<<<< ours bar ======= baz @@ -328,8 +328,8 @@ a=B:Ca-1 README.md=8;Cb-1 README.md=9;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, fromCommit: "4", expected: ` -x=E:Mmessage\n,3,4;I *s=S:1*S:a*S:b; -Os Mmessage\n,a,b!I *README.md=hello world*8*9!W README.md=\ +x=E:Qmessage\n#M 3: 4: 0 4;I *s=S:1*S:a*S:b; +Os Qmessage\n#M a: b: 0 b!I *README.md=hello world*8*9!W README.md=\ <<<<<<< ours 8 ======= @@ -404,26 +404,27 @@ x=E:Cx-4,5 t=Sa:b;Bmaster=x` fails: true, }, "continue in meta": { - initial: "x=S:C2-1;C3-1;Bmaster=2;I baz=bam;Mhi\n,2,3;Bfoo=3", - expected: "x=E:Chi\n#x-2,3 baz=bam;Bmaster=x;M;I baz=~", + initial: ` +x=S:C2-1;C3-1;Bmaster=2;I baz=bam;Qhi\n#M 2: 3: 0 3;Bfoo=3`, + expected: "x=E:Chi\n#x-2,3 baz=bam;Bmaster=x;Q;I baz=~", }, "cheap continue in meta": { - initial: "x=S:C2;Mhi\n,1,2;B2=2", - expected: "x=E:Chi\n#x-1,2 ;Bmaster=x;M", + initial: "x=S:C2;Qhi\n#M 1: 2: 0 2;B2=2", + expected: "x=E:Chi\n#x-1,2 ;Bmaster=x;Q", }, "continue with extra in non-continue sub": { initial: ` a=B| -x=U:C3-1;Mhi\n,2,3;B3=3;Os I README.md=8`, +x=U:C3-1;Qhi\n#M 2: 3: 0 3;B3=3;Os I README.md=8`, expected: ` -x=E:Chi\n#x-2,3 s=Sa:s;Bmaster=x;M;Os Chi\n#s-1 README.md=8!H=s`, +x=E:Chi\n#x-2,3 s=Sa:s;Bmaster=x;Q;Os Chi\n#s-1 README.md=8!H=s`, }, "continue in a sub": { initial: ` a=B:Ca;Ba=a| -x=U:C3-1;Mhi\n,2,3;B3=3;Os I README.md=8!Myo\n,1,a!Ba=a`, +x=U:C3-1;Qhi\n#M 2: 3: 0 3;B3=3;Os I README.md=8!Qyo\n#M 1: a: 0 a!Ba=a`, expected: ` -x=E:Chi\n#x-2,3 s=Sa:s;Bmaster=x;M;Os Cyo\n#s-1,a README.md=8!H=s!Ba=a`, +x=E:Chi\n#x-2,3 s=Sa:s;Bmaster=x;Q;Os Cyo\n#s-1,a README.md=8!H=s!Ba=a`, }, "continue in one sub, done in another": { initial: ` @@ -432,11 +433,11 @@ x=S:C2-1 s=Sa:1,t=Sa:1; C3-2 s=Sa:a,t=Sa:a; C4-2 s=Sa:ac,t=Sa:b; Bmaster=3;Bfoo=4; - Mhi\n,3,4; - Os I a=foo!Myou\n,a,ac!Bac=ac; + Qhi\n#M 3: 4: 0 4; + Os I a=foo!Qyou\n#M a: ac: 0 ac!Bac=ac; Ot H=mab`, expected: ` -x=E:Chi\n#x-3,4 s=Sa:s,t=Sa:mab;Bmaster=x;M; +x=E:Chi\n#x-3,4 s=Sa:s,t=Sa:mab;Bmaster=x;Q; Os Cyou\n#s-a,ac a=foo!H=s!Bac=ac; Ot`, }, @@ -463,18 +464,18 @@ x=E:Chi\n#x-3,4 s=Sa:s,t=Sa:mab;Bmaster=x;M; fails: true, }, "noop": { - initial: "x=S:Mfoo,1,1", - expected: "x=E:M", + initial: "x=S:Qfoo#M 1: 1: 0 1", + expected: "x=E:Q", }, "noop with sub": { - initial: "a=B|x=U:Mfoo,1,1;Os Mfoo,1,1", - expected: "x=E:M;Os M", + initial: "a=B|x=U:Qfoo#M 1: 1: 0 1;Os Qfoo#M 1: 1: 0 1", + expected: "x=E:Q;Os Q", }, "moved back a sub": { initial: ` a=B| -x=U:Mx,1,1;Os Cs-1!H=s!Bs=s`, - expected: `x=E:M;Os H=1!Cs-1!Bs=s`, +x=U:Qx#M 1: 1: 0 1;Os Cs-1!H=s!Bs=s`, + expected: `x=E:Q;Os H=1!Cs-1!Bs=s`, }, }; Object.keys(cases).forEach(caseName => { diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js index fdab3dd37..85234e045 100644 --- a/node/test/util/print_status_util.js +++ b/node/test/util/print_status_util.js @@ -35,7 +35,6 @@ const colors = require("colors"); const NodeGit = require("nodegit"); const Rebase = require("../../lib/util/rebase"); -const Merge = require("../../lib/util/merge"); const CherryPick = require("../../lib/util/cherry_pick"); const RepoStatus = require("../../lib/util/repo_status"); const PrintStatusUtil = require("../../lib/util/print_status_util"); @@ -748,20 +747,6 @@ A merge is in progress. exact: `\ On branch ${colors.green("master")}. nothing to commit, working tree clean -`, - }, - "merge": { - input: new RepoStatus({ - currentBranchName: "master", - merge: new Merge("hello", "1", "1"), - }), - exact: `\ -On branch ${colors.green("master")}. -A merge is in progress. - (after resolving conflicts mark the corrected paths - with 'git meta add', then run "git meta merge --continue") - (use "git meta merge --abort" to check out the original branch) -nothing to commit, working tree clean `, }, "cherry-pick": { diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index 9542e454a..b18e5b83e 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -40,8 +40,6 @@ const CherryPick = require("../../lib/util/cherry_pick"); const CherryPickFileUtil = require("../../lib/util/cherry_pick_file_util"); const ConflictUtil = require("../../lib/util/conflict_util"); const DeinitUtil = require("../../lib/util/deinit_util"); -const Merge = require("../../lib/util/merge"); -const MergeFileUtil = require("../../lib/util/merge_file_util"); const Rebase = require("../../lib/util/rebase"); const RepoAST = require("../../lib/util/repo_ast"); const ReadRepoASTUtil = require("../../lib/util/read_repo_ast_util"); @@ -1453,55 +1451,6 @@ describe("readRAST", function () { assert.equal(rebase.onto, secondCommit.id().tostrS()); })); - it("merge", co.wrap(function *() { - // Start out with a base repo having two branches, "master", and "foo", - // foo having one commit on top of master. - - const start = yield repoWithCommit(); - const r = start.repo; - - // Switch to master - - yield r.checkoutBranch("master"); - - const head = yield r.getHeadCommit(); - const sha = head.id().tostrS(); - - const merge = new Merge("hello", sha, sha); - - const original = yield ReadRepoASTUtil.readRAST(r); - const expected = original.copy({ - merge: merge, - }); - - yield MergeFileUtil.writeMerge(r.path(), merge); - - const actual = yield ReadRepoASTUtil.readRAST(r); - - RepoASTUtil.assertEqualASTs(actual, expected); - })); - - it("merge - unreachable", co.wrap(function *() { - const r = yield TestUtil.createSimpleRepository(); - r.detachHead(); - const secondCommit = yield TestUtil.generateCommit(r); - const thirdCommit = yield TestUtil.generateCommit(r); - - // Then begin a merge. - - const merge = new Merge("hello world", - secondCommit.id().tostrS(), - thirdCommit.id().tostrS()); - yield MergeFileUtil.writeMerge(r.path(), merge); - - // Remove the branches, making the commits reachable only from the - // rebase. - - const ast = yield ReadRepoASTUtil.readRAST(r); - const actualMerge = ast.merge; - assert.deepEqual(actualMerge, merge); - })); - it("cherry-pick", co.wrap(function *() { // Start out with a base repo having two branches, "master", and "foo", // foo having one commit on top of master. diff --git a/node/test/util/repo_ast.js b/node/test/util/repo_ast.js index abdb644da..033b20033 100644 --- a/node/test/util/repo_ast.js +++ b/node/test/util/repo_ast.js @@ -245,7 +245,6 @@ const REBASE = SequencerState.TYPE.REBASE; const Commit = RepoAST.Commit; const Rebase = RepoAST.Rebase; - const Merge = RepoAST.Merge; const CherryPick = RepoAST.CherryPick; const Remote = RepoAST.Remote; @@ -275,7 +274,6 @@ const REBASE = SequencerState.TYPE.REBASE; eopenSubmodules: ("openSubmodules" in expected) ? expected.openSubmodules : {}, erebase: ("rebase" in expected) ? expected.rebase : null, - emerge: ("merge" in expected) ? expected.merge : null, echerryPick: ("cherryPick" in expected) ? expected.cherryPick : null, esequencerState: ("sequencerState" in expected) ? @@ -293,7 +291,6 @@ const REBASE = SequencerState.TYPE.REBASE; head: null, currentBranchName: null, rebase: null, - merge: null, cherryPick: null, sequencerState: null, bare: false, @@ -319,12 +316,6 @@ const REBASE = SequencerState.TYPE.REBASE; commits: {"1": c1 }, head: "1", }, undefined, true), - "bad bare with merge": m({ - bare: true, - merge: new Merge("foo", "1", "1"), - commits: {"1": c1 }, - head: "1", - }, undefined, true), "bad bare with cherry": m({ bare: true, cherryPick: new CherryPick("1", "1"), @@ -654,21 +645,6 @@ const REBASE = SequencerState.TYPE.REBASE; currentCommit: 1, }), }), - "with merge specific commits": m({ - commits: { - "1": new Commit(), - "2": new Commit(), - }, - head: "1", - merge: new Merge("fff", "2", "2"), - }, { - commits: { - "1": new Commit(), - "2": new Commit(), - }, - head: "1", - merge: new Merge("fff", "2", "2"), - }), "with cherry-pick specific commits": m({ commits: { "1": new Commit(), @@ -747,7 +723,6 @@ const REBASE = SequencerState.TYPE.REBASE; assert.deepEqual(obj.workdir, c.eworkdir); assert.deepEqual(obj.openSubmodules, c.eopenSubmodules); assert.deepEqual(obj.rebase, c.erebase); - assert.deepEqual(obj.merge, c.emerge); assert.deepEqual(obj.cherryPick, c.echerryPick); assert.deepEqual(obj.sequencerState, c.esequencerState); assert.equal(obj.bare, c.ebare); @@ -800,15 +775,6 @@ const REBASE = SequencerState.TYPE.REBASE; describe("renderCommit", function () { const Commit = RepoAST.Commit; const c1 = new Commit({ changes: { foo: "bar" }}); - const c2 = new Commit({ changes: { foo: "baz" }}); - const mergeChild = new Commit({ - parents: ["3"], - changes: { bam: "blast" }, - }); - const merge = new Commit({ - parents: ["1", "2"], - changes: {} - }); const deleter = new Commit({ parents: ["1"], changes: { foo: null } @@ -827,25 +793,6 @@ const REBASE = SequencerState.TYPE.REBASE; "1": { foo: "bar" }, }, }, - "merge": { - commits: { "1": c1, "2": c2, "3": merge}, - from: "3", - expected: { foo: "bar" }, - ecache: { - "1": c1.changes, - "3": { foo: "bar" }, - }, - }, - "merge child": { - commits: { "1": c1, "2": c2, "3": merge, "4": mergeChild}, - from: "4", - expected: { foo: "bar", bam: "blast" }, - ecache: { - "1": c1.changes, - "3": { foo: "bar" }, - "4": { foo: "bar", bam: "blast" }, - }, - }, "deletion": { commits: { "1": c1, "2": deleter }, from: "2", @@ -888,7 +835,6 @@ const REBASE = SequencerState.TYPE.REBASE; describe("AST.copy", function () { const Rebase = RepoAST.Rebase; - const Merge = RepoAST.Merge; const CherryPick = RepoAST.CherryPick; const base = new RepoAST({ commits: { "1": new RepoAST.Commit()}, @@ -899,7 +845,6 @@ const REBASE = SequencerState.TYPE.REBASE; index: { foo: "bar" }, workdir: { foo: "bar" }, rebase: new Rebase("hello", "1", "1"), - merge: new Merge("hello", "1", "1"), cherryPick: new CherryPick("1", "1"), sequencerState: new SequencerState({ type: REBASE, @@ -920,7 +865,6 @@ const REBASE = SequencerState.TYPE.REBASE; index: { foo: "bar" }, workdir: { foo: "bar" }, rebase: new Rebase("hello world", "2", "2"), - merge: new Merge("hello world", "2", "2"), cherryPick: new CherryPick("2", "2"), sequencerState: new SequencerState({ type: REBASE, @@ -946,7 +890,6 @@ const REBASE = SequencerState.TYPE.REBASE; index: {}, workdir: {}, rebase: null, - merge: null, cherryPick: null, sequencerState: null, }, @@ -974,7 +917,6 @@ const REBASE = SequencerState.TYPE.REBASE; assert.deepEqual(obj.workdir, c.e.workdir); assert.deepEqual(obj.openSubmodules, c.e.openSubmodules); assert.deepEqual(obj.rebase, c.e.rebase); - assert.deepEqual(obj.merge, c.e.merge); assert.deepEqual(obj.cherryPick, c.e.cherryPick); assert.deepEqual(obj.sequencerState, c.e.sequencerState); assert.equal(obj.bare, c.e.bare); diff --git a/node/test/util/repo_ast_util.js b/node/test/util/repo_ast_util.js index 6c8e55fbf..89abcddce 100644 --- a/node/test/util/repo_ast_util.js +++ b/node/test/util/repo_ast_util.js @@ -38,8 +38,8 @@ const RepoASTUtil = require("../../lib/util/repo_ast_util"); describe("RepoAstUtil", function () { const Conflict = RepoAST.Conflict; const Sequencer = RepoAST.SequencerState; - const CommitAndRef = Sequencer.CommitAndRef; const MERGE = Sequencer.TYPE.MERGE; + const CommitAndRef = Sequencer.CommitAndRef; const Submodule = RepoAST.Submodule; describe("assertEqualCommits", function () { const Commit = RepoAST.Commit; @@ -138,7 +138,6 @@ describe("RepoAstUtil", function () { const Conflict = RepoAST.Conflict; const Commit = AST.Commit; const Rebase = AST.Rebase; - const Merge = AST.Merge; const CherryPick = AST.CherryPick; const Remote = AST.Remote; const Submodule = AST.Submodule; @@ -172,7 +171,6 @@ describe("RepoAstUtil", function () { workdir: { foo: "bar" }, openSubmodules: { y: anAST }, rebase: new Rebase("foo", "1", "1"), - merge: new Merge("foo", "1", "1"), cherryPick: new CherryPick("1", "1"), sequencerState: new Sequencer({ type: MERGE, @@ -194,7 +192,6 @@ describe("RepoAstUtil", function () { workdir: { foo: "bar" }, openSubmodules: { y: anAST }, rebase: new Rebase("foo", "1", "1"), - merge: new Merge("foo", "1", "1"), cherryPick: new CherryPick("1", "1"), sequencerState: new Sequencer({ type: MERGE, @@ -549,73 +546,6 @@ describe("RepoAstUtil", function () { }), fails: true, }, - "missing merge": { - actual: new AST({ - commits: { "1": aCommit}, - head: "1", - merge: new Merge("foo", "1", "1"), - }), - expected: new AST({ - commits: { "1": aCommit}, - head: "1", - }), - fails: true, - }, - "unexpected merge": { - actual: new AST({ - commits: { "1": aCommit}, - head: "1", - }), - expected: new AST({ - commits: { "1": aCommit}, - head: "1", - merge: new Merge("foo", "1", "1"), - }), - fails: true, - }, - "wrong merge message": { - actual: new AST({ - commits: { "1": aCommit}, - head: "1", - merge: new Merge("foo", "1", "1"), - }), - expected: new AST({ - commits: { "1": aCommit}, - head: "1", - merge: new Merge("foo bar", "1", "1"), - }), - fails: true, - }, - "wrong merge original commit": { - actual: new AST({ - commits: { "1": aCommit, "2": aCommit}, - head: "1", - branches: { master: new RepoAST.Branch("2", null), }, - merge: new Merge("foo", "2", "1"), - }), - expected: new AST({ - commits: { "1": aCommit, "2": aCommit}, - head: "1", - branches: { master: new RepoAST.Branch("2", null), }, - merge: new Merge("foo", "1", "1"), - }), - fails: true, - }, - "wrong merge head": { - actual: new AST({ - commits: { "1": aCommit, "2": aCommit}, - head: "1", - branches: { master: new RepoAST.Branch("2", null), }, - merge: new Merge("foo", "1", "2"), - }), - expected: new AST({ - commits: { "1": aCommit, "2": aCommit}, - head: "1", - branches: { master: new RepoAST.Branch("2", null), }, - merge: new Merge("foo", "1", "1"), - }), - fails: true, - }, "missing cherry": { actual: new AST({ commits: { "1": aCommit}, @@ -774,7 +704,6 @@ describe("RepoAstUtil", function () { describe("mapCommitsAndUrls", function () { const Commit = RepoAST.Commit; const Rebase = RepoAST.Rebase; - const Merge = RepoAST.Merge; const CherryPick = RepoAST.CherryPick; const c1 = new Commit({ message: "foo" }); const cases = { @@ -1154,32 +1083,6 @@ describe("RepoAstUtil", function () { rebase: new Rebase("foo", "1", "1"), }), }, - "merge": { - i: new RepoAST({ - commits: { "1": c1 }, - head: "1", - merge: new Merge("foo", "1", "1"), - }), - m: { "1": "2"}, - e: new RepoAST({ - commits: { "2": c1 }, - head: "2", - merge: new Merge("foo", "2", "2"), - }), - }, - "merge unmapped": { - i: new RepoAST({ - commits: { "1": c1 }, - head: "1", - merge: new Merge("foo", "1", "1"), - }), - m: {}, - e: new RepoAST({ - commits: { "1": c1 }, - head: "1", - merge: new Merge("foo", "1", "1"), - }), - }, "cherry-pick": { i: new RepoAST({ commits: { "1": c1 }, @@ -1216,6 +1119,7 @@ describe("RepoAstUtil", function () { target: new CommitAndRef("1", "foo/bar"), commits: ["1"], currentCommit: 0, + message: "meh", }), }), m: { "1": "2"}, @@ -1228,6 +1132,7 @@ describe("RepoAstUtil", function () { target: new CommitAndRef("2", "foo/bar"), commits: ["2"], currentCommit: 0, + message: "meh", }), }), }, @@ -1406,21 +1311,6 @@ describe("RepoAstUtil", function () { }, }), }, - "no clone merge": { - original: new AST({ - commits: { "1": c1}, - head: "1", - merge: new RepoAST.Merge("foo", "1", "1"), - }), - url: "foo", - expected: new AST({ - commits: { "1": c1 }, - head: "1", - remotes: { - origin: new Remote("foo", {}), - }, - }), - }, }; Object.keys(cases).forEach((caseName) => { const c = cases[caseName]; diff --git a/node/test/util/repo_status.js b/node/test/util/repo_status.js index ebe90482c..fea120f7b 100644 --- a/node/test/util/repo_status.js +++ b/node/test/util/repo_status.js @@ -32,7 +32,6 @@ const assert = require("chai").assert; -const Merge = require("../../lib/util/merge"); const CherryPick = require("../../lib/util/cherry_pick"); const Rebase = require("../../lib/util/rebase"); const RepoStatus = require("../../lib/util/repo_status"); @@ -481,7 +480,6 @@ const MERGE = SequencerState.TYPE.MERGE; workdir: {}, submodules: {}, rebase: null, - merge: null, cherryPick: null, sequencerState: null, }; @@ -509,7 +507,6 @@ const MERGE = SequencerState.TYPE.MERGE; }), }, rebase: new Rebase("foo", "1", "2"), - merge: new Merge("baz", "2", "1"), cherryPick: new CherryPick("2", "1"), sequencerState: new SequencerState({ type: MERGE, @@ -531,7 +528,6 @@ const MERGE = SequencerState.TYPE.MERGE; }), }, rebase: new Rebase("foo", "1", "2"), - merge: new Merge("baz", "2", "1"), cherryPick: new CherryPick("2", "1"), sequencerState: new SequencerState({ type: MERGE, @@ -555,7 +551,6 @@ const MERGE = SequencerState.TYPE.MERGE; assert.deepEqual(result.workdir, c.e.workdir); assert.deepEqual(result.submodules, c.e.submodules); assert.deepEqual(result.rebase, c.e.rebase); - assert.deepEqual(result.merge, c.e.merge); assert.deepEqual(result.cherryPick, c.e.cherryPick); assert.deepEqual(result.sequencerState, c.e.sequencerState); }); @@ -1060,7 +1055,6 @@ const MERGE = SequencerState.TYPE.MERGE; }, workdir: { x: FILESTATUS.MODIFIED }, rebase: new Rebase("2", "4", "b"), - merge: new Merge("hah", "1", "1"), cherryPick: new CherryPick("1", "1"), sequencerState: new SequencerState({ type: MERGE, @@ -1079,7 +1073,6 @@ const MERGE = SequencerState.TYPE.MERGE; }, workdir: { x: FILESTATUS.ADDED }, rebase: new Rebase("a", "4", "b"), - merge: new Merge("a", "2", "2"), cherryPick: new CherryPick("a", "2"), sequencerState: new SequencerState({ type: MERGE, @@ -1105,7 +1098,6 @@ const MERGE = SequencerState.TYPE.MERGE; submodules: anotherStat.submodules, workdir: anotherStat.workdir, rebase: anotherStat.rebase, - merge: anotherStat.merge, cherryPick: anotherStat.cherryPick, sequencerState: anotherStat.sequencerState, }); diff --git a/node/test/util/sequencer_state.js b/node/test/util/sequencer_state.js index f6cb0d78c..2e20ac9da 100644 --- a/node/test/util/sequencer_state.js +++ b/node/test/util/sequencer_state.js @@ -96,7 +96,8 @@ const CommitAndRef = SequencerState.CommitAndRef; originalHead: original, target: target, commits: ["3"], - currentCommit: 0 + currentCommit: 0, + message: "meh", }); assert.isFrozen(seq); assert.equal(seq.type, TYPE.MERGE); @@ -104,6 +105,7 @@ const CommitAndRef = SequencerState.CommitAndRef; assert.deepEqual(seq.target, target); assert.deepEqual(seq.commits, ["3"]); assert.equal(seq.currentCommit, 0); + assert.equal(seq.message, "meh"); }); describe("equal", function () { const cnr0 = new CommitAndRef("a", "foo"); @@ -116,6 +118,7 @@ const CommitAndRef = SequencerState.CommitAndRef; target: cnr1, commits: ["1", "2", "3"], currentCommit: 1, + message: "moo", }), rhs: new SequencerState({ type: TYPE.MERGE, @@ -123,6 +126,7 @@ const CommitAndRef = SequencerState.CommitAndRef; target: cnr1, commits: ["1", "2", "3"], currentCommit: 1, + message: "moo", }), expected: true, }, @@ -211,6 +215,25 @@ const CommitAndRef = SequencerState.CommitAndRef; }), expected: false, }, + "different message": { + lhs: new SequencerState({ + type: TYPE.MERGE, + originalHead: cnr0, + target: cnr1, + commits: ["1", "2", "3"], + currentCommit: 1, + message: "ooo", + }), + rhs: new SequencerState({ + type: TYPE.MERGE, + originalHead: cnr0, + target: cnr1, + commits: ["1", "2", "3"], + currentCommit: 1, + message: "moo", + }), + expected: false, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; @@ -227,11 +250,12 @@ const CommitAndRef = SequencerState.CommitAndRef; target: new CommitAndRef("b", null), commits: ["1"], currentCommit: 0, + message: "meh", }); const result = "" + input; assert.equal(result, `\ SequencerState(type=REBASE, originalHead=CommitAndRef(sha=a), \ -target=CommitAndRef(sha=b), commits=["1"], currentCommit=0)`); +target=CommitAndRef(sha=b), commits=["1"], currentCommit=0, msg=meh)`); }); }); diff --git a/node/test/util/sequencer_state_util.js b/node/test/util/sequencer_state_util.js index ac64cdc2d..8571f6c3d 100644 --- a/node/test/util/sequencer_state_util.js +++ b/node/test/util/sequencer_state_util.js @@ -188,6 +188,29 @@ describe("readSequencerState", function () { assert.equal(result.target.ref, "34"); assert.deepEqual(result.commits, ["1", "2", "3"]); assert.equal(result.currentCommit, 1); + assert.isNull(result.message); + })); + it("good state with message", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const fileDir = path.join(gitDir, "meta_sequencer"); + mkdirp.sync(fileDir); + yield fs.writeFile(path.join(fileDir, "TYPE"), + SequencerState.TYPE.MERGE + "\n"); + yield fs.writeFile(path.join(fileDir, "ORIGINAL_HEAD"), "24\n"); + yield fs.writeFile(path.join(fileDir, "TARGET"), "12\n34\n"); + yield fs.writeFile(path.join(fileDir, "COMMITS"), "1\n2\n3\n"); + yield fs.writeFile(path.join(fileDir, "CURRENT_COMMIT"), "1\n"); + yield fs.writeFile(path.join(fileDir, "MESSAGE"), "foo\n"); + const result = yield SequencerStateUtil.readSequencerState(gitDir); + assert.instanceOf(result, SequencerState); + assert.equal(result.type, SequencerState.TYPE.MERGE); + assert.equal(result.originalHead.sha, "24"); + assert.isNull(result.originalHead.ref); + assert.equal(result.target.sha, "12"); + assert.equal(result.target.ref, "34"); + assert.deepEqual(result.commits, ["1", "2", "3"]); + assert.equal(result.currentCommit, 1); + assert.equal(result.message, "foo\n"); })); it("bad type", co.wrap(function *() { const gitDir = yield TestUtil.makeTempDir(); @@ -308,6 +331,22 @@ describe("writeSequencerState", function () { const read = yield SequencerStateUtil.readSequencerState(gitDir); assert.deepEqual(read, initial); })); + it("with message", co.wrap(function *() { + const gitDir = yield TestUtil.makeTempDir(); + const original = new CommitAndRef("a", null); + const target = new CommitAndRef("b", "c"); + const initial = new SequencerState({ + type: SequencerState.TYPE.REBASE, + originalHead: original, + target: target, + commits: ["1", "2"], + currentCommit: 0, + message: "mahaha", + }); + yield SequencerStateUtil.writeSequencerState(gitDir, initial); + const read = yield SequencerStateUtil.readSequencerState(gitDir); + assert.deepEqual(read, initial); + })); }); describe("mapCommits", function () { const cases = { @@ -318,6 +357,7 @@ describe("mapCommits", function () { target: new CommitAndRef("1", "foo"), currentCommit: 0, commits: ["1"], + message: "foo", }), commitMap: { "1": "2", @@ -328,6 +368,7 @@ describe("mapCommits", function () { target: new CommitAndRef("2", "foo"), currentCommit: 0, commits: ["2"], + message: "foo", }), }, "multiple": { diff --git a/node/test/util/shorthand_parser_util.js b/node/test/util/shorthand_parser_util.js index bc84b3e58..89a7cae6c 100644 --- a/node/test/util/shorthand_parser_util.js +++ b/node/test/util/shorthand_parser_util.js @@ -642,20 +642,6 @@ describe("ShorthandParserUtil", function () { rebase: null, }), }, - "merge": { - i: "S:Mhi,1,2", - e: m({ - type: "S", - merge: new RepoAST.Merge("hi", "1", "2"), - }), - }, - "merge null": { - i: "S:M", - e: m({ - type: "S", - merge: null, - }), - }, "cherry-pick": { i: "S:P1,2", e: m({ @@ -716,6 +702,20 @@ describe("ShorthandParserUtil", function () { }), }), }, + "sequencer with message": { + i: "S:Qhello world#R 1:foo 3: 2 a,b,c", + e: m({ + type: "S", + sequencerState: new SequencerState({ + type: SequencerState.TYPE.REBASE, + originalHead: new CommitAndRef("1", "foo"), + target: new CommitAndRef("3", null), + currentCommit: 2, + commits: ["a", "b", "c"], + message: "hello world", + }), + }), + }, "new submodule": { i: "S:I x=Sfoo:;Ox", e: m({ @@ -752,7 +752,6 @@ describe("ShorthandParserUtil", function () { assert.equal(r.currentBranchName, e.currentBranchName); assert.deepEqual(r.openSubmodules, e.openSubmodules); assert.deepEqual(r.rebase, e.rebase); - assert.deepEqual(r.merge, e.merge); assert.deepEqual(r.cherryPick, e.cherryPick); assert.deepEqual(r.sequencerState, e.sequencerState); }); @@ -926,12 +925,6 @@ describe("ShorthandParserUtil", function () { rebase: new RepoAST.Rebase("foo", "1", "1"), }), }, - "merge": { - i: "S:Mfoo,1,1", - e: S.copy({ - merge: new RepoAST.Merge("foo", "1", "1"), - }), - }, "cherry-pick": { i: "S:P1,1", e: S.copy({ @@ -1419,59 +1412,6 @@ x=S:Efoo,8,9`, }), } }, - "missing commits in merge": { - i: ` -a=B:C8-1;C9-1;Bmaster=8;Bfoo=9| -x=S:Mfoo,8,9`, - e: { - a: B.copy({ - commits: { - "1": new Commit({ - changes: { - "README.md": "hello world" - }, - message: "the first commit", - }), - "8": new Commit({ - parents: ["1"], - changes: { "8": "8" }, - message: "message\n", - }), - "9": new Commit({ - parents: ["1"], - changes: { "9": "9" }, - message: "message\n", - }), - }, - branches: { - master: new RepoAST.Branch("8", null), - foo: new RepoAST.Branch("9", null), - }, - head: "8", - }), - x: S.copy({ - commits: { - "1": new Commit({ - changes: { - "README.md": "hello world" - }, - message: "the first commit", - }), - "8": new Commit({ - parents: ["1"], - changes: { "8": "8" }, - message: "message\n", - }), - "9": new Commit({ - parents: ["1"], - changes: { "9": "9" }, - message: "message\n", - }), - }, - merge: new RepoAST.Merge("foo", "8", "9"), - }), - } - }, "missing commits in cherry-pick": { i: ` a=B:C8-1;C9-1;Bmaster=8;Bfoo=9| diff --git a/node/test/util/status_util.js b/node/test/util/status_util.js index 4b1d2d594..c51e43705 100644 --- a/node/test/util/status_util.js +++ b/node/test/util/status_util.js @@ -36,7 +36,6 @@ const path = require("path"); const NodeGit = require("nodegit"); const CherryPick = require("../../lib/util/cherry_pick"); -const Merge = require("../../lib/util/merge"); const Rebase = require("../../lib/util/rebase"); const RepoAST = require("../../lib/util/repo_ast"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); @@ -114,7 +113,6 @@ describe("StatusUtil", function () { staged: { x: RepoStatus.FILESTATUS.ADDED }, workdir: { y: RepoStatus.FILESTATUS.ADDED }, rebase: new Rebase("foo", "1", "1"), - merge: new Merge("foo", "1", "1"), cherryPick: new CherryPick("1", "1"), sequencerState: new SequencerState({ type: TYPE.MERGE, @@ -132,7 +130,6 @@ describe("StatusUtil", function () { staged: { x: RepoStatus.FILESTATUS.ADDED }, workdir: { y: RepoStatus.FILESTATUS.ADDED }, rebase: new Rebase("foo", "3", "3"), - merge: new Merge("foo", "3", "3"), cherryPick: new CherryPick("3", "3"), sequencerState: new SequencerState({ type: TYPE.MERGE, @@ -585,14 +582,6 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, rebase: new Rebase("master", "2", "3"), }), }, - "merge": { - state: "x=S:C2-1;C3-1;Bfoo=3;Bmaster=2;Mhi\n,2,3", - expected: new RepoStatus({ - headCommit: "2", - currentBranchName: "master", - merge: new Merge("hi\n", "2", "3"), - }), - }, "cherry-pick": { state: "x=S:C2-1;C3-1;Bfoo=3;Bmaster=2;P2,3", expected: new RepoStatus({ @@ -919,12 +908,6 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, }), fails: true, }, - "merge": { - input: new RepoStatus({ - merge: new Merge("foo", "bart", "baz"), - }), - fails: true, - }, "cherry-pick": { input: new RepoStatus({ cherryPick: new CherryPick("foo", "bart"), diff --git a/node/test/util/write_repo_ast_util.js b/node/test/util/write_repo_ast_util.js index 76490e9c8..7e4e174ce 100644 --- a/node/test/util/write_repo_ast_util.js +++ b/node/test/util/write_repo_ast_util.js @@ -342,9 +342,9 @@ S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q", expected: "\ S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q;H=3", }, - "with in-progress merge": "S:Mhello,1,1", "with in-progress cherry-pick": "S:P1,1", "with in-progress sequencer": "S:QR 1:foo 1:bar 0 1", + "sequencer with message": "S:Qfoo#R 1:foo 1:bar 0 1", "headless": { input: new RepoAST(), expected: new RepoAST(), From e86979986c42d50b11b6245d0fbd4b006aa6d0dc Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 23 Mar 2018 17:35:58 -0400 Subject: [PATCH 072/402] Change cherry-pick to use sequencer Addresses card: https://github.com/twosigma/git-meta/projects/2#card-8117326 Depends on: https://github.com/twosigma/git-meta/pull/519 --- node/lib/util/cherry_pick.js | 94 --------------------- node/lib/util/cherry_pick_file_util.js | 105 ------------------------ node/lib/util/cherry_pick_util.js | 59 ++++++++----- node/lib/util/print_status_util.js | 21 ----- node/lib/util/read_repo_ast_util.js | 9 -- node/lib/util/repo_ast.js | 25 ------ node/lib/util/repo_ast_util.js | 23 ------ node/lib/util/repo_status.js | 19 ----- node/lib/util/shorthand_parser_util.js | 42 +--------- node/lib/util/status_util.js | 40 +-------- node/lib/util/write_repo_ast_util.js | 13 --- node/test/util/cherry_pick.js | 71 ---------------- node/test/util/cherry_pick_file_util.js | 82 ------------------ node/test/util/cherry_pick_util.js | 34 ++++---- node/test/util/print_status_util.js | 15 ---- node/test/util/read_repo_ast_util.js | 50 ----------- node/test/util/repo_ast.js | 47 ----------- node/test/util/repo_ast_util.js | 69 ---------------- node/test/util/repo_status.js | 8 -- node/test/util/shorthand_parser_util.js | 74 ----------------- node/test/util/status_util.js | 17 ---- node/test/util/write_repo_ast_util.js | 1 - 22 files changed, 58 insertions(+), 860 deletions(-) delete mode 100644 node/lib/util/cherry_pick.js delete mode 100644 node/lib/util/cherry_pick_file_util.js delete mode 100644 node/test/util/cherry_pick.js delete mode 100644 node/test/util/cherry_pick_file_util.js diff --git a/node/lib/util/cherry_pick.js b/node/lib/util/cherry_pick.js deleted file mode 100644 index 0e0bcd8f4..000000000 --- a/node/lib/util/cherry_pick.js +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2018, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -const assert = require("chai").assert; - -/** - * This module defines the `CherryPick` value-semantic type. - */ - -/** - * @class CherryPick - * - * This class represents an in-progress cherry-pick. - */ -class CherryPick { - /** - * Create a new `CherryPick` object. - * - * @param {String} originalHead - * @param {String} picked - */ - constructor(originalHead, picked) { - assert.isString(originalHead); - assert.isString(picked); - this.d_originalHead = originalHead; - this.d_picked = picked; - Object.freeze(this); - } - - /** - * @property {String} originalHead head commit when cherry-pick started - */ - get originalHead() { - return this.d_originalHead; - } - - /** - * @property {String} picked sha of commit being cherry-picked - */ - get picked() { - return this.d_picked; - } - - /** - * Return true if the specified `rhs` represents the same value as this - * `CherryPick` object and false otherwise. Two `CherryPick` objects - * represent the same value if they have the same `originalHead` and - * `picked` properties. - * - * @param {CherryPick} rhs - * @return {Bool} - */ - equal(rhs) { - assert.instanceOf(rhs, CherryPick); - return this.d_originalHead === rhs.d_originalHead && - this.d_picked === rhs.d_picked; - } -} - -CherryPick.prototype.toString = function () { - return `CherryPick(originalHead=${this.d_originalHead}, \ -picked=${this.d_picked})`; -}; - -module.exports = CherryPick; diff --git a/node/lib/util/cherry_pick_file_util.js b/node/lib/util/cherry_pick_file_util.js deleted file mode 100644 index f67a59784..000000000 --- a/node/lib/util/cherry_pick_file_util.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) 2018, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -const assert = require("chai").assert; -const co = require("co"); -const fs = require("fs-promise"); -const path = require("path"); -const rimraf = require("rimraf"); - -const CherryPick = require("./cherry_pick"); - -/** - * This module contains methods for accessing files that pertain to in-progress - * cherry-picks. - */ - -const metaCherryPickDir = "META_CHERRY_PICK"; -const originalHeadFile = "ORIG_HEAD"; -const pickFile = "PICK"; - -/** - * Return the `CherryPick` object in the specified `gitDir`, if one exists, or - * null if there is no cherry-pick in progress. - * - * @param {String} gitDir - * @return {String|null} - */ -exports.readCherryPick = co.wrap(function *(gitDir) { - assert.isString(gitDir); - let originalHead; - let pick; - const root = path.join(gitDir, metaCherryPickDir); - try { - originalHead = yield fs.readFile(path.join(root, originalHeadFile), - "utf8"); - pick = yield fs.readFile(path.join(root, pickFile), "utf8"); - } - catch (e) { - // TODO: Emit diagnostic if directory exists but is malformed. - return null; - } - return new CherryPick(originalHead.split("\n")[0], pick.split("\n")[0]); -}); - -/** - * Write the specified `cherryPick` to the specified `gitDir`. The behavior is - * undefined if there is already a cherry-pick recorded in `gitDir`. - * - * @param {String} gitDir - * @param {CherryPick} cherryPick - */ -exports.writeCherryPick = co.wrap(function *(gitDir, cherryPick) { - assert.isString(gitDir); - assert.instanceOf(cherryPick, CherryPick); - - const root = path.join(gitDir, metaCherryPickDir); - yield fs.mkdir(root); - yield fs.writeFile(path.join(root, originalHeadFile), - cherryPick.originalHead + "\n"); - yield fs.writeFile(path.join(root, pickFile), - cherryPick.picked + "\n"); -}); - -/** - * Remove files related to a meta cherry-pick. - * - * @param {String} gitDir - */ -exports.cleanCherryPick = co.wrap(function *(gitDir) { - assert.isString(gitDir); - const root = path.join(gitDir, metaCherryPickDir); - const promise = new Promise(callback => { - return rimraf(root, {}, callback); - }); - yield promise; -}); diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 12f463f24..ab62349f1 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -38,8 +38,6 @@ const NodeGit = require("nodegit"); const path = require("path"); const rimraf = require("rimraf"); -const CherryPick = require("./cherry_pick"); -const CherryPickFileUtil = require("./cherry_pick_file_util"); const ConflictUtil = require("./conflict_util"); const DeinitUtil = require("./deinit_util"); const DoWorkQueue = require("./do_work_queue"); @@ -47,6 +45,8 @@ const GitUtil = require("./git_util"); const Open = require("./open"); const RebaseUtil = require("./rebase_util"); const Reset = require("./reset"); +const SequencerState = require("./sequencer_state"); +const SequencerStateUtil = require("./sequencer_state_util"); const StatusUtil = require("./status_util"); const Submodule = require("./submodule"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -54,6 +54,24 @@ const SubmoduleUtil = require("./submodule_util"); const TreeUtil = require("./tree_util"); const UserError = require("./user_error"); +const CommitAndRef = SequencerState.CommitAndRef; +const CHERRY_PICK = SequencerState.TYPE.CHERRY_PICK; + +/** + * Throw a `UserError` if the specfied `seq` is null or does not indicate a + * cherry-pick. + * + * @param {SequencerState|null} seq + */ +function ensureCherryInProgress(seq) { + if (null !== seq) { + assert.instanceOf(seq, SequencerState); + } + if (null === seq || CHERRY_PICK !== seq.type) { + throw new UserError("No cherry-pick in progress."); + } +} + /** * Change the specified `submodules` in the specified index. If a name maps to * a `Submodule`, update it in the specified `index` in the specified `repo` @@ -387,7 +405,7 @@ const finish = co.wrap(function *(repo, commit) { defaultSig, commit.committer(), commit.message()); - yield CherryPickFileUtil.cleanCherryPick(repo.path()); + yield SequencerStateUtil.cleanSequencerState(repo.path()); console.log("Cherry-pick complete."); return metaCommit.tostrS(); }); @@ -452,10 +470,14 @@ Please try with normal 'git cherry-pick'.`); // cherry-pick file. const head = yield metaRepo.getHeadCommit(); - const cherryPick = new CherryPick(head.id().tostrS(), - commit.id().tostrS()); - yield CherryPickFileUtil.writeCherryPick(metaRepo.path(), cherryPick); - + const seq = new SequencerState({ + type: CHERRY_PICK, + originalHead: new CommitAndRef(head.id().tostrS(), null), + target: new CommitAndRef(commit.id().tostrS(), null), + currentCommit: 0, + commits: [commit.id().tostrS()], + }); + yield SequencerStateUtil.writeSequencerState(metaRepo.path(), seq); const opener = new Open.Opener(metaRepo, null); @@ -493,7 +515,7 @@ Please try with normal 'git cherry-pick'.`); if ("" === errorMessage) { if (0 === Object.keys(changes.simpleChanges).length && 0 === Object.keys(picks.commits).length) { - yield CherryPickFileUtil.cleanCherryPick(metaRepo.path()); + yield SequencerStateUtil.cleanSequencerState(metaRepo.path()); throw new UserError("Nothing to commit."); } result.newMetaCommit = yield finish(metaRepo, commit); @@ -519,16 +541,13 @@ exports.continue = co.wrap(function *(repo) { assert.instanceOf(repo, NodeGit.Repository); const status = yield StatusUtil.getRepoStatus(repo); - const cherryPick = status.cherryPick; - if (null === cherryPick) { - throw new UserError("No cherry-pick in progress."); - } + const seq = status.sequencerState; + ensureCherryInProgress(seq); if (status.isConflicted()) { throw new UserError("Resolve conflicts then continue cherry-pick."); } const index = yield repo.index(); - const commit = yield repo.getCommit(cherryPick.picked); - + const commit = yield repo.getCommit(seq.target.sha); const subResult = yield RebaseUtil.continueSubmodules(repo, index, status, @@ -545,7 +564,7 @@ exports.continue = co.wrap(function *(repo) { if (status.isIndexDeepClean() && 0 === Object.keys(subResult.commits).length && 0 === Object.keys(subResult.newCommits).length) { - yield CherryPickFileUtil.cleanCherryPick(repo.path()); + yield SequencerStateUtil.cleanSequencerState(repo.path()); throw new UserError("Nothing to commit."); } @@ -564,12 +583,10 @@ exports.continue = co.wrap(function *(repo) { exports.abort = co.wrap(function *(repo) { assert.instanceOf(repo, NodeGit.Repository); - const cherry = yield CherryPickFileUtil.readCherryPick(repo.path()); - if (null === cherry) { - throw new UserError("No cherry-pick in progress."); - } - const commit = yield repo.getCommit(cherry.originalHead); + const seq = yield SequencerStateUtil.readSequencerState(repo.path()); + ensureCherryInProgress(seq); + const commit = yield repo.getCommit(seq.originalHead.sha); yield Reset.reset(repo, commit, Reset.TYPE.MERGE); - yield CherryPickFileUtil.cleanCherryPick(repo.path()); + yield SequencerStateUtil.cleanSequencerState(repo.path()); console.log("Cherry-pick aborted."); }); diff --git a/node/lib/util/print_status_util.js b/node/lib/util/print_status_util.js index 1504040e3..077c68141 100644 --- a/node/lib/util/print_status_util.js +++ b/node/lib/util/print_status_util.js @@ -41,7 +41,6 @@ const colors = require("colors/safe"); const path = require("path"); const GitUtil = require("./git_util"); -const CherryPick = require("./cherry_pick"); const SequencerState = require("./sequencer_state"); const Rebase = require("./rebase"); const RepoStatus = require("./repo_status"); @@ -365,22 +364,6 @@ You are currently rebasing branch '${rebase.headName}' on '${shortSha}'. `; }; -/** - * Return a message describing the specified `cherryPick`. - * - * @param {CherryPick} - * @return {String} - */ -function printCherryPick(cherryPick) { - assert.instanceOf(cherryPick, CherryPick); - return `\ -A cherry-pick is in progress. - (after resolving conflicts mark the corrected paths - with 'git meta add', then run "git meta cherry-pick --continue") - (use "git meta cherry-pick --abort" to check out the original branch) -`; -} - /** * Return the command to which the specified sequencer `type` corresponds. * @@ -449,10 +432,6 @@ exports.printRepoStatus = function (status, cwd) { result += exports.printCurrentBranch(status); - if (null !== status.cherryPick) { - result += printCherryPick(status.cherryPick); - } - if (null !== status.sequencerState) { result += exports.printSequencer(status.sequencerState); } diff --git a/node/lib/util/read_repo_ast_util.js b/node/lib/util/read_repo_ast_util.js index 1ceac9551..57f11c523 100644 --- a/node/lib/util/read_repo_ast_util.js +++ b/node/lib/util/read_repo_ast_util.js @@ -42,7 +42,6 @@ const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); -const CherryPickFileUtil = require("./cherry_pick_file_util"); const RepoAST = require("./repo_ast"); const RebaseFileUtil = require("./rebase_file_util"); const SequencerStateUtil = require("./sequencer_state_util"); @@ -542,13 +541,6 @@ exports.readRAST = co.wrap(function *(repo) { yield loadCommit(NodeGit.Oid.fromString(rebase.onto)); } - const cherryPick = yield CherryPickFileUtil.readCherryPick(repo.path()); - - if (null !== cherryPick) { - yield loadCommit(NodeGit.Oid.fromString(cherryPick.originalHead)); - yield loadCommit(NodeGit.Oid.fromString(cherryPick.picked)); - } - const sequencer = yield SequencerStateUtil.readSequencerState(repo.path()); if (null !== sequencer) { @@ -570,7 +562,6 @@ exports.readRAST = co.wrap(function *(repo) { workdir: workdir, openSubmodules: openSubmodules, rebase: rebase, - cherryPick: cherryPick, sequencerState: sequencer, bare: bare, }); diff --git a/node/lib/util/repo_ast.js b/node/lib/util/repo_ast.js index c3fa5b7c9..0e74a42c1 100644 --- a/node/lib/util/repo_ast.js +++ b/node/lib/util/repo_ast.js @@ -38,7 +38,6 @@ const deeper = require("deeper"); const deepCopy = require("deepcopy"); const Rebase = require("./rebase"); -const CherryPick = require("./cherry_pick"); const SequencerState = require("./sequencer_state"); /** @@ -393,7 +392,6 @@ class AST { * @param {Object} [args.notes] * @param {Object} [args.openSubmodules] * @param {Rebase} [args.rebase] - * @param {CherryPick} [args.cherryPick] * @param {SequencerState} [args.sequencerState] */ constructor(args) { @@ -560,19 +558,6 @@ in commit ${id}.`); } } - this.d_cherryPick = null; - if ("cherryPick" in args) { - const cherryPick = args.cherryPick; - if (null !== cherryPick) { - assert.instanceOf(cherryPick, CherryPick); - assert.isFalse(this.d_bare); - checkAndTraverse(cherryPick.originalHead, - "original head of cherry-pick"); - checkAndTraverse(cherryPick.picked, "picked commit"); - this.d_cherryPick = cherryPick; - } - } - this.d_sequencerState = null; if ("sequencerState" in args) { const sequencerState = args.sequencerState; @@ -761,13 +746,6 @@ in commit ${id}.`); return this.d_rebase; } - /** - * @property {CherryPick} null unless a cherry-pick is in progress - */ - get cherryPick() { - return this.d_cherryPick; - } - /** * @property {SequencerState} null unless a sequence operation is ongoing */ @@ -834,8 +812,6 @@ in commit ${id}.`); openSubmodules: ("openSubmodules" in args) ? args.openSubmodules : this.d_openSubmodules, rebase: ("rebase" in args) ? args.rebase : this.d_rebase, - cherryPick: ("cherryPick" in args) ? - args.cherryPick : this.d_cherryPick, sequencerState: ("sequencerState" in args) ? args.sequencerState: this.d_sequencerState, bare: ("bare" in args) ? args.bare : this.d_bare, @@ -921,7 +897,6 @@ AST.Branch = Branch; AST.Commit = Commit; AST.Conflict = Conflict; AST.Rebase = Rebase; -AST.CherryPick = CherryPick; AST.Remote = Remote; AST.SequencerState = SequencerState; AST.Submodule = Submodule; diff --git a/node/lib/util/repo_ast_util.js b/node/lib/util/repo_ast_util.js index ce0664c48..9b60b9487 100644 --- a/node/lib/util/repo_ast_util.js +++ b/node/lib/util/repo_ast_util.js @@ -489,21 +489,6 @@ ${colorExp(expected.rebase.onto)} but got ${colorAct(actual.rebase.onto)}.`); } } - // Check cherry-pick - - if (null === actual.cherryPick && null !== expected.cherryPick) { - result.push("Missing cherry-pick."); - } - else if (null !== actual.cherryPick && null === expected.cherryPick) { - result.push("Unexpected cherry-pick."); - } - else if (null !== actual.cherryPick && - !actual.cherryPick.equal(expected.cherryPick)) { - result.push(`\ -Expected cherry pick to be ${actual.cherryPick} but got \ -${expected.cherryPick}`); - } - // Check sequencer if (null === actual.sequencerState && null !== expected.sequencerState) { @@ -761,13 +746,6 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { mapCommitId(rebase.onto)); } - let cherryPick = ast.cherryPick; - if (null !== cherryPick) { - cherryPick = new RepoAST.CherryPick( - mapCommitId(cherryPick.originalHead), - mapCommitId(cherryPick.picked)); - } - let sequencer = ast.sequencerState; if (null !== sequencer) { const original = mapCommitAndRef(sequencer.originalHead); @@ -795,7 +773,6 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { workdir: mapChanges(ast.workdir), openSubmodules: openSubmodules, rebase: rebase, - cherryPick: cherryPick, sequencerState: sequencer, }); }; diff --git a/node/lib/util/repo_status.js b/node/lib/util/repo_status.js index 9b21a7b88..22bb1f23b 100644 --- a/node/lib/util/repo_status.js +++ b/node/lib/util/repo_status.js @@ -32,7 +32,6 @@ const assert = require("chai").assert; const Rebase = require("./rebase"); -const CherryPick = require("./cherry_pick"); const SequencerState = require("./sequencer_state"); /** @@ -446,7 +445,6 @@ class RepoStatus { * @param {Object} [args.submodules] map from name to `Submodule` * @param {Object} [args.workdir] map from name to `FILESTATUS` * @param {Rebase} [args.rebase] rebase, if one is in progress - * @param {CherryPick} [args.cherryPick] cherry-pick, if one is in progress * @param {SequencerState} * [args.sequencerState] state of sequencer */ @@ -463,7 +461,6 @@ class RepoStatus { this.d_workdir = {}; this.d_submodules = {}; this.d_rebase = null; - this.d_cherryPick = null; this.d_sequencerState = null; if ("currentBranchName" in args) { @@ -510,13 +507,6 @@ class RepoStatus { this.d_rebase = rebase; } - if ("cherryPick" in args) { - const cherryPick = args.cherryPick; - if (null !== cherryPick) { - assert.instanceOf(cherryPick, CherryPick); - } - this.d_cherryPick = cherryPick; - } if ("sequencerState" in args) { const sequencerState = args.sequencerState; if (null !== sequencerState) { @@ -695,13 +685,6 @@ class RepoStatus { return this.d_rebase; } - /** - * @property {CherryPick} cherryPick if ~null, state of cherryPick - */ - get cherryPick() { - return this.d_cherryPick; - } - /** * @property {SequencerState} sequencerState if ~null, state of sequencer */ @@ -732,8 +715,6 @@ class RepoStatus { args.submodules: this.d_submodules, workdir: ("workdir" in args) ? args.workdir : this.d_workdir, rebase: ("rebase" in args) ? args.rebase : this.d_rebase, - cherryPick: ("cherryPick" in args) ? - args.cherryPick : this.d_cherryPick, sequencerState: ("sequencerState" in args) ? args.sequencerState : this.d_sequencerState, }); diff --git a/node/lib/util/shorthand_parser_util.js b/node/lib/util/shorthand_parser_util.js index e3ae4e3d7..8777298e8 100644 --- a/node/lib/util/shorthand_parser_util.js +++ b/node/lib/util/shorthand_parser_util.js @@ -60,8 +60,7 @@ const SequencerState = RepoAST.SequencerState; * base repo type = 'S' | 'B' | ('C') | 'A' | 'N' * override = | | | | * | | | | - * | | | - * + * | | * head = 'H='| nothing means detached * nothing = * commit = + @@ -81,7 +80,6 @@ const SequencerState = RepoAST.SequencerState; * [' '=[](',\s*'=[])*] * note = N =message * rebase = E,, - * cherry-pick = P, * sequencer = Q[message'#']' '' ' * ' '' '(',')* * sequencer type = C | M | R @@ -188,8 +186,6 @@ const SequencerState = RepoAST.SequencerState; * -- file `x` to be `y`. * S:Emaster,1,2 -- rebase in progress started on "master", * original head "1", onto commit "2" - * S:CP1,2 -- cherry-pick in progress started with - * original head "1", picking commit "2" * S:I *foo=a*b*c -- The file 'foo' is flagged in the index * as conflicted, having a base content of 'a', * "our" content as 'b', and "their" content as @@ -389,7 +385,6 @@ function prepareASTArguments(baseAST, rawRepo) { workdir: baseAST.workdir, openSubmodules: baseAST.openSubmodules, rebase: baseAST.rebase, - cherryPick: baseAST.cherryPick, sequencerState: baseAST.sequencerState, bare: baseAST.bare, }; @@ -503,12 +498,6 @@ function prepareASTArguments(baseAST, rawRepo) { resultArgs.rebase = rawRepo.rebase; } - // Override cherry-pick if provided - - if ("cherryPick" in rawRepo) { - resultArgs.cherryPick = rawRepo.cherryPick; - } - if ("sequencerState" in rawRepo) { resultArgs.sequencerState = rawRepo.sequencerState; } @@ -590,7 +579,6 @@ function parseOverrides(shorthand, begin, end, delimiter) { let notes = {}; let openSubmodules = {}; let rebase; - let cherryPick; let sequencer; /** @@ -958,26 +946,6 @@ function parseOverrides(shorthand, begin, end, delimiter) { rebase = new RepoAST.Rebase(parts[0], parts[1], parts[2]); } - /** - * Parse the cherry-pick definition beginning at the specified `begin` and - * terminating at the specified `end`. - * - * @param {Number} begin - * @param {Number} end - */ - function parseCherryPick(begin, end) { - if (begin === end) { - cherryPick = null; - return; // RETURN - } - const cherryPickDef = shorthand.substr(begin, end - begin); - const parts = cherryPickDef.split(","); - assert.equal(parts.length, - 2, - `Wrong number of cherry-pick parts in ${cherryPickDef}`); - cherryPick = new RepoAST.CherryPick(parts[0], parts[1]); - } - /** * Parse the sequencer definition beginning at the specified `begin` and * terminating at the specified `end`. @@ -1045,7 +1013,6 @@ function parseOverrides(shorthand, begin, end, delimiter) { case "H": return parseHeadOverride; case "R": return parseRemote; case "I": return parseIndex; - case "P": return parseCherryPick; case "N": return parseNote; case "W": return parseWorkdir; case "O": return parseOpenSubmodule; @@ -1092,9 +1059,6 @@ function parseOverrides(shorthand, begin, end, delimiter) { if (undefined !== rebase) { result.rebase = rebase; } - if (undefined !== cherryPick) { - result.cherryPick = cherryPick; - } if (undefined !== sequencer) { result.sequencerState = sequencer; } @@ -1354,10 +1318,6 @@ exports.parseMultiRepoShorthand = function (shorthand, existingRepos) { includeCommit(resultArgs.rebase.originalHead); includeCommit(resultArgs.rebase.onto); } - if (resultArgs.cherryPick) { - includeCommit(resultArgs.cherryPick.originalHead); - includeCommit(resultArgs.cherryPick.picked); - } if (resultArgs.sequencerState) { includeCommit(resultArgs.sequencerState.originalHead.sha); includeCommit(resultArgs.sequencerState.target.sha); diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 03164db2e..0309affac 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -40,8 +40,6 @@ const assert = require("chai").assert; const co = require("co"); const NodeGit = require("nodegit"); -const CherryPick = require("./cherry_pick"); -const CherryPickFileUtil = require("./cherry_pick_file_util"); const DiffUtil = require("./diff_util"); const GitUtil = require("./git_util"); const PrintStatusUtil = require("./print_status_util"); @@ -119,30 +117,6 @@ function remapRebase(rebase, commitMap) { return new Rebase(rebase.headName, originalHead, onto); } -/** - * Return a new `CherryPick` object having the same value as the specified - * `cherryPick` but with commit shas being replaced by commits in the specified - * `commitMap`. - * - * @param {CherryPick} cherryPick - * @param {Object} commitMap from sha to sha - * @return {CherryPick} - */ -function remapCherryPick(cherryPick, commitMap) { - assert.instanceOf(cherryPick, CherryPick); - assert.isObject(commitMap); - - let originalHead = cherryPick.originalHead; - let picked = cherryPick.picked; - if (originalHead in commitMap) { - originalHead = commitMap[originalHead]; - } - if (picked in commitMap) { - picked = commitMap[picked]; - } - return new CherryPick(originalHead, picked); -} - /** * Return a new `RepoStatus` object having the same value as the specified * `status` but with all commit shas replaced by commits in the specified @@ -178,9 +152,6 @@ exports.remapRepoStatus = function (status, commitMap, urlMap) { workdir: status.workdir, rebase: status.rebase === null ? null : remapRebase(status.rebase, commitMap), - cherryPick: status.cherryPick === null ? - null : - remapCherryPick(status.cherryPick, commitMap), sequencerState: status.sequencerState === null ? null : SequencerStateUtil.mapCommits(status.sequencerState, @@ -472,7 +443,6 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { args.rebase = rebase; } - args.cherryPick = yield CherryPickFileUtil.readCherryPick(repo.path()); args.sequencerState = yield SequencerStateUtil.readSequencerState(repo.path()); @@ -611,8 +581,8 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { /** * Throw a `UserError` unless the specified `status` reflects a repository in a * normal, ready state, that is, it does not have any conflicts or in-progress - * operations such as a rebase or cherry-pick. Adjust output paths to - * be relative to the specified `cwd`. + * operations from the sequencer. Adjust output paths to be relative to the + * specified `cwd`. * * @param {RepoStatus} status */ @@ -624,12 +594,6 @@ exports.ensureReady = function (status) { Before proceeding, you must complete the rebase in progress (by running 'git meta rebase --continue') or abort it (by running 'git meta rebase --abort').`); - } - if (null !== status.cherryPick) { - throw new UserError(`\ -Before proceeding, you must complete the cherry-pick in progress (by running -'git meta cherry-pick --continue') or abort it (by running -'git meta cherry-pick --abort').`); } if (status.isConflicted()) { throw new UserError(`\ diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index 837dffa64..1912d382a 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -45,7 +45,6 @@ const mkdirp = require("mkdirp"); const NodeGit = require("nodegit"); const path = require("path"); -const CherryPickFileUtil = require("./cherry_pick_file_util"); const ConflictUtil = require("./conflict_util"); const DoWorkQueue = require("./do_work_queue"); const RebaseFileUtil = require("./rebase_file_util"); @@ -466,18 +465,6 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { indexHead = rebase.onto; } - // Write out the cherry-pick info, if it exists. - - if (null !== ast.cherryPick) { - // Map commits - - const originalSha = commitMap[ast.cherryPick.originalHead]; - const pickSha = commitMap[ast.cherryPick.picked]; - const cherryPick = new RepoAST.CherryPick(originalSha, - pickSha); - yield CherryPickFileUtil.writeCherryPick(repo.path(), cherryPick); - } - // Write out sequencer state if there is one. const sequencer = ast.sequencerState; if (null !== sequencer) { diff --git a/node/test/util/cherry_pick.js b/node/test/util/cherry_pick.js deleted file mode 100644 index dd16c030e..000000000 --- a/node/test/util/cherry_pick.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2018, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -const assert = require("chai").assert; - -const CherryPick = require("../../lib/util/cherry_pick"); - -describe("CherryPick", function () { - it("constructor", function () { - const c = new CherryPick("12", "32"); - assert.instanceOf(c, CherryPick); - assert.isFrozen(c); - assert.equal(c.originalHead, "12"); - assert.equal(c.picked, "32"); - }); - describe("equal", function () { - const cases = { - "same": { - lhs: new CherryPick("1", "2"), - rhs: new CherryPick("1", "2"), - expected: true, - }, - "diff original head": { - lhs: new CherryPick("1", "2"), - rhs: new CherryPick("2", "2"), - expected: false, - }, - "diff pick": { - lhs: new CherryPick("1", "2"), - rhs: new CherryPick("1", "1"), - expected: false, - }, - }; - Object.keys(cases).forEach(caseName => { - const c = cases[caseName]; - it(caseName, function () { - const result = c.lhs.equal(c.rhs); - assert.equal(result, c.expected); - }); - }); - }); -}); diff --git a/node/test/util/cherry_pick_file_util.js b/node/test/util/cherry_pick_file_util.js deleted file mode 100644 index e4e197fb1..000000000 --- a/node/test/util/cherry_pick_file_util.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2018, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -const assert = require("chai").assert; -const co = require("co"); -const fs = require("fs-promise"); -const path = require("path"); - -const CherryPickFileUtil = require("../../lib//util/cherry_pick_file_util"); -const CherryPick = require("../../lib//util/cherry_pick"); -const TestUtil = require("../../lib/util/test_util"); - -describe("CherryPickFileUtil", function () { - describe("readCherryPick", function () { - it("found", co.wrap(function *() { - const tempDir = yield TestUtil.makeTempDir(); - const gitDir = path.join(tempDir, "META_CHERRY_PICK"); - yield fs.mkdir(gitDir); - yield fs.writeFile(path.join(gitDir, "ORIG_HEAD"), "123\n"); - yield fs.writeFile(path.join(gitDir, "PICK"), "456\n"); - const result = yield CherryPickFileUtil.readCherryPick(tempDir); - assert.deepEqual(result, new CherryPick("123", "456")); - })); - it("not found", co.wrap(function *() { - const tempDir = yield TestUtil.makeTempDir(); - const gitDir = path.join(tempDir, "META_CHERRY_PICK"); - yield fs.mkdir(gitDir); - yield fs.writeFile(path.join(gitDir, "ORIG_HEAD"), "123\n"); - const result = yield CherryPickFileUtil.readCherryPick(tempDir); - assert.isNull(result); - })); - }); - it("writeCherryPick", co.wrap(function *() { - const tempDir = yield TestUtil.makeTempDir(); - const cherryPick = new CherryPick("bar", "baz"); - yield CherryPickFileUtil.writeCherryPick(tempDir, cherryPick); - const root = path.join(tempDir, "META_CHERRY_PICK"); - const originalHead = yield fs.readFile(path.join(root, "ORIG_HEAD"), - "utf8"); - assert.equal(originalHead, "bar\n"); - const pick = yield fs.readFile(path.join(root, "PICK"), "utf8"); - assert.equal(pick, "baz\n"); - })); - it("cleanCherryPick", co.wrap(function *() { - const tempDir = yield TestUtil.makeTempDir(); - yield CherryPickFileUtil.writeCherryPick( - tempDir, - new CherryPick("there", "all")); - yield CherryPickFileUtil.cleanCherryPick(tempDir); - const result = yield CherryPickFileUtil.readCherryPick(tempDir); - assert.isNull(result); - })); -}); diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index ccc0e3884..b1c5c3837 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -550,7 +550,7 @@ x=S:C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2;Os H=x`, "in-progress will fail": { input: ` a=Ax:Cz-y;Cy-x;Bfoo=z| -x=S:P2,1;C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2`, +x=S:QC 2: 1: 0 2;C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2`, fails: true, }, "dirty will fail": { @@ -630,7 +630,7 @@ x=E:C9-2 t=Sa:qt;Bmaster=9;Ot Cqt-1 q=q!H=qt`, a=B:Ca-1;Cb-1 a=8;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;C8-2 s=Sa:b;Bmaster=3;Bfoo=8`, expected: ` -x=E:P3,8;Os Edetached HEAD,b,a!I *a=~*a*8!W a=\ +x=E:QC 3: 8: 0 8;Os Edetached HEAD,b,a!I *a=~*a*8!W a=\ <<<<<<< HEAD a ======= @@ -647,7 +647,7 @@ Submodule ${colors.red("s")} is conflicted. a=B:Ca-1;Cb-1 a=8;Cc-1;Ba=a;Bb=b;Bc=c| x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:a,t=Sa:a;C8-2 s=Sa:b,t=Sa:c;Bmaster=3;Bfoo=8`, expected: ` -x=E:P3,8;I t=Sa:ct;Ot Cct-a c=c!H=ct; +x=E:QC 3: 8: 0 8;I t=Sa:ct;Ot Cct-a c=c!H=ct; Os Edetached HEAD,b,a!I *a=~*a*8!W a=\ <<<<<<< HEAD a @@ -664,7 +664,7 @@ Submodule ${colors.red("s")} is conflicted. input: ` a=B:Ca-1;Ba=a| x=U:C3-2 s=foo;C8-2 s=Sa:a;Bmaster=3;Bfoo=8`, - expected: `x=E:P3,8;I *s=S:1*foo*S:a;W s=foo`, + expected: `x=E:QC 3: 8: 0 8;I *s=S:1*foo*S:a;W s=foo`, errorMessage: `\ Conflicting entries for submodule ${colors.red("s")} `, @@ -725,29 +725,29 @@ describe("continue", function () { fails: true, }, "conflicted": { - input: "a=B|x=U:Os I foo=bar,*x=a*b*c;Cfoo#g;Bg=g;P2,g", + input: "a=B|x=U:Os I foo=bar,*x=a*b*c;Cfoo#g;Bg=g;QC 2: g: 0 g", fails: true, }, "continue with a staged submodule commit": { - input: "a=B:Ca-1;Ba=a|x=U:I s=Sa:a;Cmoo#g;Bg=g;P2,g", - expected: "x=E:Cmoo#CP-2 s=Sa:a;Bmaster=CP;P;I s=~", + input: "a=B:Ca-1;Ba=a|x=U:I s=Sa:a;Cmoo#g;Bg=g;QC 2: g: 0 g", + expected: "x=E:Cmoo#CP-2 s=Sa:a;Bmaster=CP;Q;I s=~", }, "regular continue": { input: ` a=B:Ca-1;Cb-1;Ba=a;Bb=b| -x=U:C3-2 s=Sa:b;Cfoo#g-2;Bg=g;P1,g;Bmaster=3;Os EHEAD,b,a!I b=b`, +x=U:C3-2 s=Sa:b;Cfoo#g-2;Bg=g;QC 1: g: 0 g;Bmaster=3;Os EHEAD,b,a!I b=b`, expected: ` -x=E:P;Cfoo#CP-3 s=Sa:bs;Bmaster=CP;Os Cbs-a b=b!E`, +x=E:Q;Cfoo#CP-3 s=Sa:bs;Bmaster=CP;Os Cbs-a b=b!E`, }, "nothing to do": { - input: "a=B|x=U:Os;Cfoo#g;Bg=g;P2,g", - expected: "x=E:P", + input: "a=B|x=U:Os;Cfoo#g;Bg=g;QC 2: g: 0 g", + expected: "x=E:Q", fails: true, }, "continue with staged files": { - input: "a=B|x=U:Os I foo=bar;Cfoo#g;Bg=g;P2,g", + input: "a=B|x=U:Os I foo=bar;Cfoo#g;Bg=g;QC 2: g: 0 g", expected: ` -x=E:Cfoo#CP-2 s=Sa:Ns;Bmaster=CP;Os Cfoo#Ns-1 foo=bar;P`, +x=E:Cfoo#CP-2 s=Sa:Ns;Bmaster=CP;Os Cfoo#Ns-1 foo=bar;Q`, }, }; Object.keys(cases).forEach(caseName => { @@ -787,19 +787,19 @@ describe("abort", function () { fails: true, }, "some changes in meta": { - input: "x=S:C2-1;Bmaster=2;P1,2", + input: "x=S:C2-1;Bmaster=2;QC 1: 2: 0 2", expected: "x=S", }, "some conflicted changes in meta": { input: ` -x=S:C2-1;Bmaster=2;P1,2;I *README.md=b*c*d;W README.md=8`, +x=S:C2-1;Bmaster=2;QC 1: 2: 0 2;I *README.md=b*c*d;W README.md=8`, expected: "x=S", }, "sub with a conflict": { input: ` a=B:Ca-1;Cb-1 a=8;Ba=a;Bb=b| -x=U:P3,8;C3-2 s=Sa:a;C8-2 s=Sa:b;Bmaster=3;Bfoo=8; +x=U:QC 3: 8: 0 8;C3-2 s=Sa:a;C8-2 s=Sa:b;Bmaster=3;Bfoo=8; Os Ba=a!Bb=b!Edetached HEAD,b,a!I *a=~*a*8!W a=\ <<<<<<< HEAD a @@ -808,7 +808,7 @@ a >>>>>>> message ; `, - expected: `x=E:P;Os E!I a=~!W a=~!Ba=a!Bb=b`, + expected: `x=E:Q;Os E!I a=~!W a=~!Ba=a!Bb=b`, } }; Object.keys(cases).forEach(caseName => { diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js index 85234e045..accf93264 100644 --- a/node/test/util/print_status_util.js +++ b/node/test/util/print_status_util.js @@ -35,7 +35,6 @@ const colors = require("colors"); const NodeGit = require("nodegit"); const Rebase = require("../../lib/util/rebase"); -const CherryPick = require("../../lib/util/cherry_pick"); const RepoStatus = require("../../lib/util/repo_status"); const PrintStatusUtil = require("../../lib/util/print_status_util"); const SequencerState = require("../../lib/util/sequencer_state"); @@ -747,20 +746,6 @@ A merge is in progress. exact: `\ On branch ${colors.green("master")}. nothing to commit, working tree clean -`, - }, - "cherry-pick": { - input: new RepoStatus({ - currentBranchName: "master", - cherryPick: new CherryPick("1", "1"), - }), - exact: `\ -On branch ${colors.green("master")}. -A cherry-pick is in progress. - (after resolving conflicts mark the corrected paths - with 'git meta add', then run "git meta cherry-pick --continue") - (use "git meta cherry-pick --abort" to check out the original branch) -nothing to commit, working tree clean `, }, "sequencer": { diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index b18e5b83e..b07a01c64 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -36,8 +36,6 @@ const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); -const CherryPick = require("../../lib/util/cherry_pick"); -const CherryPickFileUtil = require("../../lib/util/cherry_pick_file_util"); const ConflictUtil = require("../../lib/util/conflict_util"); const DeinitUtil = require("../../lib/util/deinit_util"); const Rebase = require("../../lib/util/rebase"); @@ -1451,54 +1449,6 @@ describe("readRAST", function () { assert.equal(rebase.onto, secondCommit.id().tostrS()); })); - it("cherry-pick", co.wrap(function *() { - // Start out with a base repo having two branches, "master", and "foo", - // foo having one commit on top of master. - - const start = yield repoWithCommit(); - const r = start.repo; - - // Switch to master - - yield r.checkoutBranch("master"); - - const head = yield r.getHeadCommit(); - const sha = head.id().tostrS(); - - const cherryPick = new CherryPick(sha, sha); - - const original = yield ReadRepoASTUtil.readRAST(r); - const expected = original.copy({ - cherryPick: cherryPick, - }); - - yield CherryPickFileUtil.writeCherryPick(r.path(), cherryPick); - - const actual = yield ReadRepoASTUtil.readRAST(r); - - RepoASTUtil.assertEqualASTs(actual, expected); - })); - - it("cherry-pick - unreachable", co.wrap(function *() { - const r = yield TestUtil.createSimpleRepository(); - r.detachHead(); - const secondCommit = yield TestUtil.generateCommit(r); - const thirdCommit = yield TestUtil.generateCommit(r); - - // Then begin a cherry-pick. - - const cherryPick = new CherryPick(secondCommit.id().tostrS(), - thirdCommit.id().tostrS()); - yield CherryPickFileUtil.writeCherryPick(r.path(), cherryPick); - - // Remove the branches, making the commits reachable only from the - // rebase. - - const ast = yield ReadRepoASTUtil.readRAST(r); - const actualCherryPick = ast.cherryPick; - assert.deepEqual(actualCherryPick, cherryPick); - })); - it("sequencer", co.wrap(function *() { // Start out with a base repo having two branches, "master", and "foo", // foo having one commit on top of master. diff --git a/node/test/util/repo_ast.js b/node/test/util/repo_ast.js index 033b20033..25316c089 100644 --- a/node/test/util/repo_ast.js +++ b/node/test/util/repo_ast.js @@ -245,7 +245,6 @@ const REBASE = SequencerState.TYPE.REBASE; const Commit = RepoAST.Commit; const Rebase = RepoAST.Rebase; - const CherryPick = RepoAST.CherryPick; const Remote = RepoAST.Remote; const c1 = new Commit(); @@ -274,8 +273,6 @@ const REBASE = SequencerState.TYPE.REBASE; eopenSubmodules: ("openSubmodules" in expected) ? expected.openSubmodules : {}, erebase: ("rebase" in expected) ? expected.rebase : null, - echerryPick: ("cherryPick" in expected) ? - expected.cherryPick : null, esequencerState: ("sequencerState" in expected) ? expected.sequencerState : null, fails : fails, @@ -291,7 +288,6 @@ const REBASE = SequencerState.TYPE.REBASE; head: null, currentBranchName: null, rebase: null, - cherryPick: null, sequencerState: null, bare: false, }, @@ -316,12 +312,6 @@ const REBASE = SequencerState.TYPE.REBASE; commits: {"1": c1 }, head: "1", }, undefined, true), - "bad bare with cherry": m({ - bare: true, - cherryPick: new CherryPick("1", "1"), - commits: {"1": c1 }, - head: "1", - }, undefined, true), "bad bare with sequencer": m({ bare: true, sequencerState: new SequencerState({ @@ -603,19 +593,6 @@ const REBASE = SequencerState.TYPE.REBASE; "bad rebase": m({ rebase: new Rebase("fff", "1", "1"), }, undefined, true), - "with cherry": m({ - commits: { - "1": new Commit(), - }, - head: "1", - cherryPick: new CherryPick( "1", "1"), - }, { - commits: { - "1": new Commit(), - }, - head: "1", - cherryPick: new CherryPick("1", "1"), - }), "with sequencer state": m({ commits: { "1": new Commit(), @@ -645,21 +622,6 @@ const REBASE = SequencerState.TYPE.REBASE; currentCommit: 1, }), }), - "with cherry-pick specific commits": m({ - commits: { - "1": new Commit(), - "2": new Commit(), - }, - head: "1", - cherryPick: new CherryPick("2", "2"), - }, { - commits: { - "1": new Commit(), - "2": new Commit(), - }, - head: "1", - cherryPick: new CherryPick("2", "2"), - }), "with sequencer specific commits": m({ commits: { "1": new Commit(), @@ -689,9 +651,6 @@ const REBASE = SequencerState.TYPE.REBASE; currentCommit: 1, }), }), - "bad cherry-pick": m({ - cherryPick: new CherryPick("1", "1"), - }, undefined, true), "bad sequencer": m({ sequencerState: new SequencerState({ type: REBASE, @@ -723,7 +682,6 @@ const REBASE = SequencerState.TYPE.REBASE; assert.deepEqual(obj.workdir, c.eworkdir); assert.deepEqual(obj.openSubmodules, c.eopenSubmodules); assert.deepEqual(obj.rebase, c.erebase); - assert.deepEqual(obj.cherryPick, c.echerryPick); assert.deepEqual(obj.sequencerState, c.esequencerState); assert.equal(obj.bare, c.ebare); @@ -835,7 +793,6 @@ const REBASE = SequencerState.TYPE.REBASE; describe("AST.copy", function () { const Rebase = RepoAST.Rebase; - const CherryPick = RepoAST.CherryPick; const base = new RepoAST({ commits: { "1": new RepoAST.Commit()}, branches: { "master": new RepoAST.Branch("1", null) }, @@ -845,7 +802,6 @@ const REBASE = SequencerState.TYPE.REBASE; index: { foo: "bar" }, workdir: { foo: "bar" }, rebase: new Rebase("hello", "1", "1"), - cherryPick: new CherryPick("1", "1"), sequencerState: new SequencerState({ type: REBASE, originalHead: new CommitAndRef("1", null), @@ -865,7 +821,6 @@ const REBASE = SequencerState.TYPE.REBASE; index: { foo: "bar" }, workdir: { foo: "bar" }, rebase: new Rebase("hello world", "2", "2"), - cherryPick: new CherryPick("2", "2"), sequencerState: new SequencerState({ type: REBASE, originalHead: new CommitAndRef("2", "refs/heads/master"), @@ -890,7 +845,6 @@ const REBASE = SequencerState.TYPE.REBASE; index: {}, workdir: {}, rebase: null, - cherryPick: null, sequencerState: null, }, e: new RepoAST({ @@ -917,7 +871,6 @@ const REBASE = SequencerState.TYPE.REBASE; assert.deepEqual(obj.workdir, c.e.workdir); assert.deepEqual(obj.openSubmodules, c.e.openSubmodules); assert.deepEqual(obj.rebase, c.e.rebase); - assert.deepEqual(obj.cherryPick, c.e.cherryPick); assert.deepEqual(obj.sequencerState, c.e.sequencerState); assert.equal(obj.bare, c.e.bare); }); diff --git a/node/test/util/repo_ast_util.js b/node/test/util/repo_ast_util.js index 89abcddce..258122b67 100644 --- a/node/test/util/repo_ast_util.js +++ b/node/test/util/repo_ast_util.js @@ -138,7 +138,6 @@ describe("RepoAstUtil", function () { const Conflict = RepoAST.Conflict; const Commit = AST.Commit; const Rebase = AST.Rebase; - const CherryPick = AST.CherryPick; const Remote = AST.Remote; const Submodule = AST.Submodule; @@ -171,7 +170,6 @@ describe("RepoAstUtil", function () { workdir: { foo: "bar" }, openSubmodules: { y: anAST }, rebase: new Rebase("foo", "1", "1"), - cherryPick: new CherryPick("1", "1"), sequencerState: new Sequencer({ type: MERGE, originalHead: new CommitAndRef("1", null), @@ -192,7 +190,6 @@ describe("RepoAstUtil", function () { workdir: { foo: "bar" }, openSubmodules: { y: anAST }, rebase: new Rebase("foo", "1", "1"), - cherryPick: new CherryPick("1", "1"), sequencerState: new Sequencer({ type: MERGE, originalHead: new CommitAndRef("1", null), @@ -546,45 +543,6 @@ describe("RepoAstUtil", function () { }), fails: true, }, - "missing cherry": { - actual: new AST({ - commits: { "1": aCommit}, - head: "1", - cherryPick: new CherryPick("1", "1"), - }), - expected: new AST({ - commits: { "1": aCommit}, - head: "1", - }), - fails: true, - }, - "unexpected cherry": { - actual: new AST({ - commits: { "1": aCommit}, - head: "1", - }), - expected: new AST({ - commits: { "1": aCommit}, - head: "1", - cherryPick: new CherryPick("1", "1"), - }), - fails: true, - }, - "wrong cherry": { - actual: new AST({ - commits: { "1": aCommit, "2": aCommit}, - head: "1", - branches: { master: new RepoAST.Branch("2", null), }, - cherryPick: new CherryPick("2", "1"), - }), - expected: new AST({ - commits: { "1": aCommit, "2": aCommit}, - head: "1", - branches: { master: new RepoAST.Branch("2", null), }, - cherryPick: new CherryPick("1", "1"), - }), - fails: true, - }, "missing sequencer": { actual: new AST({ commits: { "1": aCommit}, @@ -704,7 +662,6 @@ describe("RepoAstUtil", function () { describe("mapCommitsAndUrls", function () { const Commit = RepoAST.Commit; const Rebase = RepoAST.Rebase; - const CherryPick = RepoAST.CherryPick; const c1 = new Commit({ message: "foo" }); const cases = { "trivial": { i: new RepoAST(), m: {}, e: new RepoAST() }, @@ -1083,32 +1040,6 @@ describe("RepoAstUtil", function () { rebase: new Rebase("foo", "1", "1"), }), }, - "cherry-pick": { - i: new RepoAST({ - commits: { "1": c1 }, - head: "1", - cherryPick: new CherryPick("1", "1"), - }), - m: { "1": "2"}, - e: new RepoAST({ - commits: { "2": c1 }, - head: "2", - cherryPick: new CherryPick("2", "2"), - }), - }, - "cherry-pick unmapped": { - i: new RepoAST({ - commits: { "1": c1 }, - head: "1", - cherryPick: new CherryPick("1", "1"), - }), - m: {}, - e: new RepoAST({ - commits: { "1": c1 }, - head: "1", - cherryPick: new CherryPick("1", "1"), - }), - }, "sequencer": { i: new RepoAST({ commits: { "1": c1 }, diff --git a/node/test/util/repo_status.js b/node/test/util/repo_status.js index fea120f7b..516a8d74a 100644 --- a/node/test/util/repo_status.js +++ b/node/test/util/repo_status.js @@ -32,7 +32,6 @@ const assert = require("chai").assert; -const CherryPick = require("../../lib/util/cherry_pick"); const Rebase = require("../../lib/util/rebase"); const RepoStatus = require("../../lib/util/repo_status"); const SequencerState = require("../../lib/util/sequencer_state"); @@ -480,7 +479,6 @@ const MERGE = SequencerState.TYPE.MERGE; workdir: {}, submodules: {}, rebase: null, - cherryPick: null, sequencerState: null, }; return Object.assign(result, args); @@ -507,7 +505,6 @@ const MERGE = SequencerState.TYPE.MERGE; }), }, rebase: new Rebase("foo", "1", "2"), - cherryPick: new CherryPick("2", "1"), sequencerState: new SequencerState({ type: MERGE, originalHead: new CommitAndRef("foo", null), @@ -528,7 +525,6 @@ const MERGE = SequencerState.TYPE.MERGE; }), }, rebase: new Rebase("foo", "1", "2"), - cherryPick: new CherryPick("2", "1"), sequencerState: new SequencerState({ type: MERGE, originalHead: new CommitAndRef("foo", null), @@ -551,7 +547,6 @@ const MERGE = SequencerState.TYPE.MERGE; assert.deepEqual(result.workdir, c.e.workdir); assert.deepEqual(result.submodules, c.e.submodules); assert.deepEqual(result.rebase, c.e.rebase); - assert.deepEqual(result.cherryPick, c.e.cherryPick); assert.deepEqual(result.sequencerState, c.e.sequencerState); }); }); @@ -1055,7 +1050,6 @@ const MERGE = SequencerState.TYPE.MERGE; }, workdir: { x: FILESTATUS.MODIFIED }, rebase: new Rebase("2", "4", "b"), - cherryPick: new CherryPick("1", "1"), sequencerState: new SequencerState({ type: MERGE, originalHead: new CommitAndRef("foo", null), @@ -1073,7 +1067,6 @@ const MERGE = SequencerState.TYPE.MERGE; }, workdir: { x: FILESTATUS.ADDED }, rebase: new Rebase("a", "4", "b"), - cherryPick: new CherryPick("a", "2"), sequencerState: new SequencerState({ type: MERGE, originalHead: new CommitAndRef("foo", null), @@ -1098,7 +1091,6 @@ const MERGE = SequencerState.TYPE.MERGE; submodules: anotherStat.submodules, workdir: anotherStat.workdir, rebase: anotherStat.rebase, - cherryPick: anotherStat.cherryPick, sequencerState: anotherStat.sequencerState, }); assert.deepEqual(newStat, anotherStat); diff --git a/node/test/util/shorthand_parser_util.js b/node/test/util/shorthand_parser_util.js index 89a7cae6c..a8ede324f 100644 --- a/node/test/util/shorthand_parser_util.js +++ b/node/test/util/shorthand_parser_util.js @@ -642,20 +642,6 @@ describe("ShorthandParserUtil", function () { rebase: null, }), }, - "cherry-pick": { - i: "S:P1,2", - e: m({ - type: "S", - cherryPick: new RepoAST.CherryPick("1", "2"), - }), - }, - "cherry-pick null": { - i: "S:P", - e: m({ - type: "S", - cherryPick: null, - }), - }, "sequencer null": { i: "S:Q", e: m({ @@ -752,7 +738,6 @@ describe("ShorthandParserUtil", function () { assert.equal(r.currentBranchName, e.currentBranchName); assert.deepEqual(r.openSubmodules, e.openSubmodules); assert.deepEqual(r.rebase, e.rebase); - assert.deepEqual(r.cherryPick, e.cherryPick); assert.deepEqual(r.sequencerState, e.sequencerState); }); }); @@ -925,12 +910,6 @@ describe("ShorthandParserUtil", function () { rebase: new RepoAST.Rebase("foo", "1", "1"), }), }, - "cherry-pick": { - i: "S:P1,1", - e: S.copy({ - cherryPick: new RepoAST.CherryPick("1", "1"), - }), - }, "sequencer": { i: "S:QM 1:foo 1: 0 1", e: S.copy({ @@ -1412,59 +1391,6 @@ x=S:Efoo,8,9`, }), } }, - "missing commits in cherry-pick": { - i: ` -a=B:C8-1;C9-1;Bmaster=8;Bfoo=9| -x=S:P8,9`, - e: { - a: B.copy({ - commits: { - "1": new Commit({ - changes: { - "README.md": "hello world" - }, - message: "the first commit", - }), - "8": new Commit({ - parents: ["1"], - changes: { "8": "8" }, - message: "message\n", - }), - "9": new Commit({ - parents: ["1"], - changes: { "9": "9" }, - message: "message\n", - }), - }, - branches: { - master: new RepoAST.Branch("8", null), - foo: new RepoAST.Branch("9", null), - }, - head: "8", - }), - x: S.copy({ - commits: { - "1": new Commit({ - changes: { - "README.md": "hello world" - }, - message: "the first commit", - }), - "8": new Commit({ - parents: ["1"], - changes: { "8": "8" }, - message: "message\n", - }), - "9": new Commit({ - parents: ["1"], - changes: { "9": "9" }, - message: "message\n", - }), - }, - cherryPick: new RepoAST.CherryPick("8", "9"), - }), - } - }, "new open sub": { i: "a=B|x=S:I s=Sa:;Os", e: { diff --git a/node/test/util/status_util.js b/node/test/util/status_util.js index c51e43705..8125fd61c 100644 --- a/node/test/util/status_util.js +++ b/node/test/util/status_util.js @@ -35,7 +35,6 @@ const co = require("co"); const path = require("path"); const NodeGit = require("nodegit"); -const CherryPick = require("../../lib/util/cherry_pick"); const Rebase = require("../../lib/util/rebase"); const RepoAST = require("../../lib/util/repo_ast"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); @@ -113,7 +112,6 @@ describe("StatusUtil", function () { staged: { x: RepoStatus.FILESTATUS.ADDED }, workdir: { y: RepoStatus.FILESTATUS.ADDED }, rebase: new Rebase("foo", "1", "1"), - cherryPick: new CherryPick("1", "1"), sequencerState: new SequencerState({ type: TYPE.MERGE, originalHead: new CommitAndRef("1", null), @@ -130,7 +128,6 @@ describe("StatusUtil", function () { staged: { x: RepoStatus.FILESTATUS.ADDED }, workdir: { y: RepoStatus.FILESTATUS.ADDED }, rebase: new Rebase("foo", "3", "3"), - cherryPick: new CherryPick("3", "3"), sequencerState: new SequencerState({ type: TYPE.MERGE, originalHead: new CommitAndRef("3", null), @@ -582,14 +579,6 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, rebase: new Rebase("master", "2", "3"), }), }, - "cherry-pick": { - state: "x=S:C2-1;C3-1;Bfoo=3;Bmaster=2;P2,3", - expected: new RepoStatus({ - headCommit: "2", - currentBranchName: "master", - cherryPick: new CherryPick("2", "3"), - }), - }, "sequencer": { state: "x=S:C2-1;C3-1;Bfoo=3;Bmaster=2;QM 1: 2:foo 1 2,3", expected: new RepoStatus({ @@ -908,12 +897,6 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, }), fails: true, }, - "cherry-pick": { - input: new RepoStatus({ - cherryPick: new CherryPick("foo", "bart"), - }), - fails: true, - }, "sequencer": { input: new RepoStatus({ sequencerState: new SequencerState({ diff --git a/node/test/util/write_repo_ast_util.js b/node/test/util/write_repo_ast_util.js index 7e4e174ce..fb87fea91 100644 --- a/node/test/util/write_repo_ast_util.js +++ b/node/test/util/write_repo_ast_util.js @@ -342,7 +342,6 @@ S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q", expected: "\ S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q;H=3", }, - "with in-progress cherry-pick": "S:P1,1", "with in-progress sequencer": "S:QR 1:foo 1:bar 0 1", "sequencer with message": "S:Qfoo#R 1:foo 1:bar 0 1", "headless": { From 01ea515d4cadd266a6ea517fabfc751029acb069 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Mon, 26 Mar 2018 17:50:25 -0400 Subject: [PATCH 073/402] Added `listRebaseCommits` method to enumerate, in the correct order, the set of commits to apply in a rebase operation. Addresses: https://github.com/twosigma/git-meta/projects/2#card-8116798 --- node/lib/util/rebase_util.js | 92 +++++++++++++++++++++++++++++++++++ node/test/util/rebase_util.js | 86 ++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index 191a8e90b..68acdf8d6 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -788,3 +788,95 @@ ${colors.green(rebaseInfo.onto)}.`); console.log("Finished rebase."); return result; }); + +/** + * From the specified `repo`, return a list of non-merge commits that are part + * of the history of `from` but not of `onto` (inclusive of `from`), in + * depth-first order from left-to right. Note that this will include commits + * that could be fast-forwarded; if you need to do something else when `onto` + * can be fast-forwarded from `from`, you must check beforehand. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} from + * @param {NodeGit.Commit} onto + * @return [String] + */ +exports.listRebaseCommits = co.wrap(function *(repo, from, onto) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(from, NodeGit.Commit); + assert.instanceOf(onto, NodeGit.Commit); + + const ontoSha = onto.id().tostrS(); + const seen = new Set([ontoSha]); // shas that stop traversal + const result = []; + const todo = []; // { sha: String | null, parents: [Commit]} + + // We proceed as follows: + // + // 1. Each item in the `todo` list represents a child commit with + // zero or more parents left to process. + // 2. If the list of parents is empty in the last element of `todo`, + // record the sha of the child commit of this element into `result` + // (unless it was null, which would indicate a skipped merge commit). + // 3. Otherwise, pop the last parent off and "enqueue" it onto the todo + // list. + // 4. The `enqueue` function will skip any commits that have been + // previously seen, or that are in the history of `onto`. + // 5. We start things off by enqueuing `from`. + // + // Note that (2) ensures that all parents of a commit are added to `result` + // (where appropriate) before the commit itself, and (3) that a commit and + // all of its ancestors are processed before any of its siblings. + + const enqueue = co.wrap(function *(commit) { + const sha = commit.id().tostrS(); + + // If we've seen a commit already, do not process it or any of its + // children. Otherwise, record that we've seen it. + + if (seen.has(sha)) { + return; // RETURN + } + seen.add(sha); + + // Skip this commit if it's an ancestor of `onto`. + + const inHistory = yield NodeGit.Graph.descendantOf(repo, ontoSha, sha); + if (inHistory) { + return; // RETURN + } + const parents = yield commit.getParents(); + + // Record a null as the `sha` if this was a merge commit so that we + // know not to add it to `result` after processing its parents. We + // work from the back, so reverse the parents to get left first. + + todo.push({ + sha: 1 >= parents.length ? sha : null, + parents: parents.reverse(), + }); + }); + + yield enqueue(from); // Kick it off with the first, `from`, commit. + + while (0 !== todo.length) { + const back = todo[todo.length - 1]; + const parents = back.parents; + if (0 === parents.length) { + // If nothing to do for last item, pop it off, record the child sha + // in the result list if non-null (indicating non-merge), and move + // on. + + if (null !== back.sha) { + result.push(back.sha); + } + todo.pop(); + } else { + // Otherwise, pop off the last parent and attempt to enqueue it. + + const next = parents.pop(); + yield enqueue(next); + } + } + return result; +}); diff --git a/node/test/util/rebase_util.js b/node/test/util/rebase_util.js index c99dbf089..01d8935e8 100644 --- a/node/test/util/rebase_util.js +++ b/node/test/util/rebase_util.js @@ -747,4 +747,90 @@ x=E:C3M-4 s=Sa:a1s,t=Sb:b1t;E;Bmaster=3M; })); }); }); + + describe("listRebaseCommits", function () { + const cases = { + "same commit": { + input: "S", + from: "1", + onto: "1", + expected: [], + }, + "ancestor": { + input: "S:C2-1;Bfoo=2", + from: "1", + onto: "2", + expected: [], + }, + "descendant": { + input: "S:C2-1;Bfoo=2", + from: "2", + onto: "1", + expected: ["2"], + }, + "descendants": { + input: "S:C3-2;C2-1;Bfoo=3", + from: "3", + onto: "1", + expected: ["2", "3"], + }, + "merge of base": { + input: "S:C2-1;Bmaster=2;C4-3,1;C3-1;Bfoo=4", + from: "4", + onto: "2", + expected: ["3"], + }, + "non FFWD": { + input: "S:Cf-1;Co-1;Bf=f;Bo=o", + from: "f", + onto: "o", + expected: ["f"], + }, + "left-to-right": { + input: "S:Co-1;Cf-b,a;Ca-1;Cb-1;Bf=f;Bo=o", + from: "f", + onto: "o", + expected: ["b", "a"], + }, + "left-to-right and deep first": { + input: "S:Co-1;Cf-b,a;Ca-1;Cb-c;Cc-1;Bf=f;Bo=o", + from: "f", + onto: "o", + expected: ["c", "b", "a"], + }, + "double deep": { + input: "S:Co-1;Cf-b,a;Ca-1;Cb-c,d;Cc-1;Cd-1;Bf=f;Bo=o", + from: "f", + onto: "o", + expected: ["c", "d", "a"], + }, + "and deep on the right": { + input: "S:Co-1;Cf-b,a;Ca-q,r;Cq-1;Cr-1;Cb-1;Bf=f;Bo=o", + from: "f", + onto: "o", + expected: ["b", "q", "r"], + }, + "new commit in history more than once": { + input: "S:Co-1;Cf-r,a;Ca-q,r;Cq-1;Cr-1;Bf=f;Bo=o", + from: "f", + onto: "o", + expected: ["r", "q"], + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(c.input); + const repo = written.repo; + const old = written.oldCommitMap; + const from = yield repo.getCommit(old[c.from]); + const onto = yield repo.getCommit(old[c.onto]); + const result = yield RebaseUtil.listRebaseCommits(repo, + from, + onto); + const commits = result.map(sha => written.commitMap[sha]); + assert.deepEqual(commits, c.expected); + })); + }); + }); }); From 5f90859e5bb734a2fc3239c3818b410e7c87715b Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Mon, 26 Mar 2018 10:52:36 -0400 Subject: [PATCH 074/402] Mv submodule rebase code into SubmoduleRebaseUtil With this change, `CherryPickUtil` is no longer dependent on `RebaseUtil`, which will need to use `CherryPickUtil` soon. Addresses: https://github.com/twosigma/git-meta/projects/2#card-8117527 --- node/lib/util/cherry_pick_util.js | 17 +- node/lib/util/rebase_util.js | 259 +----------------- node/lib/util/submodule_rebase_util.js | 282 ++++++++++++++++++++ node/test/util/rebase_util.js | 303 --------------------- node/test/util/submodule_rebase_util.js | 340 ++++++++++++++++++++++++ 5 files changed, 644 insertions(+), 557 deletions(-) create mode 100644 node/lib/util/submodule_rebase_util.js create mode 100644 node/test/util/submodule_rebase_util.js diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index ab62349f1..aa6cc943a 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -43,13 +43,13 @@ const DeinitUtil = require("./deinit_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); const Open = require("./open"); -const RebaseUtil = require("./rebase_util"); const Reset = require("./reset"); const SequencerState = require("./sequencer_state"); const SequencerStateUtil = require("./sequencer_state_util"); const StatusUtil = require("./status_util"); const Submodule = require("./submodule"); const SubmoduleConfigUtil = require("./submodule_config_util"); +const SubmoduleRebaseUtil = require("./submodule_rebase_util"); const SubmoduleUtil = require("./submodule_util"); const TreeUtil = require("./tree_util"); const UserError = require("./user_error"); @@ -351,9 +351,10 @@ ${colors.green(commitText)}.`); yield fetcher.fetchSha(repo, name, change.oldSha); const newCommit = yield repo.getCommit(change.newSha); const oldCommit = yield repo.getCommit(change.oldSha); - const rewriteResult = yield RebaseUtil.rewriteCommits(repo, - newCommit, - oldCommit); + const rewriteResult = yield SubmoduleRebaseUtil.rewriteCommits( + repo, + newCommit, + oldCommit); result.commits[name] = rewriteResult.commits; yield metaIndex.addByPath(name); if (null !== rewriteResult.conflictedCommit) { @@ -548,10 +549,10 @@ exports.continue = co.wrap(function *(repo) { } const index = yield repo.index(); const commit = yield repo.getCommit(seq.target.sha); - const subResult = yield RebaseUtil.continueSubmodules(repo, - index, - status, - commit); + const subResult = yield SubmoduleRebaseUtil.continueSubmodules(repo, + index, + status, + commit); yield index.write(); const result = { newMetaCommit: null, diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index 191a8e90b..2def74038 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -34,8 +34,6 @@ const assert = require("chai").assert; const co = require("co"); const colors = require("colors"); const NodeGit = require("nodegit"); -const path = require("path"); -const rimraf = require("rimraf"); const DeinitUtil = require("./deinit_util"); const DoWorkQueue = require("../util/do_work_queue"); @@ -46,19 +44,10 @@ const RepoStatus = require("./repo_status"); const RebaseFileUtil = require("./rebase_file_util"); const StatusUtil = require("./status_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); +const SubmoduleRebaseUtil = require("./submodule_rebase_util"); const SubmoduleUtil = require("./submodule_util"); const UserError = require("./user_error"); -/** - * Return a conflict description for the submodule having the specified `name`. - * - * @param {String} name - * @return {String} - */ -function subConflictErrorMessage(name) { - return `Conflict in ${colors.red(name)}`; -} - /** * Put the head of the specified `repo` on the specified `commitSha`. */ @@ -67,139 +56,6 @@ const setHead = co.wrap(function *(repo, commitSha) { yield GitUtil.setHeadHard(repo, commit); }); -/** - * Call `next` on the specified `rebase`; return the rebase operation for the - * rebase or null if there is no further operation. - * - * TODO: independent test - * - * @async - * @private - * @param {NodeGit.Rebase} rebase - * @return {RebaseOperation|null} - */ -exports.callNext = co.wrap(function *(rebase) { - try { - return yield rebase.next(); - } - catch (e) { - // It's cumbersome, but the way the nodegit library indicates - // that you are at the end of the rebase is by throwing an - // exception. At this point we call `finish` on the rebase and - // break out of the contaiing while loop. - - if (e.errno === NodeGit.Error.CODE.ITEROVER) { - return null; - } - throw e; - } -}); - -const cleanupRebaseDir = co.wrap(function *(repo) { - const gitDir = repo.path(); - const rebaseDir = yield RebaseFileUtil.findRebasingDir(gitDir); - if (null !== rebaseDir) { - const rebasePath = path.join(gitDir, rebaseDir); - yield (new Promise(callback => { - return rimraf(rebasePath, {}, callback); - })); - } -}); - -/** - * Finish the specified `rebase` in the specified `repo`. Note that this - * method is necessary only as a workaround for: - * https://github.com/twosigma/git-meta/issues/115. - * - * @param {NodeGit.Repository} repo - * @param {NodeGit.Rebase} rebase - */ -const callFinish = co.wrap(function *(repo, rebase) { - const result = rebase.finish(); - const CLEANUP_FAILURE = -15; - if (CLEANUP_FAILURE === result) { - yield cleanupRebaseDir(repo); - } -}); - -/** - * Process the specified `rebase` for the specified `repo`, beginning with the - * specified `op`. Return an object describing any encountered error and - * commits made. If successful, clean up and finish the rebase. If - * `null === op`, finish the rebase and return. - * - * @param {NodeGit.Repository} repo - * @param {NodeGit.Rebase} rebase - * @param {NodeGit.RebaseOperation|null} op - * @return {Object} - * @return {Object} return.commits - * @return {String|null} return.conflictedCommit - */ -exports.processRebase = co.wrap(function *(repo, rebase, op) { - assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(rebase, NodeGit.Rebase); - if (null !== op) { - assert.instanceOf(op, NodeGit.RebaseOperation); - } - const result = { - commits: {}, - conflictedCommit: null, - }; - const signature = repo.defaultSignature(); - while (null !== op) { - const index = yield repo.index(); - if (index.hasConflicts()) { - result.conflictedCommit = op.id().tostrS(); - return result; // RETURN - } - const newCommit = rebase.commit(null, signature, null); - const originalCommit = op.id().tostrS(); - result.commits[newCommit.tostrS()] = originalCommit; - op = yield exports.callNext(rebase); - } - yield callFinish(repo, rebase); - return result; -}); - -/** - * Rebease the commits from the specified `branch` commit on the HEAD of - * the specified `repo`. If the optionally specified `upstream` is provided, - * rewrite only commits beginning with `upstream`; otherwise, rewrite all - * reachable commits. Return an object containing a map that describes any - * written commits and an error message if some part of the rewrite failed. - * - * @param {NodeGit.Repository} repo - * @param {NodeGit.Commit} commit - * @param {NodeGit.Commit|null} upstream - * @return {Object} - * @return {Object} return.commits new sha to original sha - * @return {String|null} return.conflictedCommit error message if failed - */ -exports.rewriteCommits = co.wrap(function *(repo, branch, upstream) { - assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(branch, NodeGit.Commit); - if (null !== upstream) { - assert.instanceOf(upstream, NodeGit.Commit); - } - - const head = yield repo.head(); - const ontoAnnotated = yield NodeGit.AnnotatedCommit.fromRef(repo, head); - const branchAnnotated = - yield NodeGit.AnnotatedCommit.lookup(repo, branch.id()); - let upstreamAnnotated = null; - if (null !== upstream) { - upstreamAnnotated = - yield NodeGit.AnnotatedCommit.lookup(repo, upstream.id()); - } - const rebase = yield NodeGit.Rebase.init(repo, - branchAnnotated, - upstreamAnnotated, - ontoAnnotated, - null); - const op = yield exports.callNext(rebase); - return yield exports.processRebase(repo, rebase, op); -}); - /** * Return an object indicating the commits that were created during rebasing * and/or an error message indicating that the rebase was stopped due to a @@ -232,8 +88,8 @@ const rebaseSubmodule = co.wrap(function *(opener, name, from, onto) { null); console.log(`Submodule ${colors.blue(name)}: starting \ rebase; rewinding to ${colors.green(ontoCommitId.tostrS())}.`); - const op = yield exports.callNext(rebase); - return yield exports.processRebase(repo, rebase, op); + const op = yield SubmoduleRebaseUtil.callNext(rebase); + return yield SubmoduleRebaseUtil.processRebase(repo, rebase, op); }); /** @@ -415,7 +271,8 @@ const processMetaRebaseOp = co.wrap(function *(metaRepo, } if (null !== ret.conflictedCommit) { - errorMessage += subConflictErrorMessage(name) + "\n"; + errorMessage += + SubmoduleRebaseUtil.subConflictErrorMessage(name) + "\n"; conflicted.add(name); } } @@ -480,7 +337,7 @@ const driveRebase = co.wrap(function *(metaRepo, let idx = rebase.operationCurrent(); const total = rebase.operationEntrycount(); function makeCallNext() { - return exports.callNext(rebase); + return SubmoduleRebaseUtil.callNext(rebase); } while (idx < total) { const rebaseOper = rebase.operationByIndex(idx); @@ -519,7 +376,7 @@ const driveRebase = co.wrap(function *(metaRepo, yield DoWorkQueue.doInParallel(openSubs, fetchOpened); } - yield callFinish(metaRepo, rebase); + yield SubmoduleRebaseUtil.callFinish(metaRepo, rebase); return result; }); @@ -592,7 +449,7 @@ up-to-date.`); null, null); console.log(`Rewinding to ${colors.green(commitId.tostrS())}.`); - yield exports.callNext(rebase); + yield SubmoduleRebaseUtil.callNext(rebase); return { rebase: rebase, submoduleCommits: {}, @@ -647,97 +504,6 @@ ${colors.green(rebaseInfo.originalHead)}.`); })); }); -/** - * Continue the rebase in the specified `repo` and return an object describing - * any generated commits and the sha of the conflicted commit if there was one. - * The behavior is undefined unless `true === repo.isRebasing()`. - * - * @param {NodeGit.Repository} repo - * @return {Object} return - * @return {Object} return.commits - * @return {String|null} return.conflictedCommit - */ -const continueRebase = co.wrap(function *(repo) { - assert.instanceOf(repo, NodeGit.Repository); - assert(repo.isRebasing()); - - const rebase = yield NodeGit.Rebase.open(repo); - const idx = rebase.operationCurrent(); - const op = rebase.operationByIndex(idx); - return yield exports.processRebase(repo, rebase, op); -}); - -/** - * Continue rebases in the submodules in the specifed `repo` having the - * `index and `status`. If staged changes are found in submodules that don't - * have in-progress rebases, commit them using the specified message and - * signature from the specified original `commit`. Return an object describing - * any commits that were generated along with an error message if any continues - * failed. - * - * @param {NodeGit.Repository} repo - * @param {NodeGit.Index} index - * @param {RepoStatus} status - * @param {NodeGit.Commit} commit - * @return {Object} - * @return {Object} return.commits map from name to sha map - * @return {Object} return.newCommits from name to newly-created commits - * @return {String|null} return.errorMessage - */ -exports.continueSubmodules = co.wrap(function *(repo, index, status, commit) { - assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(index, NodeGit.Index); - assert.instanceOf(status, RepoStatus); - assert.instanceOf(commit, NodeGit.Commit); - - const commits = {}; - const newCommits = {}; - const subs = status.submodules; - let errorMessage = ""; - const continueSub = co.wrap(function *(name) { - const sub = subs[name]; - const workdir = sub.workdir; - if (null === workdir) { - // Return early if the submodule is closed. - return; // RETURN - } - const subStatus = workdir.status; - const rebaseInfo = subStatus.rebase; - const subRepo = yield SubmoduleUtil.getRepo(repo, name); - if (null === rebaseInfo) { - if (0 !== Object.keys(subStatus.staged).length) { - const id = yield subRepo.createCommitOnHead([], - commit.author(), - commit.committer(), - commit.message()); - newCommits[name] = id.tostrS(); - } - yield index.addByPath(name); - - // Return early if no rebase in this submodule. - return; // RETURN - } - console.log(`Submodule ${colors.blue(name)} continuing \ -rewrite from ${colors.green(rebaseInfo.originalHead)} onto \ -${colors.green(rebaseInfo.onto)}.`); - const result = yield continueRebase(subRepo); - commits[name] = result.commits; - if (null !== result.conflictedCommit) { - errorMessage += subConflictErrorMessage(name) + "\n"; - } - else { - yield index.addByPath(name); - yield index.conflictRemove(name); - } - }); - yield DoWorkQueue.doInParallel(Object.keys(subs), continueSub); - return { - errorMessage: "" === errorMessage ? null : errorMessage, - commits: commits, - newCommits: newCommits, - }; -}); - /** * Continue the rebase in progress on the specified `repo`. * @@ -771,10 +537,11 @@ ${colors.green(rebaseInfo.onto)}.`); const idx = rebase.operationCurrent(); const op = rebase.operationByIndex(idx); const baseCommit = yield repo.getCommit(op.id()); - const result = yield exports.continueSubmodules(repo, - index, - status, - baseCommit); + const result = yield SubmoduleRebaseUtil.continueSubmodules( + repo, + index, + status, + baseCommit); if (null !== result.errorMessage) { throw new UserError(result.errorMessage); } diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js new file mode 100644 index 000000000..4c5a990e9 --- /dev/null +++ b/node/lib/util/submodule_rebase_util.js @@ -0,0 +1,282 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const colors = require("colors"); +const NodeGit = require("nodegit"); +const path = require("path"); +const rimraf = require("rimraf"); + +const DoWorkQueue = require("../util/do_work_queue"); +const RebaseFileUtil = require("./rebase_file_util"); +const RepoStatus = require("./repo_status"); +const SubmoduleUtil = require("./submodule_util"); + +/** + * Continue the rebase in the specified `repo` and return an object describing + * any generated commits and the sha of the conflicted commit if there was one. + * The behavior is undefined unless `true === repo.isRebasing()`. + * + * @param {NodeGit.Repository} repo + * @return {Object} return + * @return {Object} return.commits + * @return {String|null} return.conflictedCommit + */ +const continueRebase = co.wrap(function *(repo) { + assert.instanceOf(repo, NodeGit.Repository); + assert(repo.isRebasing()); + + const rebase = yield NodeGit.Rebase.open(repo); + const idx = rebase.operationCurrent(); + const op = rebase.operationByIndex(idx); + return yield exports.processRebase(repo, rebase, op); +}); + +const cleanupRebaseDir = co.wrap(function *(repo) { + assert.instanceOf(repo, NodeGit.Repository); + + const gitDir = repo.path(); + const rebaseDir = yield RebaseFileUtil.findRebasingDir(gitDir); + if (null !== rebaseDir) { + const rebasePath = path.join(gitDir, rebaseDir); + yield (new Promise(callback => { + return rimraf(rebasePath, {}, callback); + })); + } +}); + +/** + * Finish the specified `rebase` in the specified `repo`. Note that this + * method is necessary only as a workaround for: + * https://github.com/twosigma/git-meta/issues/115. + * + * TODO: independent test + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Rebase} rebase + */ +exports.callFinish = co.wrap(function *(repo, rebase) { + const result = rebase.finish(); + const CLEANUP_FAILURE = -15; + if (CLEANUP_FAILURE === result) { + yield cleanupRebaseDir(repo); + } +}); + +/** + * Call `next` on the specified `rebase`; return the rebase operation for the + * rebase or null if there is no further operation. + * + * @async + * @private + * @param {NodeGit.Rebase} rebase + * @return {RebaseOperation|null} + */ +exports.callNext = co.wrap(function *(rebase) { + try { + return yield rebase.next(); + } + catch (e) { + // It's cumbersome, but the way the nodegit library indicates + // that you are at the end of the rebase is by throwing an + // exception. At this point we call `finish` on the rebase and + // break out of the contaiing while loop. + + if (e.errno === NodeGit.Error.CODE.ITEROVER) { + return null; + } + throw e; + } +}); + +/** + * Process the specified `rebase` for the specified `repo`, beginning with the + * specified `op`. Return an object describing any encountered error and + * commits made. If successful, clean up and finish the rebase. If + * `null === op`, finish the rebase and return. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Rebase} rebase + * @param {NodeGit.RebaseOperation|null} op + * @return {Object} + * @return {Object} return.commits + * @return {String|null} return.conflictedCommit + */ +exports.processRebase = co.wrap(function *(repo, rebase, op) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(rebase, NodeGit.Rebase); + if (null !== op) { + assert.instanceOf(op, NodeGit.RebaseOperation); + } + const result = { + commits: {}, + conflictedCommit: null, + }; + const signature = repo.defaultSignature(); + while (null !== op) { + const index = yield repo.index(); + if (index.hasConflicts()) { + result.conflictedCommit = op.id().tostrS(); + return result; // RETURN + } + const newCommit = rebase.commit(null, signature, null); + const originalCommit = op.id().tostrS(); + result.commits[newCommit.tostrS()] = originalCommit; + op = yield exports.callNext(rebase); + } + yield exports.callFinish(repo, rebase); + return result; +}); + +/** + * Rebase the commits from the specified `branch` commit on the HEAD of the + * specified `repo`. If the optionally specified `upstream` is provided, + * rewrite only commits beginning with `upstream`; otherwise, rewrite all + * reachable commits. Return an object containing a map that describes any + * written commits and an error message if some part of the rewrite failed. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + * @param {NodeGit.Commit|null} upstream + * @return {Object} + * @return {Object} return.commits new sha to original sha + * @return {String|null} return.conflictedCommit error message if failed + */ +exports.rewriteCommits = co.wrap(function *(repo, branch, upstream) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(branch, NodeGit.Commit); + if (null !== upstream) { + assert.instanceOf(upstream, NodeGit.Commit); + } + + const head = yield repo.head(); + const ontoAnnotated = yield NodeGit.AnnotatedCommit.fromRef(repo, head); + const branchAnnotated = + yield NodeGit.AnnotatedCommit.lookup(repo, branch.id()); + let upstreamAnnotated = null; + if (null !== upstream) { + upstreamAnnotated = + yield NodeGit.AnnotatedCommit.lookup(repo, upstream.id()); + } + const rebase = yield NodeGit.Rebase.init(repo, + branchAnnotated, + upstreamAnnotated, + ontoAnnotated, + null); + const op = yield exports.callNext(rebase); + return yield exports.processRebase(repo, rebase, op); +}); + +/** + * Return a conflict description for the submodule having the specified `name`. + * + * TODO: independent test + * + * @param {String} name + * @return {String} + */ +exports.subConflictErrorMessage = function (name) { + return `Conflict in ${colors.red(name)}`; +}; + + +/** + * Continue rebases in the submodules in the specifed `repo` having the + * `index and `status`. If staged changes are found in submodules that don't + * have in-progress rebases, commit them using the specified message and + * signature from the specified original `commit`. Return an object describing + * any commits that were generated along with an error message if any continues + * failed. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Index} index + * @param {RepoStatus} status + * @param {NodeGit.Commit} commit + * @return {Object} + * @return {Object} return.commits map from name to sha map + * @return {Object} return.newCommits from name to newly-created commits + * @return {String|null} return.errorMessage + */ +exports.continueSubmodules = co.wrap(function *(repo, index, status, commit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(index, NodeGit.Index); + assert.instanceOf(status, RepoStatus); + assert.instanceOf(commit, NodeGit.Commit); + + const commits = {}; + const newCommits = {}; + const subs = status.submodules; + let errorMessage = ""; + const continueSub = co.wrap(function *(name) { + const sub = subs[name]; + const workdir = sub.workdir; + if (null === workdir) { + // Return early if the submodule is closed. + return; // RETURN + } + const subStatus = workdir.status; + const rebaseInfo = subStatus.rebase; + const subRepo = yield SubmoduleUtil.getRepo(repo, name); + if (null === rebaseInfo) { + if (0 !== Object.keys(subStatus.staged).length) { + const id = yield subRepo.createCommitOnHead([], + commit.author(), + commit.committer(), + commit.message()); + newCommits[name] = id.tostrS(); + } + yield index.addByPath(name); + + // Return early if no rebase in this submodule. + return; // RETURN + } + console.log(`Submodule ${colors.blue(name)} continuing \ +rewrite from ${colors.green(rebaseInfo.originalHead)} onto \ +${colors.green(rebaseInfo.onto)}.`); + const result = yield continueRebase(subRepo); + commits[name] = result.commits; + if (null !== result.conflictedCommit) { + errorMessage += exports.subConflictErrorMessage(name) + "\n"; + } + else { + yield index.addByPath(name); + yield index.conflictRemove(name); + } + }); + yield DoWorkQueue.doInParallel(Object.keys(subs), continueSub); + return { + errorMessage: "" === errorMessage ? null : errorMessage, + commits: commits, + newCommits: newCommits, + }; +}); diff --git a/node/test/util/rebase_util.js b/node/test/util/rebase_util.js index c99dbf089..06fd5cc3f 100644 --- a/node/test/util/rebase_util.js +++ b/node/test/util/rebase_util.js @@ -32,12 +32,9 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); -const NodeGit = require("nodegit"); const RebaseUtil = require("../../lib/util/rebase_util"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); -const StatusUtil = require("../../lib/util/status_util"); const SubmoduleUtil = require("../../lib/util/submodule_util"); function makeRebaser(operation) { @@ -62,202 +59,6 @@ function makeRebaser(operation) { } describe("rebase", function () { - it("callNext", co.wrap(function *() { - const init = "S:C2-1;Bmaster=2;C3-1;Bfoo=3"; - const written = yield RepoASTTestUtil.createRepo(init); - const repo = written.repo; - const ontoSha = written.oldCommitMap["3"]; - const fromId = NodeGit.Oid.fromString(ontoSha); - const fromAnnotated = - yield NodeGit.AnnotatedCommit.lookup(repo, fromId); - const head = yield repo.head(); - const ontoAnnotated = yield NodeGit.AnnotatedCommit.fromRef(repo, - head); - const rebase = yield NodeGit.Rebase.init(repo, - fromAnnotated, - ontoAnnotated, - null, - null); - const first = yield RebaseUtil.callNext(rebase); - assert.equal(first.id().tostrS(), ontoSha); - const second = yield RebaseUtil.callNext(rebase); - assert.isNull(second); - })); - - describe("processRebase", function () { - const cases = { - "no conflicts": { - initial: "x=S:C2-1;Cr-1;Bmaster=2;Br=r", - expected: "x=E:Crr-2 r=r;H=rr", - conflictedCommit: null, - }, - "conflict": { - initial: "x=S:C2-1;Cr-1 2=3;Bmaster=2;Br=r", - expected: "x=E:I *2=~*2*3;W 2=u;H=2;Edetached HEAD,r,2", - conflictedCommit: "r", - expectedTransformer: function (expected, mapping) { - const content = `\ -<<<<<<< ${mapping.reverseCommitMap["2"]} -2 -======= -3 ->>>>>>> message -`; - expected.x = expected.x.copy({ - workdir: { - "2": content, - }, - }); - return expected; - }, - }, - "fast forward": { - initial: "x=S:C2-r;Cr-1;Bmaster=2;Br=r", - expected: "x=E:H=2", - conflictedCommit: null, - }, - }; - Object.keys(cases).forEach(caseName => { - it(caseName, co.wrap(function *() { - const c = cases[caseName]; - const op = co.wrap(function *(repos, maps) { - const repo = repos.x; - const headCommit = yield repo.getHeadCommit(); - const AnnotatedCommit = NodeGit.AnnotatedCommit; - const headAnnotated = yield AnnotatedCommit.lookup( - repo, - headCommit.id()); - const targetCommitSha = maps.reverseCommitMap.r; - const targetCommit = yield repo.getCommit(targetCommitSha); - const targetAnnotated = yield AnnotatedCommit.lookup( - repo, - targetCommit.id()); - const rebase = yield NodeGit.Rebase.init(repo, - targetAnnotated, - headAnnotated, - null, - null); - const op = yield RebaseUtil.callNext(rebase); - const result = yield RebaseUtil.processRebase(repo, - rebase, - op); - if (null === c.conflictedCommit) { - assert.isNull(result.conflictedCommit); - } else { - assert.equal( - result.conflictedCommit, - maps.reverseCommitMap[c.conflictedCommit]); - } - const commitMap = {}; - Object.keys(result.commits).forEach(newSha => { - const oldSha = result.commits[newSha]; - const oldLogicalCommit = maps.commitMap[oldSha]; - commitMap[newSha] = oldLogicalCommit + "r"; - }); - return { - commitMap: commitMap, - }; - }); - const options = {}; - if (undefined !== c.expectedTransformer) { - options.expectedTransformer = c.expectedTransformer; - } - yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, - c.expected, - op, - c.fails, - options); - })); - }); - }); - - describe("rewriteCommits", function () { - const cases = { - "normal rebase": { - initial: "x=S:C2-1;Cr-1;Bmaster=2;Br=r", - expected: "x=E:Crr-2 r=r;H=rr", - upstream: null, - conflictedCommit: null, - }, - "skip none": { - initial: "x=S:C2-1;Cr-1;Bmaster=2;Br=r", - expected: "x=E:Crr-2 r=r;H=rr", - upstream: "1", - conflictedCommit: null, - }, - "conflict": { - initial: "x=S:C2-1;Cr-1 2=3;Bmaster=2;Br=r", - expected: `x=E:I *2=~*2*3;H=2;Edetached HEAD,r,2;W 2=\ -<<<<<<< master -2 -======= -3 ->>>>>>> message -; -`, - upstream: null, - conflictedCommit: "r", - }, - "multiple commits": { - initial: "x=S:C2-1;C3-1;Cr-3;Bmaster=2;Br=r", - expected: "x=E:Crr-3r r=r;C3r-2 3=3;H=rr", - upstream: null, - conflictedCommit: null, - }, - "skip a commit": { - initial: "x=S:C2-1;C3-1;Cr-3;Bmaster=2;Br=r", - expected: "x=E:Crr-2 r=r;H=rr", - upstream: "3", - conflictedCommit: null, - }, - }; - Object.keys(cases).forEach(caseName => { - it(caseName, co.wrap(function *() { - const c = cases[caseName]; - const op = co.wrap(function *(repos, maps) { - const repo = repos.x; - const targetSha = maps.reverseCommitMap.r; - const targetCommit = yield repo.getCommit(targetSha); - let upstreamCommit = null; - if (null !== c.upstream) { - const upstreamSha = maps.reverseCommitMap[c.upstream]; - upstreamCommit = yield repo.getCommit(upstreamSha); - } - const result = yield RebaseUtil.rewriteCommits( - repo, - targetCommit, - upstreamCommit); - if (null === c.conflictedCommit) { - assert.isNull(result.conflictedCommit); - } else { - assert.equal( - result.conflictedCommit, - maps.reverseCommitMap[c.conflictedCommit]); - } - const commitMap = {}; - Object.keys(result.commits).forEach(newSha => { - const oldSha = result.commits[newSha]; - const oldLogicalCommit = maps.commitMap[oldSha]; - commitMap[newSha] = oldLogicalCommit + "r"; - }); - return { - commitMap: commitMap, - }; - }); - const options = {}; - if (undefined !== c.expectedTransformer) { - options.expectedTransformer = c.expectedTransformer; - } - yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, - c.expected, - op, - c.fails, - options); - })); - }); - }); - - describe("rebase", function () { // Will append the leter 'M' to any created meta-repo commits, and the @@ -535,110 +336,6 @@ x=U:C3-2 s=Sa:q;C4-2 s=Sa:r; }); }); - describe("continueSubmodules", function () { - const cases = { - "nothing to do": { - initial: `x=S`, - }, - "a closed sub": { - initial: "x=S:C2-1 s=Sa:1;Bmaster=2", - }, - "change in sub is staged": { - initial: "a=B:Ca-1;Ba=a|x=U:Os H=a", - expected: "x=E:I s=Sa:a", - }, - "rebase in a sub": { - initial: ` -a=B:Cq-1;Cr-1;Bq=q;Br=r| -x=U:C3-2 s=Sa:q;Bmaster=3;Os EHEAD,q,r!I q=q`, - expected: ` -x=E:I s=Sa:qs;Os Cqs-r q=q!H=qs!E` - }, - "rebase in a sub, was conflicted": { - initial: ` -a=B:Cq-1;Cr-1;Bq=q;Br=r| -x=U:C3-2 s=Sa:q;Bmaster=3;I *s=S:1*S:r*S:q;Os EHEAD,q,r!I q=q`, - expected: ` -x=E:I s=Sa:qs;Os Cqs-r q=q!H=qs!E` - }, - "rebase two in a sub": { - initial: ` -a=B:Cp-q;Cq-1;Cr-1;Bp=p;Br=r| -x=U:C3-2 s=Sa:q;Bmaster=3;Os EHEAD,p,r!I q=q!Bp=p`, - expected: ` -x=E:I s=Sa:ps;Os Cps-qs p=p!Cqs-r q=q!H=ps!E!Bp=p` - }, - "rebase in two subs": { - initial: ` -a=B:Cp-q;Cq-1;Cr-1;Cz-1;Bp=p;Br=r;Bz=z| -x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q,t=Sa:q;Bmaster=3; - Os EHEAD,p,r!I q=q!Bp=p; - Ot EHEAD,z,r!I z=8!Bz=z; -`, - expected: ` -x=E:I s=Sa:ps,t=Sa:zt; - Os Cps-qs p=p!Cqs-r q=q!H=ps!E!Bp=p; - Ot Czt-r z=8!H=zt!E!Bz=z; -`, - }, - "rebase in two subs, conflict in one": { - initial: ` -a=B:Cp-q r=8;Cq-1;Cr-1;Cz-1;Bp=p;Br=r;Bz=z| -x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q,t=Sa:q;Bmaster=3; - Os EHEAD,p,r!I q=q!Bp=p; - Ot EHEAD,z,r!I z=8!Bz=z; -`, - expected: ` -x=E:I t=Sa:zt; - Os Cqs-r q=q!H=qs!EHEAD,p,r!Bp=p!I *r=~*r*8!W r=^<<<<; - Ot Czt-r z=8!H=zt!E!Bz=z; -`, - errorMessage: `\ -Conflict in ${colors.red("s")} -`, - }, - "made a commit in a sub without a rebase": { - initial: `a=B|x=U:Cfoo#9-1;B9=9;Os I a=b`, - expected: `x=E:I s=Sa:Ns;Os Cfoo#Ns-1 a=b!H=Ns`, - baseCommit: "9", - message: "foo", - }, - }; - Object.keys(cases).forEach(caseName => { - const c = cases[caseName]; - it(caseName, co.wrap(function *() { - const continuer = co.wrap(function *(repos, maps) { - const repo = repos.x; - const index = yield repo.index(); - const status = yield StatusUtil.getRepoStatus(repo); - const baseSha = c.baseCommit || "1"; - const baseCommit = - yield repo.getCommit(maps.reverseCommitMap[baseSha]); - const result = yield RebaseUtil.continueSubmodules( - repo, - index, - status, - baseCommit); - assert.equal(result.errorMessage, c.errorMessage || null); - const commitMap = {}; - RepoASTTestUtil.mapSubCommits(commitMap, - result.commits, - maps.commitMap); - Object.keys(result.newCommits).forEach(name => { - commitMap[result.newCommits[name]] = "N" + name; - }); - return { - commitMap: commitMap, - }; - }); - yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, - c.expected, - continuer, - c.fails); - })); - }); - }); - describe("continue", function () { const cases = { "meta-only": { diff --git a/node/test/util/submodule_rebase_util.js b/node/test/util/submodule_rebase_util.js new file mode 100644 index 000000000..862ac8c2a --- /dev/null +++ b/node/test/util/submodule_rebase_util.js @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2016, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const colors = require("colors"); +const NodeGit = require("nodegit"); + +const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const StatusUtil = require("../../lib/util/status_util"); +const SubmoduleRebaseUtil = require("../../lib/util/submodule_rebase_util"); + +describe("SubmoduleRebaseUtil", function () { +it("callNext", co.wrap(function *() { + const init = "S:C2-1;Bmaster=2;C3-1;Bfoo=3"; + const written = yield RepoASTTestUtil.createRepo(init); + const repo = written.repo; + const ontoSha = written.oldCommitMap["3"]; + const fromId = NodeGit.Oid.fromString(ontoSha); + const fromAnnotated = + yield NodeGit.AnnotatedCommit.lookup(repo, fromId); + const head = yield repo.head(); + const ontoAnnotated = yield NodeGit.AnnotatedCommit.fromRef(repo, + head); + const rebase = yield NodeGit.Rebase.init(repo, + fromAnnotated, + ontoAnnotated, + null, + null); + const first = yield SubmoduleRebaseUtil.callNext(rebase); + assert.equal(first.id().tostrS(), ontoSha); + const second = yield SubmoduleRebaseUtil.callNext(rebase); + assert.isNull(second); +})); + +describe("processRebase", function () { + const cases = { + "no conflicts": { + initial: "x=S:C2-1;Cr-1;Bmaster=2;Br=r", + expected: "x=E:Crr-2 r=r;H=rr", + conflictedCommit: null, + }, + "conflict": { + initial: "x=S:C2-1;Cr-1 2=3;Bmaster=2;Br=r", + expected: "x=E:I *2=~*2*3;W 2=u;H=2;Edetached HEAD,r,2", + conflictedCommit: "r", + expectedTransformer: function (expected, mapping) { + const content = `\ +<<<<<<< ${mapping.reverseCommitMap["2"]} +2 +======= +3 +>>>>>>> message +`; + expected.x = expected.x.copy({ + workdir: { + "2": content, + }, + }); + return expected; + }, + }, + "fast forward": { + initial: "x=S:C2-r;Cr-1;Bmaster=2;Br=r", + expected: "x=E:H=2", + conflictedCommit: null, + }, + }; + Object.keys(cases).forEach(caseName => { + it(caseName, co.wrap(function *() { + const c = cases[caseName]; + const op = co.wrap(function *(repos, maps) { + const repo = repos.x; + const headCommit = yield repo.getHeadCommit(); + const AnnotatedCommit = NodeGit.AnnotatedCommit; + const headAnnotated = yield AnnotatedCommit.lookup( + repo, + headCommit.id()); + const targetCommitSha = maps.reverseCommitMap.r; + const targetCommit = yield repo.getCommit(targetCommitSha); + const targetAnnotated = yield AnnotatedCommit.lookup( + repo, + targetCommit.id()); + const rebase = yield NodeGit.Rebase.init(repo, + targetAnnotated, + headAnnotated, + null, + null); + const op = yield SubmoduleRebaseUtil.callNext(rebase); + const result = yield SubmoduleRebaseUtil.processRebase(repo, + rebase, + op); + if (null === c.conflictedCommit) { + assert.isNull(result.conflictedCommit); + } else { + assert.equal( + result.conflictedCommit, + maps.reverseCommitMap[c.conflictedCommit]); + } + const commitMap = {}; + Object.keys(result.commits).forEach(newSha => { + const oldSha = result.commits[newSha]; + const oldLogicalCommit = maps.commitMap[oldSha]; + commitMap[newSha] = oldLogicalCommit + "r"; + }); + return { + commitMap: commitMap, + }; + }); + const options = {}; + if (undefined !== c.expectedTransformer) { + options.expectedTransformer = c.expectedTransformer; + } + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + c.expected, + op, + c.fails, + options); + })); + }); +}); + +describe("rewriteCommits", function () { + const cases = { + "normal rebase": { + initial: "x=S:C2-1;Cr-1;Bmaster=2;Br=r", + expected: "x=E:Crr-2 r=r;H=rr", + upstream: null, + conflictedCommit: null, + }, + "skip none": { + initial: "x=S:C2-1;Cr-1;Bmaster=2;Br=r", + expected: "x=E:Crr-2 r=r;H=rr", + upstream: "1", + conflictedCommit: null, + }, + "conflict": { + initial: "x=S:C2-1;Cr-1 2=3;Bmaster=2;Br=r", + expected: `x=E:I *2=~*2*3;H=2;Edetached HEAD,r,2;W 2=\ +<<<<<<< master +2 +======= +3 +>>>>>>> message +; +`, + upstream: null, + conflictedCommit: "r", + }, + "multiple commits": { + initial: "x=S:C2-1;C3-1;Cr-3;Bmaster=2;Br=r", + expected: "x=E:Crr-3r r=r;C3r-2 3=3;H=rr", + upstream: null, + conflictedCommit: null, + }, + "skip a commit": { + initial: "x=S:C2-1;C3-1;Cr-3;Bmaster=2;Br=r", + expected: "x=E:Crr-2 r=r;H=rr", + upstream: "3", + conflictedCommit: null, + }, + }; + Object.keys(cases).forEach(caseName => { + it(caseName, co.wrap(function *() { + const c = cases[caseName]; + const op = co.wrap(function *(repos, maps) { + const repo = repos.x; + const targetSha = maps.reverseCommitMap.r; + const targetCommit = yield repo.getCommit(targetSha); + let upstreamCommit = null; + if (null !== c.upstream) { + const upstreamSha = maps.reverseCommitMap[c.upstream]; + upstreamCommit = yield repo.getCommit(upstreamSha); + } + const result = yield SubmoduleRebaseUtil.rewriteCommits( + repo, + targetCommit, + upstreamCommit); + if (null === c.conflictedCommit) { + assert.isNull(result.conflictedCommit); + } else { + assert.equal( + result.conflictedCommit, + maps.reverseCommitMap[c.conflictedCommit]); + } + const commitMap = {}; + Object.keys(result.commits).forEach(newSha => { + const oldSha = result.commits[newSha]; + const oldLogicalCommit = maps.commitMap[oldSha]; + commitMap[newSha] = oldLogicalCommit + "r"; + }); + return { + commitMap: commitMap, + }; + }); + const options = {}; + if (undefined !== c.expectedTransformer) { + options.expectedTransformer = c.expectedTransformer; + } + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + c.expected, + op, + c.fails, + options); + })); + }); +}); +describe("continueSubmodules", function () { + const cases = { + "nothing to do": { + initial: `x=S`, + }, + "a closed sub": { + initial: "x=S:C2-1 s=Sa:1;Bmaster=2", + }, + "change in sub is staged": { + initial: "a=B:Ca-1;Ba=a|x=U:Os H=a", + expected: "x=E:I s=Sa:a", + }, + "rebase in a sub": { + initial: ` +a=B:Cq-1;Cr-1;Bq=q;Br=r| +x=U:C3-2 s=Sa:q;Bmaster=3;Os EHEAD,q,r!I q=q`, + expected: ` +x=E:I s=Sa:qs;Os Cqs-r q=q!H=qs!E` + }, + "rebase in a sub, was conflicted": { + initial: ` +a=B:Cq-1;Cr-1;Bq=q;Br=r| +x=U:C3-2 s=Sa:q;Bmaster=3;I *s=S:1*S:r*S:q;Os EHEAD,q,r!I q=q`, + expected: ` +x=E:I s=Sa:qs;Os Cqs-r q=q!H=qs!E` + }, + "rebase two in a sub": { + initial: ` +a=B:Cp-q;Cq-1;Cr-1;Bp=p;Br=r| +x=U:C3-2 s=Sa:q;Bmaster=3;Os EHEAD,p,r!I q=q!Bp=p`, + expected: ` +x=E:I s=Sa:ps;Os Cps-qs p=p!Cqs-r q=q!H=ps!E!Bp=p` + }, + "rebase in two subs": { + initial: ` +a=B:Cp-q;Cq-1;Cr-1;Cz-1;Bp=p;Br=r;Bz=z| +x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q,t=Sa:q;Bmaster=3; + Os EHEAD,p,r!I q=q!Bp=p; + Ot EHEAD,z,r!I z=8!Bz=z; +`, + expected: ` +x=E:I s=Sa:ps,t=Sa:zt; + Os Cps-qs p=p!Cqs-r q=q!H=ps!E!Bp=p; + Ot Czt-r z=8!H=zt!E!Bz=z; +`, + }, + "rebase in two subs, conflict in one": { + initial: ` +a=B:Cp-q r=8;Cq-1;Cr-1;Cz-1;Bp=p;Br=r;Bz=z| +x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q,t=Sa:q;Bmaster=3; + Os EHEAD,p,r!I q=q!Bp=p; + Ot EHEAD,z,r!I z=8!Bz=z; +`, + expected: ` +x=E:I t=Sa:zt; + Os Cqs-r q=q!H=qs!EHEAD,p,r!Bp=p!I *r=~*r*8!W r=^<<<<; + Ot Czt-r z=8!H=zt!E!Bz=z; +`, + errorMessage: `\ +Conflict in ${colors.red("s")} +`, + }, + "made a commit in a sub without a rebase": { + initial: `a=B|x=U:Cfoo#9-1;B9=9;Os I a=b`, + expected: `x=E:I s=Sa:Ns;Os Cfoo#Ns-1 a=b!H=Ns`, + baseCommit: "9", + message: "foo", + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const continuer = co.wrap(function *(repos, maps) { + const repo = repos.x; + const index = yield repo.index(); + const status = yield StatusUtil.getRepoStatus(repo); + const baseSha = c.baseCommit || "1"; + const baseCommit = + yield repo.getCommit(maps.reverseCommitMap[baseSha]); + const result = yield SubmoduleRebaseUtil.continueSubmodules( + repo, + index, + status, + baseCommit); + assert.equal(result.errorMessage, c.errorMessage || null); + const commitMap = {}; + RepoASTTestUtil.mapSubCommits(commitMap, + result.commits, + maps.commitMap); + Object.keys(result.newCommits).forEach(name => { + commitMap[result.newCommits[name]] = "N" + name; + }); + return { + commitMap: commitMap, + }; + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + c.expected, + continuer, + c.fails); + })); + }); +}); +}); From 51f5833477b328d7259c904160e1868cdad56c7a Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 27 Mar 2018 10:46:08 -0400 Subject: [PATCH 075/402] de-indent test cases --- node/test/util/rebase_util.js | 589 +++++++++++++++++++--------------- 1 file changed, 337 insertions(+), 252 deletions(-) diff --git a/node/test/util/rebase_util.js b/node/test/util/rebase_util.js index 40c8a989a..180593d21 100644 --- a/node/test/util/rebase_util.js +++ b/node/test/util/rebase_util.js @@ -59,331 +59,330 @@ function makeRebaser(operation) { } describe("rebase", function () { - describe("rebase", function () { +describe("rebase", function () { - // Will append the leter 'M' to any created meta-repo commits, and the - // submodule name to commits created in respective submodules. + // Will append the leter 'M' to any created meta-repo commits, and the + // submodule name to commits created in respective submodules. - function rebaser(repoName, commit) { - const rebaseOper = co.wrap(function *(repos, maps) { - assert.property(repos, repoName); - const repo = repos[repoName]; - const reverseCommitMap = maps.reverseCommitMap; - assert.property(reverseCommitMap, commit); - const originalActualCommit = reverseCommitMap[commit]; - const originalCommit = - yield repo.getCommit(originalActualCommit); + function rebaser(repoName, commit) { + const rebaseOper = co.wrap(function *(repos, maps) { + assert.property(repos, repoName); + const repo = repos[repoName]; + const reverseCommitMap = maps.reverseCommitMap; + assert.property(reverseCommitMap, commit); + const originalActualCommit = reverseCommitMap[commit]; + const originalCommit = + yield repo.getCommit(originalActualCommit); - return yield RebaseUtil.rebase(repo, originalCommit); - }); + return yield RebaseUtil.rebase(repo, originalCommit); + }); - return makeRebaser(rebaseOper); - } - const cases = { - "trivially nothing to do": { - initial: "x=S", - rebaser: rebaser("x", "1"), - }, - "nothing to do, in past": { - initial: "x=S:C2-1;Bmaster=2", - rebaser: rebaser("x", "1"), - }, - "ffwd": { - initial: "x=S:C2-1;Bfoo=2", - rebaser: rebaser("x", "2"), - expected: "x=E:Bmaster=2", - }, - "simple rebase": { - initial: "x=S:C2-1;C3-1;Bmaster=2;Bfoo=3", - rebaser: rebaser("x", "3"), - expected: "x=S:C2M-3 2=2;C3-1;Bmaster=2M;Bfoo=3", - }, - "rebase two commits": { - initial: "x=S:C2-1;C3-2;C4-1;Bmaster=3;Bfoo=4;Bx=3", - rebaser: rebaser("x", "4"), - expected: "x=E:C3M-2M 3=3;C2M-4 2=2;Bmaster=3M", - }, - "rebase two commits on two": { - initial: "x=S:C2-1;C3-2;C4-1;C5-4;Bmaster=3;Bfoo=5;Bx=3", - rebaser: rebaser("x", "5"), - expected: "x=E:C3M-2M 3=3;C2M-5 2=2;Bmaster=3M", - }, - "up-to-date with sub": { - initial: "a=Aa:Cb-a;Bfoo=b|x=U:C3-2 s=Sa:b;Bmaster=3;Bfoo=2", - rebaser: rebaser("x", "2"), - }, - "ffwd with sub": { - initial: "a=Aa:Cb-a;Bfoo=b|x=U:C3-2 s=Sa:b;Bmaster=2;Bfoo=3", - rebaser: rebaser("x", "3"), - expected: "x=E:Bmaster=3", - }, - "rebase change in closed sub": { - initial: "\ + return makeRebaser(rebaseOper); + } + const cases = { + "trivially nothing to do": { + initial: "x=S", + rebaser: rebaser("x", "1"), + }, + "nothing to do, in past": { + initial: "x=S:C2-1;Bmaster=2", + rebaser: rebaser("x", "1"), + }, + "ffwd": { + initial: "x=S:C2-1;Bfoo=2", + rebaser: rebaser("x", "2"), + expected: "x=E:Bmaster=2", + }, + "simple rebase": { + initial: "x=S:C2-1;C3-1;Bmaster=2;Bfoo=3", + rebaser: rebaser("x", "3"), + expected: "x=S:C2M-3 2=2;C3-1;Bmaster=2M;Bfoo=3", + }, + "rebase two commits": { + initial: "x=S:C2-1;C3-2;C4-1;Bmaster=3;Bfoo=4;Bx=3", + rebaser: rebaser("x", "4"), + expected: "x=E:C3M-2M 3=3;C2M-4 2=2;Bmaster=3M", + }, + "rebase two commits on two": { + initial: "x=S:C2-1;C3-2;C4-1;C5-4;Bmaster=3;Bfoo=5;Bx=3", + rebaser: rebaser("x", "5"), + expected: "x=E:C3M-2M 3=3;C2M-5 2=2;Bmaster=3M", + }, + "up-to-date with sub": { + initial: "a=Aa:Cb-a;Bfoo=b|x=U:C3-2 s=Sa:b;Bmaster=3;Bfoo=2", + rebaser: rebaser("x", "2"), + }, + "ffwd with sub": { + initial: "a=Aa:Cb-a;Bfoo=b|x=U:C3-2 s=Sa:b;Bmaster=2;Bfoo=3", + rebaser: rebaser("x", "3"), + expected: "x=E:Bmaster=3", + }, + "rebase change in closed sub": { + initial: "\ a=Aa:Cb-a;Cc-a;Bmaster=b;Bfoo=c|\ x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3", - rebaser: rebaser("x", "4"), - expected: "x=E:C3M-4 s=Sa:bs;Bmaster=3M;Os Cbs-c b=b!H=bs", - }, - "rebase change in sub, sub already open": { - initial: "\ + rebaser: rebaser("x", "4"), + expected: "x=E:C3M-4 s=Sa:bs;Bmaster=3M;Os Cbs-c b=b!H=bs", + }, + "rebase change in sub, sub already open": { + initial: "\ a=Aa:Cb-a;Cc-a;Bmaster=b;Bfoo=c|\ x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3;Os H=b", - rebaser: rebaser("x", "4"), - expected: "x=E:C3M-4 s=Sa:bs;Bmaster=3M;Os Cbs-c b=b!H=bs", - }, - "rebase change in sub with two commits": { - initial: "\ + rebaser: rebaser("x", "4"), + expected: "x=E:C3M-4 s=Sa:bs;Bmaster=3M;Os Cbs-c b=b!H=bs", + }, + "rebase change in sub with two commits": { + initial: "\ a=Aa:Cb-a;Cc-a;Bmaster=b;Bfoo=c|\ x=U:C4-2;C5-4 s=Sa:b;C3-2 s=Sa:c;Bmaster=5;Bfoo=5;Bother=3;Os H=b", - rebaser: rebaser("x", "3"), - expected: ` + rebaser: rebaser("x", "3"), + expected: ` x=E:C5M-4M s=Sa:bs;C4M-3 4=4;Bmaster=5M;Os Cbs-c b=b!H=bs`, - }, - "rebase change in sub with two intervening commits": { - initial: ` + }, + "rebase change in sub with two intervening commits": { + initial: ` a=Aa:Cb-a;Cc-a;Cd-c;Bmaster=b;Bfoo=d| x=U:C4-2;C5-4 s=Sa:c;C6-5;C7-6 s=Sa:d;C3-2 s=Sa:b;Bmaster=7;Bfoo=7;Bother=3`, - rebaser: rebaser("x", "3"), - expected: ` + rebaser: rebaser("x", "3"), + expected: ` x=E:C7M-6M s=Sa:ds;C6M-5M 6=6;C5M-4M s=Sa:cs;C4M-3 4=4;Bmaster=7M; Os Cds-cs d=d!Ccs-b c=c!H=ds`, - }, - "rebase change in sub with two intervening commits, open": { - initial: ` + }, + "rebase change in sub with two intervening commits, open": { + initial: ` a=Aa:Cb-a;Cc-a;Cd-c;Bmaster=b;Bfoo=d| x=U:C4-2;C5-4 s=Sa:c;C6-5;C7-6 s=Sa:d;C3-2 s=Sa:b;Bmaster=7;Bfoo=7;Bother=3; Os H=d`, - rebaser: rebaser("x", "3"), - expected: ` + rebaser: rebaser("x", "3"), + expected: ` x=E:C7M-6M s=Sa:ds;C6M-5M 6=6;C5M-4M s=Sa:cs;C4M-3 4=4;Bmaster=7M; Os Cds-cs d=d!Ccs-b c=c!H=ds`, - }, - "ffwd, but not sub (should ffwd anyway)": { - initial: "\ + }, + "ffwd, but not sub (should ffwd anyway)": { + initial: "\ a=Aa:Cb-a;Cc-a;Bmaster=b;Bfoo=c|\ x=U:C3-2 s=Sa:b;C4-3 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3", - rebaser: rebaser("x", "4"), - expected: "x=E:Bmaster=4", - }, - "no ffwd, but can ffwd sub": { - initial: "\ + rebaser: rebaser("x", "4"), + expected: "x=E:Bmaster=4", + }, + "no ffwd, but can ffwd sub": { + initial: "\ a=Aa:Cb-a;Cc-b;Bmaster=b;Bfoo=c|\ x=U:C3-2 3=3,s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3;Os", - rebaser: rebaser("x", "4"), - expected: "x=E:C3M-4 3=3;Bmaster=3M;Os H=c", - }, - "ffwd sub 2X": { - initial: ` + rebaser: rebaser("x", "4"), + expected: "x=E:C3M-4 3=3;Bmaster=3M;Os H=c", + }, + "ffwd sub 2X": { + initial: ` a=Aa:Cb-a;Cc-b;Bmaster=b;Bfoo=c| x=U:Cr-2;C3-2 s=Sa:b;C4-3 s=Sa:c;Bmaster=4;Bother=r;Os;Bfoo=4`, - rebaser: rebaser("x", "r"), - expected: "x=E:C3M-r s=Sa:b;C4M-3M s=Sa:c;Bmaster=4M;Os H=c", - }, - "ffwd-ed sub is closed after rebase": { - initial: "\ + rebaser: rebaser("x", "r"), + expected: "x=E:C3M-r s=Sa:b;C4M-3M s=Sa:c;Bmaster=4M;Os H=c", + }, + "ffwd-ed sub is closed after rebase": { + initial: "\ a=Aa:Cb-a;Cc-b;Bmaster=b;Bfoo=c|\ x=U:C3-2 3=3,s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3", - rebaser: rebaser("x", "4"), - expected: "x=E:C3M-4 3=3;Bmaster=3M", - }, - "rebase two changes in sub": { - initial: "\ + rebaser: rebaser("x", "4"), + expected: "x=E:C3M-4 3=3;Bmaster=3M", + }, + "rebase two changes in sub": { + initial: "\ a=Aa:Cb-a;Cc-b;Cd-a;Bmaster=c;Bfoo=d|\ x=U:C3-2 s=Sa:c;C4-2 s=Sa:d;Bmaster=3;Bfoo=4;Bother=3", - rebaser: rebaser("x", "4"), - expected: "\ + rebaser: rebaser("x", "4"), + expected: "\ x=E:C3M-4 s=Sa:cs;Bmaster=3M;Os Ccs-bs c=c!Cbs-d b=b!H=cs", - }, - "rebase with ffwd changes in sub and meta": { - initial: "\ + }, + "rebase with ffwd changes in sub and meta": { + initial: "\ a=B:Bmaster=3;C2-1 s=Sb:q;C3-2 s=Sb:r,rar=wow|\ b=B:Cq-1;Cr-q;Bmaster=r|\ x=Ca:Bmaster=2;Os", - rebaser: rebaser("x", "3"), - expected: "x=E:Bmaster=3;Os H=r", - }, - "make sure unchanged repos stay closed": { - initial: "\ + rebaser: rebaser("x", "3"), + expected: "x=E:Bmaster=3;Os H=r", + }, + "make sure unchanged repos stay closed": { + initial: "\ a=B|\ b=B:Cj-1;Ck-1;Bmaster=j;Bfoo=k|\ x=S:C2-1 s=Sa:1,t=Sb:1;C3-2 t=Sb:j;C4-2 t=Sb:k;Bmaster=3;Bfoo=4;Bold=3", - rebaser: rebaser("x", "4"), - expected: "\ + rebaser: rebaser("x", "4"), + expected: "\ x=E:C3M-4 t=Sb:jt;Bmaster=3M;Ot H=jt!Cjt-k j=j", - }, - "make sure unchanged repos stay closed -- onto-only change": { - initial: "\ + }, + "make sure unchanged repos stay closed -- onto-only change": { + initial: "\ a=B|\ b=B:Cj-1;Ck-1;Bmaster=j;Bfoo=k|\ x=S:C2-1 s=Sa:1,t=Sb:1;C3-2;C4-2 t=Sb:k;Bmaster=3;Bfoo=4;Bold=3", - rebaser: rebaser("x", "4"), - expected: "\ + rebaser: rebaser("x", "4"), + expected: "\ x=E:C3M-4 3=3;Bmaster=3M", - }, - "make sure unchanged repos stay closed -- local-only change": { - initial: "\ + }, + "make sure unchanged repos stay closed -- local-only change": { + initial: "\ a=B|\ b=B:Cj-1;Ck-1;Bmaster=j;Bfoo=k|\ x=S:C2-1 s=Sa:1,t=Sb:1;C3-2;C4-2 t=Sb:k;Bmaster=4;Bfoo=3;Bold=4", - rebaser: rebaser("x", "3"), - expected: "\ + rebaser: rebaser("x", "3"), + expected: "\ x=E:C4M-3 t=Sb:k;Bmaster=4M", - }, - "unchanged repos stay closed -- different onto and local": { - initial: "\ + }, + "unchanged repos stay closed -- different onto and local": { + initial: "\ a=B:Cj-1;Bmaster=j|\ b=B:Ck-1;Bmaster=k|\ x=S:C2-1 s=Sa:1,t=Sb:1;C3-2 s=Sa:j;C4-2 t=Sb:k;Bmaster=3;Bfoo=4;Bold=3", - rebaser: rebaser("x", "4"), - expected: "\ + rebaser: rebaser("x", "4"), + expected: "\ x=E:C3M-4 s=Sa:j;Bmaster=3M", - }, - "maintain submodule branch": { - initial: "\ + }, + "maintain submodule branch": { + initial: "\ a=B:Ca-1;Cb-1;Bx=a;By=b|\ x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4;Bold=3;Os Bmaster=a!*=master", - rebaser: rebaser("x", "4"), - expected: "\ + rebaser: rebaser("x", "4"), + expected: "\ x=E:C3M-4 s=Sa:as;Bmaster=3M;Os Bmaster=as!Cas-b a=a!*=master", - }, - "adding subs on both": { - initial: "\ + }, + "adding subs on both": { + initial: "\ q=B|r=B|s=B|x=S:C2-1 s=Ss:1;C3-2 q=Sq:1;C4-2 r=Sr:1;Bmaster=3;Bfoo=4;Bold=3", - rebaser: rebaser("x", "4"), - expected: "\ + rebaser: rebaser("x", "4"), + expected: "\ x=E:C3M-4 q=Sq:1;Bmaster=3M", - }, - "adding subs then changing": { - initial: "\ + }, + "adding subs then changing": { + initial: "\ q=B|\ r=B|\ s=B|\ x=S:C2-1 s=Ss:1;C3-2 q=Sq:1;C31-3 q=Sr:1;C4-2 r=Sr:1;C41-4 r=Ss:1;\ Bmaster=31;Bfoo=41;Bold=31", - rebaser: rebaser("x", "41"), - expected: "\ + rebaser: rebaser("x", "41"), + expected: "\ x=E:C3M-41 q=Sq:1;C31M-3M q=Sr:1;Bmaster=31M", - }, - "open sub ffwd'd": { - initial: ` + }, + "open sub ffwd'd": { + initial: ` a=B:CX-1;Bmaster=X| x=U:C3-2 a=b;C4-2 s=Sa:X;Bmaster=3;Bfoo=4;Bold=3;Os`, - rebaser: rebaser("x", "4"), - expected: ` + rebaser: rebaser("x", "4"), + expected: ` x=E:C3M-4 a=b;Bmaster=3M;Os H=X`, - }, - }; - Object.keys(cases).forEach(caseName => { - const c = cases[caseName]; - it(caseName, co.wrap(function *() { - yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, - c.expected, - c.rebaser, - c.fails); - })); - }); - it("conflict stays open", co.wrap(function *() { - const input = ` -a=B:Ca-1 t=t;Cb-1 t=u;Ba=a;Bb=b| -x=U:C31-2 s=Sa:a;C41-2 s=Sa:b;Bmaster=31;Bfoo=41;Bold=31`; - const w = yield RepoASTTestUtil.createMultiRepos(input); - const repo = w.repos.x; - const reverseCommitMap = w.reverseCommitMap; - const originalActualCommit = reverseCommitMap["41"]; - const originalCommit = yield repo.getCommit(originalActualCommit); - let threw = false; - try { - yield RebaseUtil.rebase(repo, originalCommit); - } - catch (e) { - threw = true; - } - assert(threw, "should have thrown"); - const open = yield SubmoduleUtil.isVisible(repo, "s"); - assert(open, "should be open"); + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + c.expected, + c.rebaser, + c.fails); })); }); + it("conflict stays open", co.wrap(function *() { + const input = ` +a=B:Ca-1 t=t;Cb-1 t=u;Ba=a;Bb=b| +x=U:C31-2 s=Sa:a;C41-2 s=Sa:b;Bmaster=31;Bfoo=41;Bold=31`; + const w = yield RepoASTTestUtil.createMultiRepos(input); + const repo = w.repos.x; + const reverseCommitMap = w.reverseCommitMap; + const originalActualCommit = reverseCommitMap["41"]; + const originalCommit = yield repo.getCommit(originalActualCommit); + let threw = false; + try { + yield RebaseUtil.rebase(repo, originalCommit); + } + catch (e) { + threw = true; + } + assert(threw, "should have thrown"); + const open = yield SubmoduleUtil.isVisible(repo, "s"); + assert(open, "should be open"); + })); +}); - describe("abort", function () { - const cases = { - "simple, see if workdir is cleaned up": { - initial: ` +describe("abort", function () { + const cases = { + "simple, see if workdir is cleaned up": { + initial: ` x=S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;W x=q`, - expected: `x=E:E;W x=~`, - }, - "with rebase in submodule": { - initial: ` + expected: `x=E:E;W x=~`, + }, + "with rebase in submodule": { + initial: ` a=B:Cq-1;Cr-1;Bmaster=q;Bfoo=r| x=U:C3-2 s=Sa:q;C4-2 s=Sa:r; Bmaster=3;Bfoo=4; Erefs/heads/master,3,4; Os Erefs/heads/foo,q,r!Bfoo=q!*=foo`, - expected: `x=E:E;Os Bfoo=q!*=foo`, - }, - }; - Object.keys(cases).forEach(caseName => { - const c = cases[caseName]; - it(caseName, co.wrap(function *() { - const aborter = co.wrap(function *(repos) { - yield RebaseUtil.abort(repos.x); - }); - yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, - c.expected, - aborter, - c.fails); - })); - }); + expected: `x=E:E;Os Bfoo=q!*=foo`, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const aborter = co.wrap(function *(repos) { + yield RebaseUtil.abort(repos.x); + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + c.expected, + aborter, + c.fails); + })); }); - - describe("continue", function () { - const cases = { - "meta-only": { - initial: ` +}); +describe("continue", function () { + const cases = { + "meta-only": { + initial: ` x=S:C2-1 q=r;C3-1 q=s;Bmaster=2;Erefs/heads/master,2,3;I q=z`, - expected: ` + expected: ` x=S:C2M-3 q=z;Bmaster=2M;E`, - }, - "two, meta-only": { - initial: ` + }, + "two, meta-only": { + initial: ` x=S:C2-1;C3-1;C4-3;Bmaster=4;Erefs/heads/master,4,2;I qq=hh,3=3`, - expected: ` + expected: ` x=S:C4M-3M 4=4;C3M-2 3=3,qq=hh;Bmaster=4M;E`, - }, - "meta, has to open": { - initial: ` + }, + "meta, has to open": { + initial: ` a=B:Ca-1;Cb-1;Bmaster=a;Bfoo=b| x=U:C3-2 s=Sa:a; C4-2;C5-4 s=Sa:b; Bmaster=5;Bfoo=5; I 4=4; Erefs/heads/master,5,3`, - expected: ` + expected: ` x=E:C5M-4M s=Sa:bs;C4M-3 4=4;Bmaster=5M;E;Os Cbs-a b=b!H=bs;I 4=~`, - }, - "with rebase in submodule": { - initial: ` + }, + "with rebase in submodule": { + initial: ` a=B:Cq-1;Cr-1;Bmaster=q;Bfoo=r| x=U:C3-2 s=Sa:q;C4-2 s=Sa:r; Bmaster=3;Bfoo=4;Bold=3; Erefs/heads/master,3,4; Os EHEAD,q,r!I q=q!Bq=q!Br=r`, - expected: ` + expected: ` x=E:E;C3M-4 s=Sa:qs;Bmaster=3M;Os Cqs-r q=q!H=qs!E!Bq=q!Br=r` - }, - "with rebase in submodule, other open subs": { - initial: ` + }, + "with rebase in submodule, other open subs": { + initial: ` a=B:Cq-1;Cr-1;Bmaster=q;Bfoo=r| x=S:C2-1 a=Sa:1,s=Sa:1,z=Sa:1;C3-2 s=Sa:q;C4-2 s=Sa:r; Bmaster=3;Bfoo=4;Bold=3; Erefs/heads/master,3,4; Oa;Oz; Os EHEAD,q,r!I q=q!Bq=q!Br=r`, - expected: ` + expected: ` x=E:E;C3M-4 s=Sa:qs;Bmaster=3M;Os Cqs-r q=q!H=qs!E!Bq=q!Br=r;Oa;Oz` - }, - "with rebase in submodule, staged commit in another submodule": { - initial: ` + }, + "with rebase in submodule, staged commit in another submodule": { + initial: ` a=B:Cq-1;Cr-1;Cs-q;Bmaster=q;Bfoo=r;Bbar=s| x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q,t=Sa:s;C4-2 s=Sa:r,t=Sa:q; Bmaster=3;Bfoo=4;Bold=3; @@ -391,33 +390,33 @@ x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q,t=Sa:s;C4-2 s=Sa:r,t=Sa:q; Os EHEAD,q,r!I q=q!Bq=q!Br=r; Ot H=s; I t=Sa:s`, - expected: ` + expected: ` x=E:E;C3M-4 s=Sa:qs,t=Sa:s;Bmaster=3M; Os Cqs-r q=q!H=qs!E!Bq=q!Br=r;I t=~;Ot` - }, - "with rebase in submodule, workdir commit in another submodule": { - initial: ` + }, + "with rebase in submodule, workdir commit in another submodule": { + initial: ` a=B:Cq-1;Cr-1;Cs-q;Bmaster=q;Bfoo=r;Bbar=s| x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q,t=Sa:s;C4-2 s=Sa:r,t=Sa:q; Bmaster=3;Bfoo=4;Bold=3; Erefs/heads/master,3,4; Os EHEAD,q,r!I q=q!Bq=q!Br=r; Ot H=s`, - expected: ` + expected: ` x=E:E;C3M-4 s=Sa:qs,t=Sa:s;Bmaster=3M; Os Cqs-r q=q!H=qs!E!Bq=q!Br=r; Ot H=s` - }, - "staged fix in submodule": { - initial: ` + }, + "staged fix in submodule": { + initial: ` a=B:Ca-1 q=r;Cb-1 q=s;Bmaster=a;Bfoo=b| x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Erefs/heads/master,3,4;Bold=3; Os EHEAD,a,b!I q=z!Ba=a!Bb=b`, - expected: ` + expected: ` x=E:C3M-4 s=Sa:as;E;Bmaster=3M;Os Cas-b q=z!H=as!Ba=a!Bb=b`, - }, - "multiple in subs": { - initial: ` + }, + "multiple in subs": { + initial: ` a=B:Ca1-1 f=g;Ca2-1 f=h;Bmaster=a1;Bfoo=a2| b=B:Cb1-1 q=r;Cb2-1 q=s;Bmaster=b1;Bfoo=b2| x=S:C2-1 s=Sa:1,t=Sb:1;C3-2 s=Sa:a1,t=Sb:b1;C4-2 s=Sa:a2,t=Sb:b2; @@ -425,24 +424,23 @@ x=S:C2-1 s=Sa:1,t=Sb:1;C3-2 s=Sa:a1,t=Sb:b1;C4-2 s=Sa:a2,t=Sb:b2; Erefs/heads/master,3,4; Os EHEAD,a1,a2!I f=z!Ba1=a1!Ba2=a2; Ot EHEAD,b1,b2!I q=t!Bb1=b1!Bb2=b2`, - expected: ` + expected: ` x=E:C3M-4 s=Sa:a1s,t=Sb:b1t;E;Bmaster=3M; Os Ca1s-a2 f=z!H=a1s!Ba1=a1!Ba2=a2; Ot Cb1t-b2 q=t!H=b1t!Bb1=b1!Bb2=b2` - }, - }; - Object.keys(cases).forEach(caseName => { - const c = cases[caseName]; - it(caseName, co.wrap(function *() { - const continuer = makeRebaser(co.wrap(function *(repos) { - return yield RebaseUtil.continue(repos.x); - })); - yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, - c.expected, - continuer, - c.fails); + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const continuer = makeRebaser(co.wrap(function *(repos) { + return yield RebaseUtil.continue(repos.x); })); - }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + c.expected, + continuer, + c.fails); + })); }); describe("listRebaseCommits", function () { @@ -531,3 +529,90 @@ x=E:C3M-4 s=Sa:a1s,t=Sb:b1t;E;Bmaster=3M; }); }); }); + +describe("listRebaseCommits", function () { + const cases = { + "same commit": { + input: "S", + from: "1", + onto: "1", + expected: [], + }, + "ancestor": { + input: "S:C2-1;Bfoo=2", + from: "1", + onto: "2", + expected: [], + }, + "descendant": { + input: "S:C2-1;Bfoo=2", + from: "2", + onto: "1", + expected: ["2"], + }, + "descendants": { + input: "S:C3-2;C2-1;Bfoo=3", + from: "3", + onto: "1", + expected: ["2", "3"], + }, + "merge of base": { + input: "S:C2-1;Bmaster=2;C4-3,1;C3-1;Bfoo=4", + from: "4", + onto: "2", + expected: ["3"], + }, + "non FFWD": { + input: "S:Cf-1;Co-1;Bf=f;Bo=o", + from: "f", + onto: "o", + expected: ["f"], + }, + "left-to-right": { + input: "S:Co-1;Cf-b,a;Ca-1;Cb-1;Bf=f;Bo=o", + from: "f", + onto: "o", + expected: ["b", "a"], + }, + "left-to-right and deep first": { + input: "S:Co-1;Cf-b,a;Ca-1;Cb-c;Cc-1;Bf=f;Bo=o", + from: "f", + onto: "o", + expected: ["c", "b", "a"], + }, + "double deep": { + input: "S:Co-1;Cf-b,a;Ca-1;Cb-c,d;Cc-1;Cd-1;Bf=f;Bo=o", + from: "f", + onto: "o", + expected: ["c", "d", "a"], + }, + "and deep on the right": { + input: "S:Co-1;Cf-b,a;Ca-q,r;Cq-1;Cr-1;Cb-1;Bf=f;Bo=o", + from: "f", + onto: "o", + expected: ["b", "q", "r"], + }, + "new commit in history more than once": { + input: "S:Co-1;Cf-r,a;Ca-q,r;Cq-1;Cr-1;Bf=f;Bo=o", + from: "f", + onto: "o", + expected: ["r", "q"], + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(c.input); + const repo = written.repo; + const old = written.oldCommitMap; + const from = yield repo.getCommit(old[c.from]); + const onto = yield repo.getCommit(old[c.onto]); + const result = yield RebaseUtil.listRebaseCommits(repo, + from, + onto); + const commits = result.map(sha => written.commitMap[sha]); + assert.deepEqual(commits, c.expected); + })); + }); +}); +}); From fd7101cb439f0288b8077ca995edf3f6a4923aa5 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 27 Mar 2018 13:46:12 -0400 Subject: [PATCH 076/402] Added `SequencerState.copy` To make a new `SequencerState` object by overriding parts of an existing one. --- node/lib/util/sequencer_state.js | 36 +++++++++++++++++++++++++++++++ node/test/util/sequencer_state.js | 29 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/node/lib/util/sequencer_state.js b/node/lib/util/sequencer_state.js index b9974fe89..db9f1963b 100644 --- a/node/lib/util/sequencer_state.js +++ b/node/lib/util/sequencer_state.js @@ -212,6 +212,42 @@ class SequencerState { this.d_currentCommit === rhs.d_currentCommit && this.d_message === rhs.d_message; } + + /** + * Return a new `SequencerState` object having the same value as this + * object except where overriden by the fields in the optionally specified + * `properties`. + * + * @param {Object} [properties] + * @param {String} [type] + * @param {CommitAndRef} [originalHead] + * @param {CommitAndRef} [target] + * @param {Number} [currentCommit] + * @param {[String]} [commits] + * @param {String|null} [message] + * @return {SequencerState} + */ + copy(properties) { + if (undefined === properties) { + properties = {}; + } else { + assert.isObject(properties); + } + return new SequencerState({ + type: ("type" in properties) ? properties.type : this.d_type, + originalHead: ("originalHead" in properties) ? + properties.originalHead : this.d_originalHead, + target: ("target" in properties) ? + properties.target : this.d_target, + currentCommit: ("currentCommit" in properties) ? + properties.currentCommit : this.d_currentCommit, + commits: ("commits" in properties) ? + properties.commits : this.d_commits, + message: ("message" in properties) ? + properties.message : this.d_message, + }); + } + } SequencerState.prototype.toString = function () { diff --git a/node/test/util/sequencer_state.js b/node/test/util/sequencer_state.js index 2e20ac9da..2e6e7e69a 100644 --- a/node/test/util/sequencer_state.js +++ b/node/test/util/sequencer_state.js @@ -243,6 +243,35 @@ const CommitAndRef = SequencerState.CommitAndRef; }); }); }); + it("copy", function () { + const s0 = new SequencerState({ + type: TYPE.CHERRY_PICK, + originalHead: new CommitAndRef("1", "2"), + target: new CommitAndRef("a", "b"), + currentCommit: 0, + commits: ["a"], + message: "yo", + }); + const s1 = new SequencerState({ + type: TYPE.MERGE, + originalHead: new CommitAndRef("u", "v"), + target: new CommitAndRef("8", "8"), + currentCommit: 1, + commits: ["1", "3"], + message: "there", + }); + const defaults = s0.copy(); + assert.deepEqual(defaults, s0); + const overridden = s0.copy({ + type: s1.type, + originalHead: s1.originalHead, + target: s1.target, + commits: s1.commits, + currentCommit: s1.currentCommit, + message: s1.message, + }); + assert.deepEqual(overridden, s1); + }); it("toString", function () { const input = new SequencerState({ type: TYPE.REBASE, From 79f885e6fdbc0f201e6091f64482d505b16dff73 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 27 Mar 2018 13:58:57 -0400 Subject: [PATCH 077/402] Allow `currentCommit` to indicate end --- node/lib/util/sequencer_state.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/node/lib/util/sequencer_state.js b/node/lib/util/sequencer_state.js index db9f1963b..ed4e845b1 100644 --- a/node/lib/util/sequencer_state.js +++ b/node/lib/util/sequencer_state.js @@ -113,7 +113,9 @@ CommitAndRef.prototype.toString = function () { class SequencerState { /** * Create a new `SequencerState` object. The behavior is undefined unless - * `0 <= currentLength` and `commits.length > currentCommit`. + * `0 <= currentCommit` and `commits.length >= currentCommit`. If + * `commits.length === currentCommit`, there are no more commits left on + * which to operate. * * @param {Object} properties * @param {TYPE} properties.type @@ -131,7 +133,7 @@ class SequencerState { assert.isArray(properties.commits); assert.isNumber(properties.currentCommit); assert(0 <= properties.currentCommit); - assert(properties.commits.length > properties.currentCommit); + assert(properties.commits.length >= properties.currentCommit); this.d_message = null; if ("message" in properties) { From 94b8240391b94a8d4d2186c940ffcc7781f6392b Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 2 Apr 2018 15:17:07 -0400 Subject: [PATCH 078/402] fix spelling --- node/lib/util/submodule_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 1eae56b73..db132808b 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -262,7 +262,7 @@ exports.listOpenSubmodules = co.wrap(function *(repo) { // In at least one situation -- rebase -- Git will add a submodule to // the `.git/config` file without actually opening it, meaning that the // `.git/config` file cannot be used as the single source of truth and we - // must verify with `isVisble`, which looks for a repositories `.git` file. + // must verify with `isVisible`, which looks for a repositories `.git` file. // Also, we need to make sure that the submodule is included in the // `.gitmodules` file. If a user abandons a submodule while adding it, it // may have a lingering reference in `.git/config` even though it's been From 4bbae180f5289ff850fcdcd0d5f396ef77cf12a2 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 2 Apr 2018 16:06:22 -0400 Subject: [PATCH 079/402] Break indentation out into a function As a side-effect, this fixes some broken indentation code, which was doing four copies of the empty string instead of four spaces. Later, we will use this elsewhere in the code. --- node/lib/util/repo_ast_util.js | 27 +++++------------- node/lib/util/text_util.js | 51 ++++++++++++++++++++++++++++++++++ node/test/util/text_util.js | 49 ++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 20 deletions(-) create mode 100644 node/lib/util/text_util.js create mode 100644 node/test/util/text_util.js diff --git a/node/lib/util/repo_ast_util.js b/node/lib/util/repo_ast_util.js index 9b60b9487..445fc6d1d 100644 --- a/node/lib/util/repo_ast_util.js +++ b/node/lib/util/repo_ast_util.js @@ -40,6 +40,7 @@ const colors = require("colors"); const deeper = require("deeper"); const RepoAST = require("../util/repo_ast"); +const TextUtil = require("../util/text_util"); const Sequencer = RepoAST.SequencerState; const CommitAndRef = Sequencer.CommitAndRef; @@ -244,7 +245,6 @@ ${colorAct(actual.branches[branch])}` */ function diffASTs(actual, expected) { let result = []; - const indent = "".repeat(4); // First, check the commits @@ -258,9 +258,7 @@ function diffASTs(actual, expected) { const diffs = diffCommits(actual.commits[id], expected.commits[id]); if (0 !== diffs.length) { result.push(`for commit ${colorBad(id)}`); - diffs.forEach(diff => { - result.push(indent + diff); - }); + result.push(...diffs.map((d) => TextUtil.indent(d))); } } diffObjects(actual.commits, @@ -282,9 +280,7 @@ function diffASTs(actual, expected) { expected.remotes[remote]); if (0 !== diffs.length) { result.push(`for remote ${colorBad(remote)}`); - diffs.forEach(diff => { - result.push(indent + diff); - }); + result.push(...diffs.map((d) => TextUtil.indent(d))); } } diffObjects(actual.remotes, @@ -425,9 +421,7 @@ ${colorExp(expected.currentBranchName)}` const indexChanges = diffChanges(actual.index, expected.index); if (0 !== indexChanges.length) { result.push(`In ${colorBad("index")}`); - indexChanges.forEach(diff => { - result.push(indent + diff); - }); + result.push(...indexChanges.map((d) => TextUtil.indent(d))); } // Then, check the working directory. @@ -435,9 +429,7 @@ ${colorExp(expected.currentBranchName)}` const workdirChanges = diffChanges(actual.workdir, expected.workdir); if (0 !== workdirChanges.length) { result.push(`In ${colorBad("workdir")}`); - workdirChanges.forEach(diff => { - result.push(indent + diff); - }); + result.push(...workdirChanges.map((d) => TextUtil.indent(d))); } // Check open submodules @@ -453,9 +445,7 @@ ${colorExp(expected.currentBranchName)}` expected.openSubmodules[name]); if (0 !== diffs.length) { result.push(`for open submodule ${colorBad(name)}`); - diffs.forEach(diff => { - result.push(indent + diff); - }); + result.push(...diffs.map((d) => TextUtil.indent(d))); } } diffObjects(actual.openSubmodules, @@ -571,7 +561,6 @@ exports.assertEqualRepoMaps = function (actual, expected, message) { assert.isString(message); } let result = []; - const indent = "".repeat(4); // First, check the commits @@ -587,9 +576,7 @@ exports.assertEqualRepoMaps = function (actual, expected, message) { const diffs = diffASTs(actualRepo, expectedRepo); if (0 !== diffs.length) { result.push(`for repo ${colorBad(name)}`); - diffs.forEach(diff => { - result.push(indent + diff); - }); + result.push(...diffs.map((d) => TextUtil.indent(d))); } } diffObjects(actual, diff --git a/node/lib/util/text_util.js b/node/lib/util/text_util.js new file mode 100644 index 000000000..478bfd05e --- /dev/null +++ b/node/lib/util/text_util.js @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; + +/** + * Indent a single string + * @param {String} str + * @param {Integer} count number of spaces to indent (default 4) + * @return {String} The string, indented + */ +exports.indent = function(str, count) { + assert.isString(str); + if (undefined !== count) { + assert.isNumber(count); + assert(count > 0); + } + else { + count = 4; + } + return " ".repeat(count) + str; +}; diff --git a/node/test/util/text_util.js b/node/test/util/text_util.js new file mode 100644 index 000000000..97b9af1f7 --- /dev/null +++ b/node/test/util/text_util.js @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; + +const TextUtil = require("../../lib/util/text_util"); + +describe("TextUtil", function () { + describe("indent", function () { + + // I don't know if we can verify that the returned directories are + // "temporary", but we can verify that they subsequent calls return + // different paths that are directories. + + it("breathing test", function () { + assert.equal(" morx", TextUtil.indent("morx")); + assert.equal(" three", TextUtil.indent("three", 3)); + }); + }); +}); From 88fcbb3706477e7f81acf7c9c9cf0ddca2a69ec3 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 2 Apr 2018 16:33:55 -0400 Subject: [PATCH 080/402] add a strcmp function --- node/lib/util/print_status_util.js | 7 ++----- node/lib/util/text_util.js | 8 ++++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/node/lib/util/print_status_util.js b/node/lib/util/print_status_util.js index 077c68141..47b88f568 100644 --- a/node/lib/util/print_status_util.js +++ b/node/lib/util/print_status_util.js @@ -44,6 +44,7 @@ const GitUtil = require("./git_util"); const SequencerState = require("./sequencer_state"); const Rebase = require("./rebase"); const RepoStatus = require("./repo_status"); +const TextUtil = require("./text_util"); /** * This value-semantic class describes a line entry to be printed in a status @@ -117,11 +118,7 @@ exports.StatusDescriptor = StatusDescriptor; * @return {StatusDescriptor []} */ exports.sortDescriptorsByPath = function (descriptors) { - return descriptors.sort((l, r) => { - const lPath = l.path; - const rPath = r.path; - return lPath === rPath ? 0 : (lPath < rPath ? -1 : 1); - }); + return descriptors.sort((l, r) => TextUtil.strcmp(l.path, r.path)); }; /** diff --git a/node/lib/util/text_util.js b/node/lib/util/text_util.js index 478bfd05e..3a6646204 100644 --- a/node/lib/util/text_util.js +++ b/node/lib/util/text_util.js @@ -32,6 +32,14 @@ const assert = require("chai").assert; +/** + * Compare two strings for Array.prototype.sort. This is a useful + * building-block for more complex comparison functions. + */ +exports.strcmp = function(a, b) { + return (a < b) ? -1 : ((a > b) ? 1 : 0); +}; + /** * Indent a single string * @param {String} str From b982e1f5ce85a8ee2a03e0461edff4cb6cd56924 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 27 Mar 2018 20:54:24 -0400 Subject: [PATCH 081/402] Factory CherryPickUtil to be more reusable --- node/lib/util/cherry_pick_util.js | 149 ++++++++++------------ node/lib/util/submodule_rebase_util.js | 49 ++++++- node/test/util/cherry_pick_util.js | 163 +++++++++++++++++------- node/test/util/submodule_rebase_util.js | 20 ++- 4 files changed, 249 insertions(+), 132 deletions(-) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index aa6cc943a..7ef11048d 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -343,7 +343,7 @@ exports.pickSubs = co.wrap(function *(metaRepo, opener, metaIndex, subs) { const change = subs[name]; const commitText = "(" + GitUtil.shortSha(change.oldSha) + ".." + GitUtil.shortSha(change.newSha) + "]"; - console.log(`Sub-repo ${colors.blue(name)}: cherry-picking commits \ + console.log(`Sub-repo ${colors.blue(name)}: applying commits \ ${colors.green(commitText)}.`); // Fetch the commit; it may not be present. @@ -388,27 +388,73 @@ Conflicting entries for submodule ${colors.red(name)} }); /** - * Finish the cherry-pick in progress in the specified `repo` having the - * specified `index`. Use the message and signature from the specified - * original `commit`. Return the sha of the newly-created commit. + * Rewrite the specified `commit` on top of HEAD in the specified `repo` using + * the specified `opener` to open submodules as needed. The behavior is + * undefined unless the repository is clean. Return an object describing the + * commits that were made and any error message; if no commit was made (because + * there were no changes to commit), `newMetaCommit` will be null. Throw a + * `UserError` if URL changes or direct meta-repo changes are present in + * `commit`. * * @param {NodeGit.Repository} repo - * @param {NodeGit.Index} index * @param {NodeGit.Commit} commit - * @return {String} + * @param {Open.Opener} opener + * @return {Object} return + * @return {String|null} return.newMetaCommit + * @return {Object} returm.submoduleCommits + * @return {String|null} return.errorMessage */ -const finish = co.wrap(function *(repo, commit) { +exports.rewriteCommit = co.wrap(function *(repo, commit, opener) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); + assert.instanceOf(opener, Open.Opener); - const defaultSig = repo.defaultSignature(); - const metaCommit = yield repo.createCommitOnHead([], - defaultSig, - commit.committer(), - commit.message()); - yield SequencerStateUtil.cleanSequencerState(repo.path()); - console.log("Cherry-pick complete."); - return metaCommit.tostrS(); + const hasUrlChanges = yield exports.containsUrlChanges(repo, commit); + if (hasUrlChanges) { + // TODO: Dealing with these would be a huge hassle and is probably not + // worth it at the moment since the recommended policy for monorepo + // implementations is to prevent users from making URL changes anyway. + + throw new UserError(`\ +Applying commits with submodule URL changes is not currently supported. +Please try with normal 'git cherry-pick'.`); + } + + const changes = yield exports.computeChanges(repo, commit); + const index = yield repo.index(); + + // Perform simple changes that don't require picks -- addition, deletions, + // and fast-forwards. + + yield exports.changeSubmodules(repo, opener, index, changes.simpleChanges); + + // Render any conflicts + + let errorMessage = + yield exports.writeConflicts(repo, index, changes.conflicts); + + // Then do the cherry-picks. + + const picks = yield exports.pickSubs(repo, opener, index, changes.changes); + + const conflicts = picks.conflicts; + Object.keys(conflicts).sort().forEach(name => { + errorMessage += `Submodule ${colors.red(name)} is conflicted.\n`; + }); + + const result = { + submoduleCommits: picks.commits, + errorMessage: errorMessage === "" ? null : errorMessage, + newMetaCommit: null, + }; + yield index.write(); + if ("" === errorMessage && + (0 !== Object.keys(changes.simpleChanges).length || + 0 !== Object.keys(picks.commits).length)) { + result.newMetaCommit = + yield SubmoduleRebaseUtil.makeCommit(repo, commit); + } + return result; }); /** @@ -453,20 +499,6 @@ The repository has uncommitted changes. Please stash or commit them before running cherry-pick.`); } - const hasUrlChanges = yield exports.containsUrlChanges(metaRepo, commit); - if (hasUrlChanges) { - // TODO: Dealing with these would be a huge hassle and is probably not - // worth it at the moment since the recommended policy for monorepo - // implementations is to prevent users from making URL changes anyway. - - throw new UserError(`\ -Cherry-picking commits with submodule URL changes is not currently supported. -Please try with normal 'git cherry-pick'.`); - } - - const changes = yield exports.computeChanges(metaRepo, commit); - const metaIndex = yield metaRepo.index(); - // We're going to attempt a cherry-pick if we've made it this far, record a // cherry-pick file. @@ -479,47 +511,13 @@ Please try with normal 'git cherry-pick'.`); commits: [commit.id().tostrS()], }); yield SequencerStateUtil.writeSequencerState(metaRepo.path(), seq); - const opener = new Open.Opener(metaRepo, null); - - // Perform simple changes that don't require picks -- addition, deletions, - // and fast-forwards. - - yield exports.changeSubmodules(metaRepo, - opener, - metaIndex, - changes.simpleChanges); - - // Render any conflicts - - let errorMessage = - yield exports.writeConflicts(metaRepo, metaIndex, changes.conflicts); - - // Then do the cherry-picks. - - const picks = yield exports.pickSubs(metaRepo, - opener, - metaIndex, - changes.changes); - - const conflicts = picks.conflicts; - Object.keys(conflicts).sort().forEach(name => { - errorMessage += `Submodule ${colors.red(name)} is conflicted.\n`; - }); - - const result = { - submoduleCommits: picks.commits, - errorMessage: errorMessage === "" ? null : errorMessage, - newMetaCommit: null, - }; - yield metaIndex.write(); - if ("" === errorMessage) { - if (0 === Object.keys(changes.simpleChanges).length && - 0 === Object.keys(picks.commits).length) { - yield SequencerStateUtil.cleanSequencerState(metaRepo.path()); - throw new UserError("Nothing to commit."); - } - result.newMetaCommit = yield finish(metaRepo, commit); + const result = yield exports.rewriteCommit(metaRepo, commit, opener); + if (null === result.errorMessage) { + yield SequencerStateUtil.cleanSequencerState(metaRepo.path()); + } + if (null === result.newMetaCommit) { + console.log("Nothing to commit."); } return result; }); @@ -553,23 +551,14 @@ exports.continue = co.wrap(function *(repo) { index, status, commit); - yield index.write(); const result = { - newMetaCommit: null, + newMetaCommit: subResult.metaCommit, submoduleCommits: subResult.commits, newSubmoduleCommits: subResult.newCommits, errorMessage: subResult.errorMessage, }; - yield index.write(); if (null === subResult.errorMessage) { - if (status.isIndexDeepClean() && - 0 === Object.keys(subResult.commits).length && - 0 === Object.keys(subResult.newCommits).length) { - yield SequencerStateUtil.cleanSequencerState(repo.path()); - throw new UserError("Nothing to commit."); - } - - result.newMetaCommit = yield finish(repo, commit); + yield SequencerStateUtil.cleanSequencerState(repo.path()); } return result; }); diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index 4c5a990e9..a447822d8 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -75,6 +75,29 @@ const cleanupRebaseDir = co.wrap(function *(repo) { } }); +/** + * Make a new commit on the head of the specified `repo` having the same + * committer and message as the specified original `commit`, and return its + * sha. + * + * TODO: independent test + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + * @return {String} + */ +exports.makeCommit = co.wrap(function *(repo, commit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(commit, NodeGit.Commit); + + const defaultSig = repo.defaultSignature(); + const metaCommit = yield repo.createCommitOnHead([], + commit.author(), + defaultSig, + commit.message()); + return metaCommit.tostrS(); +}); + /** * Finish the specified `rebase` in the specified `repo`. Note that this * method is necessary only as a workaround for: @@ -209,12 +232,22 @@ exports.subConflictErrorMessage = function (name) { return `Conflict in ${colors.red(name)}`; }; +/** + * Log a message indicating that the specified `commit` is being applied. + * + * @param {NodeGit.Commit} commit + */ +exports.logCommit = function (commit) { + assert.instanceOf(commit, NodeGit.Commit); + console.log(`Applying '${commit.message().split("\n")[0]}'`); +}; /** * Continue rebases in the submodules in the specifed `repo` having the * `index and `status`. If staged changes are found in submodules that don't * have in-progress rebases, commit them using the specified message and - * signature from the specified original `commit`. Return an object describing + * signature from the specified original `commit`. If there are any changes to + * commit, make a new commit in the meta-repo. Return an object describing * any commits that were generated along with an error message if any continues * failed. * @@ -223,6 +256,7 @@ exports.subConflictErrorMessage = function (name) { * @param {RepoStatus} status * @param {NodeGit.Commit} commit * @return {Object} + * @return {String|null} metaCommit * @return {Object} return.commits map from name to sha map * @return {Object} return.newCommits from name to newly-created commits * @return {String|null} return.errorMessage @@ -233,6 +267,7 @@ exports.continueSubmodules = co.wrap(function *(repo, index, status, commit) { assert.instanceOf(status, RepoStatus); assert.instanceOf(commit, NodeGit.Commit); + exports.logCommit(commit); const commits = {}; const newCommits = {}; const subs = status.submodules; @@ -274,9 +309,19 @@ ${colors.green(rebaseInfo.onto)}.`); } }); yield DoWorkQueue.doInParallel(Object.keys(subs), continueSub); - return { + yield index.write(); + const result = { errorMessage: "" === errorMessage ? null : errorMessage, commits: commits, newCommits: newCommits, + metaCommit: null, }; + if (null === result.errorMessage) { + if (status.isIndexDeepClean()) { + console.log("Nothing to commit."); + } else { + result.metaCommit = yield exports.makeCommit(repo, commit); + } + } + return result; }); diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index b1c5c3837..035763cbd 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -42,6 +42,50 @@ const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); const Submodule = require("../../lib/util/submodule"); const SubmoduleChange = require("../../lib/util/submodule_change"); +/** + * Return a commit map as expected from a manipulator for `RepoASTTestUtil` + * from a result having the `newMetaCommit` and `submoduleCommits` properties + * as returned by `rewriteCommit`. We remap as follows: + * + * * new meta commit will be called "9" (we generally cherry-pick from "8") + * * new submodule commits will be $old-logical-name + submoduleName, e.g.: + * "as" would be the name of a commit named "a" cherry-picked for submodule + * "s". + * + * @param {Object} maps + * @param {Object} result + * @return {Object} + */ +function mapCommits(maps, result) { + const oldMap = maps.commitMap; + + let commitMap = {}; + if (null !== result.newMetaCommit) { + // By convention, we name the cherry-pick generated meta-repo commit, + // if it exists, "9". + + commitMap[result.newMetaCommit] = "9"; + } + + // For the submodules, we need to first figure out what the old + // logical commit (the one from the shorthand) was, then create the + // new logical commit id by appending the submodule name. We map + // the new (physical) commit id to this new logical id. + + Object.keys(result.submoduleCommits).forEach(name => { + const subCommits = result.submoduleCommits[name]; + Object.keys(subCommits).forEach(newPhysicalId => { + const oldId = subCommits[newPhysicalId]; + const oldLogicalId = oldMap[oldId]; + const newLogicalId = oldLogicalId + name; + commitMap[newPhysicalId] = newLogicalId; + }); + }); + return { + commitMap: commitMap, + }; +} + describe("CherryPickUtil", function () { const Conflict = ConflictUtil.Conflict; const ConflictEntry = ConflictUtil.ConflictEntry; @@ -511,7 +555,7 @@ Conflicting entries for submodule ${colors.red("z")} })); }); }); -describe("cherryPick", function () { +describe("rewriteCommit", function () { // We will always cherry-pick commit 8 from repo x and will name the // meta-repo cherry-picked commit 9. Cherry-picked commits from // submodules will have the submodule name appended to the commit. @@ -545,19 +589,6 @@ x=S:C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2;Os H=x`, }, "nothing to commit": { input: "a=B|x=S:C2-1;C8-1 ;Bmaster=2;B8=8", - fails: true, - }, - "in-progress will fail": { - input: ` -a=Ax:Cz-y;Cy-x;Bfoo=z| -x=S:QC 2: 1: 0 2;C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2`, - fails: true, - }, - "dirty will fail": { - input: ` -a=Ax:Cz-y;Cy-x;Bfoo=z| -x=S:C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2;Os W x=9`, - fails: true, }, "URL change will fail": { input: ` @@ -630,7 +661,7 @@ x=E:C9-2 t=Sa:qt;Bmaster=9;Ot Cqt-1 q=q!H=qt`, a=B:Ca-1;Cb-1 a=8;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;C8-2 s=Sa:b;Bmaster=3;Bfoo=8`, expected: ` -x=E:QC 3: 8: 0 8;Os Edetached HEAD,b,a!I *a=~*a*8!W a=\ +x=E:Os Edetached HEAD,b,a!I *a=~*a*8!W a=\ <<<<<<< HEAD a ======= @@ -647,7 +678,7 @@ Submodule ${colors.red("s")} is conflicted. a=B:Ca-1;Cb-1 a=8;Cc-1;Ba=a;Bb=b;Bc=c| x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:a,t=Sa:a;C8-2 s=Sa:b,t=Sa:c;Bmaster=3;Bfoo=8`, expected: ` -x=E:QC 3: 8: 0 8;I t=Sa:ct;Ot Cct-a c=c!H=ct; +x=E:I t=Sa:ct;Ot Cct-a c=c!H=ct; Os Edetached HEAD,b,a!I *a=~*a*8!W a=\ <<<<<<< HEAD a @@ -664,52 +695,97 @@ Submodule ${colors.red("s")} is conflicted. input: ` a=B:Ca-1;Ba=a| x=U:C3-2 s=foo;C8-2 s=Sa:a;Bmaster=3;Bfoo=8`, - expected: `x=E:QC 3: 8: 0 8;I *s=S:1*foo*S:a;W s=foo`, + expected: `x=E:I *s=S:1*foo*S:a;W s=foo`, errorMessage: `\ Conflicting entries for submodule ${colors.red("s")} `, }, }; - Object.keys(cases).forEach(caseName => { const c = cases[caseName]; const picker = co.wrap(function *(repos, maps) { const x = repos.x; - const oldMap = maps.commitMap; const reverseCommitMap = maps.reverseCommitMap; assert.property(reverseCommitMap, "8"); const eightCommitSha = reverseCommitMap["8"]; const eightCommit = yield x.getCommit(eightCommitSha); - const result = yield CherryPickUtil.cherryPick(x, eightCommit); - + const opener = new Open.Opener(x, null); + const result = yield CherryPickUtil.rewriteCommit(x, + eightCommit, + opener); assert.equal(result.errorMessage, c.errorMessage || null); + return mapCommits(maps, result); + }); + + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.input, + c.expected, + picker, + c.fails); + })); + }); +}); +describe("cherryPick", function () { + // Most of the work of cherry-pick is done by `rewriteCommit` and other + // methods. We just need to validate here that we're ensuring the contract + // that we're in a good state, that we properly record and cleanup the + // sequencer. - // Now we need to build a map from new physical commit id to new - // logical commit id. For the meta commit, this is easy: we map - // the new id to the hard-coded value of "9". + const cases = { + "picking one sub": { + input: ` +a=Ax:Cz-y;Cy-x;Bfoo=z| +x=S:C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2;Os H=x`, + expected: "x=E:C9-2 s=Sa:zs;Bmaster=9;Os Czs-x z=z!H=zs", + }, + "nothing to commit": { + input: "a=B|x=S:C2-1;C8-1 ;Bmaster=2;B8=8", + }, + "in-progress will fail": { + input: ` +a=Ax:Cz-y;Cy-x;Bfoo=z| +x=S:QC 2: 1: 0 2;C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2`, + fails: true, + }, + "dirty will fail": { + input: ` +a=Ax:Cz-y;Cy-x;Bfoo=z| +x=S:C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2;Os W x=9`, + fails: true, + }, + "conflict in a sub pick": { + input: ` +a=B:Ca-1;Cb-1 a=8;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C8-2 s=Sa:b;Bmaster=3;Bfoo=8`, + expected: ` +x=E:QC 3: 8: 0 8;Os Edetached HEAD,b,a!I *a=~*a*8!W a=\ +<<<<<<< HEAD +a +======= +8 +>>>>>>> message +; +`, + errorMessage: `\ +Submodule ${colors.red("s")} is conflicted. +`, + }, + }; - let commitMap = {}; - commitMap[result.newMetaCommit] = "9"; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + const picker = co.wrap(function *(repos, maps) { + const x = repos.x; + const reverseCommitMap = maps.reverseCommitMap; + assert.property(reverseCommitMap, "8"); + const eightCommitSha = reverseCommitMap["8"]; + const eightCommit = yield x.getCommit(eightCommitSha); + const result = yield CherryPickUtil.cherryPick(x, eightCommit); - // For the submodules, we need to first figure out what the old - // logical commit (the one from the shorthand) was, then create the - // new logical commit id by appending the submodule name. We map - // the new (physical) commit id to this new logical id. + assert.equal(result.errorMessage, c.errorMessage || null); - Object.keys(result.submoduleCommits).forEach(name => { - const subCommits = result.submoduleCommits[name]; - Object.keys(subCommits).forEach(newPhysicalId => { - const oldId = subCommits[newPhysicalId]; - const oldLogicalId = oldMap[oldId]; - const newLogicalId = oldLogicalId + name; - commitMap[newPhysicalId] = newLogicalId; - }); - }); - return { - commitMap: commitMap, - }; + return mapCommits(maps, result); }); - it(caseName, co.wrap(function *() { yield RepoASTTestUtil.testMultiRepoManipulator(c.input, c.expected, @@ -742,7 +818,6 @@ x=E:Q;Cfoo#CP-3 s=Sa:bs;Bmaster=CP;Os Cbs-a b=b!E`, "nothing to do": { input: "a=B|x=U:Os;Cfoo#g;Bg=g;QC 2: g: 0 g", expected: "x=E:Q", - fails: true, }, "continue with staged files": { input: "a=B|x=U:Os I foo=bar;Cfoo#g;Bg=g;QC 2: g: 0 g", diff --git a/node/test/util/submodule_rebase_util.js b/node/test/util/submodule_rebase_util.js index 862ac8c2a..cd1d56b7a 100644 --- a/node/test/util/submodule_rebase_util.js +++ b/node/test/util/submodule_rebase_util.js @@ -244,28 +244,32 @@ describe("continueSubmodules", function () { }, "change in sub is staged": { initial: "a=B:Ca-1;Ba=a|x=U:Os H=a", - expected: "x=E:I s=Sa:a", + expected: "x=E:Cadded 's'#M-2 s=Sa:a;Bmaster=M", + baseCommit: "2", }, "rebase in a sub": { initial: ` a=B:Cq-1;Cr-1;Bq=q;Br=r| x=U:C3-2 s=Sa:q;Bmaster=3;Os EHEAD,q,r!I q=q`, expected: ` -x=E:I s=Sa:qs;Os Cqs-r q=q!H=qs!E` +x=E:CM-3 s=Sa:qs;Bmaster=M;Os Cqs-r q=q!H=qs!E`, + baseCommit: "3", }, "rebase in a sub, was conflicted": { initial: ` a=B:Cq-1;Cr-1;Bq=q;Br=r| x=U:C3-2 s=Sa:q;Bmaster=3;I *s=S:1*S:r*S:q;Os EHEAD,q,r!I q=q`, expected: ` -x=E:I s=Sa:qs;Os Cqs-r q=q!H=qs!E` +x=E:CM-3 s=Sa:qs;Bmaster=M;I s=~;Os Cqs-r q=q!H=qs!E`, + baseCommit: "3", }, "rebase two in a sub": { initial: ` a=B:Cp-q;Cq-1;Cr-1;Bp=p;Br=r| x=U:C3-2 s=Sa:q;Bmaster=3;Os EHEAD,p,r!I q=q!Bp=p`, + baseCommit: "3", expected: ` -x=E:I s=Sa:ps;Os Cps-qs p=p!Cqs-r q=q!H=ps!E!Bp=p` +x=E:CM-3 s=Sa:ps;I s=~;Bmaster=M;Os Cps-qs p=p!Cqs-r q=q!H=ps!E!Bp=p` }, "rebase in two subs": { initial: ` @@ -274,8 +278,9 @@ x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q,t=Sa:q;Bmaster=3; Os EHEAD,p,r!I q=q!Bp=p; Ot EHEAD,z,r!I z=8!Bz=z; `, + baseCommit: "3", expected: ` -x=E:I s=Sa:ps,t=Sa:zt; +x=E:CM-3 s=Sa:ps,t=Sa:zt;Bmaster=M; Os Cps-qs p=p!Cqs-r q=q!H=ps!E!Bp=p; Ot Czt-r z=8!H=zt!E!Bz=z; `, @@ -298,7 +303,7 @@ Conflict in ${colors.red("s")} }, "made a commit in a sub without a rebase": { initial: `a=B|x=U:Cfoo#9-1;B9=9;Os I a=b`, - expected: `x=E:I s=Sa:Ns;Os Cfoo#Ns-1 a=b!H=Ns`, + expected: `x=E:Cfoo#M-2 s=Sa:Ns;Bmaster=M;Os Cfoo#Ns-1 a=b!H=Ns`, baseCommit: "9", message: "foo", }, @@ -326,6 +331,9 @@ Conflict in ${colors.red("s")} Object.keys(result.newCommits).forEach(name => { commitMap[result.newCommits[name]] = "N" + name; }); + if (null !== result.metaCommit) { + commitMap[result.metaCommit] = "M"; + } return { commitMap: commitMap, }; From d94b34a4b4fc70193f519fe3400769bf7e2cf74c Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 30 Mar 2018 15:23:10 -0400 Subject: [PATCH 082/402] Fast-forward where possible --- node/lib/util/submodule_rebase_util.js | 27 +++++++++++++++++++++-- node/test/util/submodule_rebase_util.js | 29 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index a447822d8..db67104b2 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -37,7 +37,8 @@ const NodeGit = require("nodegit"); const path = require("path"); const rimraf = require("rimraf"); -const DoWorkQueue = require("../util/do_work_queue"); +const GitUtil = require("./git_util"); +const DoWorkQueue = require("./do_work_queue"); const RebaseFileUtil = require("./rebase_file_util"); const RepoStatus = require("./repo_status"); const SubmoduleUtil = require("./submodule_util"); @@ -201,8 +202,30 @@ exports.rewriteCommits = co.wrap(function *(repo, branch, upstream) { if (null !== upstream) { assert.instanceOf(upstream, NodeGit.Commit); } - const head = yield repo.head(); + const headSha = head.target().tostrS(); + const branchSha = branch.id().tostrS(); + + // We can do a fast-forward if `branch` and its entire history should be + // included. This requires two things to be true: + // 1. `branch` is a descendant of `head` or equal to `head` + // 2. `null === upstream` (implying that all ancestors are to be included) + // or `upstream` is an ancestor of or equal to `head`. + + if (null === upstream || + upstream.id().tostrS() === headSha || + (yield NodeGit.Graph.descendantOf(repo, + headSha, + upstream.id().tostrS()))) { + if (yield NodeGit.Graph.descendantOf(repo, branchSha, headSha)) { + yield GitUtil.setHeadHard(repo, branch); + return { + commits: {}, + conflictedCommit: null, + }; + } + } + const ontoAnnotated = yield NodeGit.AnnotatedCommit.fromRef(repo, head); const branchAnnotated = yield NodeGit.AnnotatedCommit.lookup(repo, branch.id()); diff --git a/node/test/util/submodule_rebase_util.js b/node/test/util/submodule_rebase_util.js index cd1d56b7a..3445d8ea6 100644 --- a/node/test/util/submodule_rebase_util.js +++ b/node/test/util/submodule_rebase_util.js @@ -157,12 +157,41 @@ describe("rewriteCommits", function () { upstream: null, conflictedCommit: null, }, + "skip if branch and head same": { + initial: "x=S:Cr-1;Bmaster=r", + upstream: null, + conflictedCommit: null, + expected: "x=E:H=r", + }, "skip none": { initial: "x=S:C2-1;Cr-1;Bmaster=2;Br=r", expected: "x=E:Crr-2 r=r;H=rr", upstream: "1", conflictedCommit: null, }, + "ffwd when all included": { + initial: "x=S:Cr-3; C3-2; C2-1;Bmaster=2;Br=r", + expected: "x=E:H=r", + upstream: null, + conflictedCommit: null, + }, + "ffwd when enough included included": { + initial: "x=S:Cr-3; C3-2; C2-1;Bmaster=3;Br=r", + expected: "x=E:H=r", + upstream: "2", + conflictedCommit: null, + }, + "ffwd when enough included included, and equal": { + initial: "x=S:Cr-3; C3-2; C2-1;Bmaster=3;Br=r", + expected: "x=E:H=r", + upstream: "3", + conflictedCommit: null, + }, + "not ffwd when skipped commit": { + initial: "x=S:Cr-3; C3-2; C2-1;Bmaster=2;Br=r", + upstream: "3", + expected: "x=E:Crr-2 r=r;H=rr", + }, "conflict": { initial: "x=S:C2-1;Cr-1 2=3;Bmaster=2;Br=r", expected: `x=E:I *2=~*2*3;H=2;Edetached HEAD,r,2;W 2=\ From a1df33029439dcf1b94d79c1f7b76eca836510de Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 30 Mar 2018 15:23:43 -0400 Subject: [PATCH 083/402] Close auto-opened submodules with no new commits e.g., because they were fast-forwarded --- node/lib/util/cherry_pick_util.js | 34 ++++++++++++++++++++---------- node/test/util/cherry_pick_util.js | 17 +++++++++++---- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 7ef11048d..0e4e25582 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -77,9 +77,9 @@ function ensureCherryInProgress(seq) { * a `Submodule`, update it in the specified `index` in the specified `repo` * and if that submodule is open, reset its HEAD, index, and worktree to * reflect that commit. Otherwise, if it maps to `null`, remove it and deinit - * it if it's open. Obtain submodule repositories from the specified `opener`. - * The behavior is undefined if any referenced submodule is open and has index - * or workdir modifications. + * it if it's open. Obtain submodule repositories from the specified `opener`, + * but do not open any closed repositories. The behavior is undefined if any + * referenced submodule is open and has index or workdir modifications. * * @param {NodeGit.Repository} repo * @param {Open.Opener} opener @@ -318,8 +318,7 @@ exports.computeChanges = co.wrap(function *(repo, commit) { * Pick the specified `subs` in the specified `metaRepo` having the specified * `metaIndex`. Stage new submodule commits in `metaRepo`. Return an object * describing any commits that were generated and conflicted commits. Use the - * specified `opener` to open submodules. - * + * specified `opener` to acces submodule repos. * * @param {NodeGit.Repository} metaRepo * @param {Open.Opener} opener * @param {NodeGit.Index} metaIndex @@ -347,6 +346,7 @@ exports.pickSubs = co.wrap(function *(metaRepo, opener, metaIndex, subs) { ${colors.green(commitText)}.`); // Fetch the commit; it may not be present. + yield fetcher.fetchSha(repo, name, change.newSha); yield fetcher.fetchSha(repo, name, change.oldSha); const newCommit = yield repo.getCommit(change.newSha); @@ -398,16 +398,14 @@ Conflicting entries for submodule ${colors.red(name)} * * @param {NodeGit.Repository} repo * @param {NodeGit.Commit} commit - * @param {Open.Opener} opener * @return {Object} return * @return {String|null} return.newMetaCommit * @return {Object} returm.submoduleCommits * @return {String|null} return.errorMessage */ -exports.rewriteCommit = co.wrap(function *(repo, commit, opener) { +exports.rewriteCommit = co.wrap(function *(repo, commit) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); - assert.instanceOf(opener, Open.Opener); const hasUrlChanges = yield exports.containsUrlChanges(repo, commit); if (hasUrlChanges) { @@ -426,6 +424,7 @@ Please try with normal 'git cherry-pick'.`); // Perform simple changes that don't require picks -- addition, deletions, // and fast-forwards. + const opener = new Open.Opener(repo, null); yield exports.changeSubmodules(repo, opener, index, changes.simpleChanges); // Render any conflicts @@ -436,8 +435,22 @@ Please try with normal 'git cherry-pick'.`); // Then do the cherry-picks. const picks = yield exports.pickSubs(repo, opener, index, changes.changes); - const conflicts = picks.conflicts; + + // Close any subs that were unnecessarily opened (i.e., because no commit + // was generated). + + const closeSub = co.wrap(function *(path) { + const commits = picks.commits[path]; + if ((undefined === commits || 0 === Object.keys(commits).length) && + !(path in picks.conflicts)) { + console.log(`Closing ${colors.green(path)}`); + yield DeinitUtil.deinit(repo, path); + } + }); + const opened = Array.from(yield opener.getOpenedSubs()); + DoWorkQueue.doInParallel(opened, closeSub); + Object.keys(conflicts).sort().forEach(name => { errorMessage += `Submodule ${colors.red(name)} is conflicted.\n`; }); @@ -511,8 +524,7 @@ running cherry-pick.`); commits: [commit.id().tostrS()], }); yield SequencerStateUtil.writeSequencerState(metaRepo.path(), seq); - const opener = new Open.Opener(metaRepo, null); - const result = yield exports.rewriteCommit(metaRepo, commit, opener); + const result = yield exports.rewriteCommit(metaRepo, commit); if (null === result.errorMessage) { yield SequencerStateUtil.cleanSequencerState(metaRepo.path()); } diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index 035763cbd..994915c66 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -608,6 +608,18 @@ a=Ax:Cz-x;Bfoo=z| x=S:C8-2 s=Sa:z;C3-2;C2-1 s=Sa:x;Bfoo=8;Bmaster=3;Os H=x`, expected: "x=E:C9-3 s=Sa:z;Bmaster=9;Os", }, + "picking one non-trivial ffwd sub": { + input: ` +a=Ax:Cz-y;Cy-x;Bfoo=z| +x=S:C8-2 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=3;Os`, + expected: "x=E:C9-3 s=Sa:z;Bmaster=9;Os H=z", + }, + "picking one non-trivial ffwd sub, closes": { + input: ` +a=Ax:Cz-y;Cy-x;Bfoo=z| +x=S:C8-2 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=3`, + expected: "x=E:C9-3 s=Sa:z;Bmaster=9", + }, "picking one sub introducing two commits": { input: ` a=Aw:Cz-y;Cy-x;Cx-w;Bfoo=z| @@ -709,10 +721,7 @@ Conflicting entries for submodule ${colors.red("s")} assert.property(reverseCommitMap, "8"); const eightCommitSha = reverseCommitMap["8"]; const eightCommit = yield x.getCommit(eightCommitSha); - const opener = new Open.Opener(x, null); - const result = yield CherryPickUtil.rewriteCommit(x, - eightCommit, - opener); + const result = yield CherryPickUtil.rewriteCommit(x, eightCommit); assert.equal(result.errorMessage, c.errorMessage || null); return mapCommits(maps, result); }); From dd9f861b2630765793204478cfbd618eddb34c71 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Mon, 2 Apr 2018 13:51:43 -0400 Subject: [PATCH 084/402] Better commit mapping validation Check that all commits read have a mapping and that all mappings are valid. --- node/lib/util/repo_ast_test_util.js | 16 ++++++++- node/lib/util/repo_ast_util.js | 32 +++++++++++++---- node/lib/util/submodule_rebase_util.js | 19 +++++++--- node/lib/util/write_repo_ast_util.js | 43 +++++++++++++++------- node/test/util/merge_util.js | 2 +- node/test/util/repo_ast_test_util.js | 12 ++++++- node/test/util/repo_ast_util.js | 48 ++++++++++++++----------- node/test/util/submodule_rebase_util.js | 12 +++++++ node/test/util/synthetic-branch.js | 5 +-- node/test/util/write_repo_ast_util.js | 1 + 10 files changed, 138 insertions(+), 52 deletions(-) diff --git a/node/lib/util/repo_ast_test_util.js b/node/lib/util/repo_ast_test_util.js index 67ce5c5f6..3e4612c3b 100644 --- a/node/lib/util/repo_ast_test_util.js +++ b/node/lib/util/repo_ast_test_util.js @@ -282,9 +282,11 @@ exports.testMultiRepoManipulator = // Copy over and verify (that they are not duplicates) remapped commits and // urls output by the manipulator. + let manipulatorRemap = {}; if (undefined !== manipulated) { if ("commitMap" in manipulated) { + manipulatorRemap = manipulated.commitMap; assert.isObject(manipulated.commitMap, "manipulator must return object"); for (let commit in manipulated.commitMap) { @@ -292,7 +294,7 @@ exports.testMultiRepoManipulator = commitMap, commit, `commit already mapped to ${commitMap[commit]}.`); - const newVal = manipulated.commitMap[commit]; + const newVal = manipulatorRemap[commit]; commitMap[commit] = newVal; mappings.reverseCommitMap[newVal] = commit; } @@ -325,6 +327,9 @@ exports.testMultiRepoManipulator = // Read in the states of the repos. + const seen = new Set(); + function rememberCommit(sha) { seen.add(sha); } + let actualASTs = {}; for (let repoName in expectedASTs) { let repo; @@ -340,11 +345,20 @@ exports.testMultiRepoManipulator = repo = yield NodeGit.Repository.open(path); } const newAST = yield ReadRepoASTUtil.readRAST(repo); + const commits = RepoASTUtil.listCommits(newAST); + Object.keys(commits).forEach(rememberCommit); actualASTs[repoName] = RepoASTUtil.mapCommitsAndUrls(newAST, commitMap, urlMap); } + // Make sure we didn't get garbage in the remap set. + + for (let sha in manipulatorRemap) { + assert(seen.has(sha), + `Remap for unseen commit ${sha} to ${manipulatorRemap[sha]}`); + } + // Allow mapping of actual ASTs. actualASTs = options.actualTransformer(actualASTs, mappings); diff --git a/node/lib/util/repo_ast_util.js b/node/lib/util/repo_ast_util.js index 9b60b9487..0df3e1ef8 100644 --- a/node/lib/util/repo_ast_util.js +++ b/node/lib/util/repo_ast_util.js @@ -609,7 +609,8 @@ exports.assertEqualRepoMaps = function (actual, expected, message) { * except that each commit ID is replaced with the value that it maps to in the * specified `commitMap`. And each remote url that exists in the specified * `urlMap` is replaced by the value in that map. URLs and commits missing - * from the maps are kept as-is. + * from the maps are kept as-is. The behavior is undefined if any commits are + * unmapped. * * @param {RepoAST} ast * @param {Object} commitMap string to string @@ -622,10 +623,8 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { assert.isObject(urlMap); function mapCommitId(commitId) { - if (commitId in commitMap) { - return commitMap[commitId]; - } - return commitId; + assert.property(commitMap, commitId); + return commitMap[commitId]; } function mapSubmodule(submodule) { @@ -634,8 +633,8 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) { url = urlMap[url]; } let sha = submodule.sha; - if (null !== sha && (sha in commitMap)) { - sha = commitMap[sha]; + if (null !== sha) { + sha = mapCommitId(sha); } return new RepoAST.Submodule(url, sha); } @@ -861,3 +860,22 @@ exports.cloneRepo = function (original, url) { currentBranchName: original.currentBranchName, }); }; + +/** + * Return all commits in the specified `repo`. + * TODO: independent test + * + * @param {RepoAST} repo + * @return {Object} sha to `RepoAST.Commit` + */ +exports.listCommits = function (repo) { + assert.instanceOf(repo, RepoAST); + const commits = repo.commits; + + // Also, commits from open submodules. + + for (let subName in repo.openSubmodules) { + Object.assign(commits, repo.openSubmodules[subName].commits); + } + return commits; +}; diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index db67104b2..c93239413 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -206,6 +206,20 @@ exports.rewriteCommits = co.wrap(function *(repo, branch, upstream) { const headSha = head.target().tostrS(); const branchSha = branch.id().tostrS(); + const result = { + commits: {}, + conflictedCommit: null, + }; + + // If we're up-to-date with the commit to be rebased onto, return + // immediately. Detach head as this is the normal behavior. + + if (headSha === branchSha || + (yield NodeGit.Graph.descendantOf(repo, headSha, branchSha))) { + repo.detachHead(); + return result; // RETURN + } + // We can do a fast-forward if `branch` and its entire history should be // included. This requires two things to be true: // 1. `branch` is a descendant of `head` or equal to `head` @@ -219,10 +233,7 @@ exports.rewriteCommits = co.wrap(function *(repo, branch, upstream) { upstream.id().tostrS()))) { if (yield NodeGit.Graph.descendantOf(repo, branchSha, headSha)) { yield GitUtil.setHeadHard(repo, branch); - return { - commits: {}, - conflictedCommit: null, - }; + return result; // RETURN } } diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index 1912d382a..f6da92b9a 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -640,6 +640,21 @@ exports.writeRAST = co.wrap(function *(ast, path) { }; }); +/** + * Return all the `Commit` objects in the specified `repos`. + * + * @param {Object} name to `RepoAST` + * @return {Object} sha to `Commit` + */ +function listCommits(repos) { + const commits = {}; + for (let repoName in repos) { + const repo = repos[repoName]; + Object.assign(commits, RepoASTUtil.listCommits(repo)); + } + return commits; +} + /** * Write the repositories described in the specified `repos` map to a the * specified `rootDirectory`. Return a map from repo name to @@ -682,26 +697,28 @@ exports.writeMultiRAST = co.wrap(function *(repos, rootDirectory) { urlMap[repoPath] = repoName; } + // First, collect all the commits: + + let commits = listCommits(repos); + + // Make an id map so that we can rewrite just URLs + + const map = {}; + for (let sha in commits) { + map[sha] = sha; + } + // Now, rewrite all the repo ASTs to have the right urls. + for (let repoName in repos) { const repoAST = repos[repoName]; repos[repoName] = - RepoASTUtil.mapCommitsAndUrls(repoAST, {}, repoPaths); + RepoASTUtil.mapCommitsAndUrls(repoAST, map, repoPaths); } - // First, collect all the commits: - - let commits = {}; - for (let repoName in repos) { - const repo = repos[repoName]; - Object.assign(commits, repo.commits); - - // Also, commits from open submodules. + // Re-list commits now that URLs are updated. - for (let subName in repo.openSubmodules) { - Object.assign(commits, repo.openSubmodules[subName].commits); - } - } + commits = listCommits(repos); const commitRepoPath = yield TestUtil.makeTempDir(); const commitRepo = yield NodeGit.Repository.init(commitRepoPath, 0); diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index 55d7ebb2e..8714be8e0 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -49,7 +49,7 @@ function mapReturnedCommits(result, maps) { // record a mapping from the new commit to it's logical name: "x". const commitMap = maps.commitMap; - if (!(result.metaCommit in commitMap)) { + if (null !== result.metaCommit && !(result.metaCommit in commitMap)) { newCommitMap[result.metaCommit] = "x"; } diff --git a/node/test/util/repo_ast_test_util.js b/node/test/util/repo_ast_test_util.js index 65413235f..1723e0360 100644 --- a/node/test/util/repo_ast_test_util.js +++ b/node/test/util/repo_ast_test_util.js @@ -332,7 +332,17 @@ Bmaster=1 origin/master`, }; }, }, - } + }, + "bad remap": { + i: {}, + e: {}, + m: function () { + return Promise.resolve({ + commitMap: { "foo": "bar"}, + }); + }, + fails: true, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; diff --git a/node/test/util/repo_ast_util.js b/node/test/util/repo_ast_util.js index 258122b67..34594894d 100644 --- a/node/test/util/repo_ast_util.js +++ b/node/test/util/repo_ast_util.js @@ -691,6 +691,7 @@ describe("RepoAstUtil", function () { } }), m: {}, + fails: true, e: new RepoAST({ commits: { "1": c1 }, head: "1", @@ -805,6 +806,7 @@ describe("RepoAstUtil", function () { }, head: "2", }), + m: { "2": "2", "y": "y" }, u: { x: "z" }, e: new RepoAST({ commits: { @@ -824,7 +826,7 @@ describe("RepoAstUtil", function () { }, head: "2", }), - m: { "3": "4" }, + m: { "3": "4", "2": "2" }, e: new RepoAST({ commits: { "2": new RepoAST.Commit({ @@ -843,7 +845,7 @@ describe("RepoAstUtil", function () { }, head: "2", }), - m: { "3": "4" }, + m: { "3": "4", "2": "2" }, u: { x: "z" }, e: new RepoAST({ commits: { @@ -863,7 +865,7 @@ describe("RepoAstUtil", function () { }, head: "2", }), - m: { "8": "4" }, + m: { "2": "2", "3": "3" }, u: { r: "z" }, e: new RepoAST({ commits: { @@ -905,7 +907,7 @@ describe("RepoAstUtil", function () { baz: new RepoAST.Submodule("x", "y"), }, }), - m: { "1": "2"}, + m: { "1": "2", "y": "y" }, u: { "q": "z"}, e: new RepoAST({ commits: { "2": c1 }, @@ -999,7 +1001,7 @@ describe("RepoAstUtil", function () { } })}, }), - m: { "1": "2" }, + m: { "1": "2", "y": "y" }, u: { "x": "z" }, e: new RepoAST({ commits: { "2": c1 }, @@ -1027,19 +1029,6 @@ describe("RepoAstUtil", function () { rebase: new Rebase("foo", "2", "2"), }), }, - "rebase unmapped": { - i: new RepoAST({ - commits: { "1": c1 }, - head: "1", - rebase: new Rebase("foo", "1", "1"), - }), - m: {}, - e: new RepoAST({ - commits: { "1": c1 }, - head: "1", - rebase: new Rebase("foo", "1", "1"), - }), - }, "sequencer": { i: new RepoAST({ commits: { "1": c1 }, @@ -1073,9 +1062,26 @@ describe("RepoAstUtil", function () { const c = cases[caseName]; const commitMap = c.m || {}; const urlMap = c.u || {}; - const result = RepoASTUtil.mapCommitsAndUrls(c.i, - commitMap, - urlMap); + let exception; + let result; + const shouldFail = undefined !== c.fails && c.fails; + try { + result = RepoASTUtil.mapCommitsAndUrls(c.i, + commitMap, + urlMap); + } catch (e) { + exception = e; + } + + if (undefined === exception) { + assert(!shouldFail, "should have failed"); + } else { + if (!shouldFail) { + throw exception; + } else { + return; + } + } RepoASTUtil.assertEqualASTs(result, c.e); }); }); diff --git a/node/test/util/submodule_rebase_util.js b/node/test/util/submodule_rebase_util.js index 3445d8ea6..e8232ebfc 100644 --- a/node/test/util/submodule_rebase_util.js +++ b/node/test/util/submodule_rebase_util.js @@ -169,6 +169,18 @@ describe("rewriteCommits", function () { upstream: "1", conflictedCommit: null, }, + "up-to-date": { + initial: "x=S:Cr-1; C3-2; C2-r;Bmaster=3;Br=r", + expected: "x=E:H=3", + upstream: null, + conflictedCommit: null, + }, + "up-to-date with upstream": { + initial: "x=S:Cr-1; C3-2; C2-r;Bmaster=3;Br=r", + expected: "x=E:H=3", + upstream: "1", + conflictedCommit: null, + }, "ffwd when all included": { initial: "x=S:Cr-3; C3-2; C2-1;Bmaster=2;Br=r", expected: "x=E:H=r", diff --git a/node/test/util/synthetic-branch.js b/node/test/util/synthetic-branch.js index 5a73c412b..f67712300 100644 --- a/node/test/util/synthetic-branch.js +++ b/node/test/util/synthetic-branch.js @@ -41,7 +41,7 @@ const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); const TestUtil = require("../../lib/util/test_util"); describe("synthetic-branch", function () { - const syntheticBranch = co.wrap(function *(repos, maps) { + const syntheticBranch = co.wrap(function *(repos) { const x = repos.x; const config = yield x.config(); yield config.setString("gitmeta.subreporootpath", "../../"); @@ -63,9 +63,6 @@ describe("synthetic-branch", function () { if (!pass) { throw new UserError("fail"); } - return { - commitMap: maps, - }; }); const cases = { diff --git a/node/test/util/write_repo_ast_util.js b/node/test/util/write_repo_ast_util.js index fb87fea91..559b8d006 100644 --- a/node/test/util/write_repo_ast_util.js +++ b/node/test/util/write_repo_ast_util.js @@ -401,6 +401,7 @@ S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q;H=3", "a=B:Bfoo/bar=1|x=Ca:Bfoo/bar=1 origin/foo/bar", "open submodule conflict": "a=B|x=U:I *README.md=aa*S:1*cc;W README.md=yyy;Os", + "open sub with commit new to sub": "a=B|x=U:Os Cfoo-1!H=foo", }; Object.keys(cases).forEach(caseName => { const input = cases[caseName]; From d95f6591f62395712f2d5099be5904bacf038c2c Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 3 Apr 2018 09:22:13 -0400 Subject: [PATCH 085/402] Ditch `upstream` if would rewrite too many commits When you specify an upstream to libgit2, it attempts to rewrite all commits between your current commit and `upstream`, even those in `onto`. It fails with these commits in an unusual way, not by giving an error in the rebsase operation, but when you try to make a rebase commit (probably because the commit would be empty) it doesn't actually succeed, but also doesn't generte an error; instead, it fails and returns a garbage commit sha (probably the failure to throw an error is a flaw in nodegit). I only detected this situation after tightening up the test harness, so that these garbage commit shas are flagged. They may be harmless, but it seems wrong (and I could see the attempt to rewrite these commits creatng conflicts or non-empty commits), so I'm changing the logic to ignore upstream when it's equal to or an ancestor of HEAD. --- node/lib/util/submodule_rebase_util.js | 20 ++++++++++++++------ node/test/util/submodule_rebase_util.js | 6 ++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index c93239413..b7afb2d20 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -205,6 +205,7 @@ exports.rewriteCommits = co.wrap(function *(repo, branch, upstream) { const head = yield repo.head(); const headSha = head.target().tostrS(); const branchSha = branch.id().tostrS(); + const upstreamSha = (upstream && upstream.id().tostrS()) || null; const result = { commits: {}, @@ -220,17 +221,24 @@ exports.rewriteCommits = co.wrap(function *(repo, branch, upstream) { return result; // RETURN } + // If the upstream is non-null, but is an ancestor of HEAD or equal to it, + // libgit2 will try to rewrite commits that should not be rewritten and + // fail. In this case, we set upstream to null, indicating at all commits + // should be included (as they should). + + if (null !== upstream) { + if (upstreamSha === headSha || + (yield NodeGit.Graph.descendantOf(repo, headSha, upstreamSha))) { + upstream = null; + } + } + // We can do a fast-forward if `branch` and its entire history should be // included. This requires two things to be true: // 1. `branch` is a descendant of `head` or equal to `head` // 2. `null === upstream` (implying that all ancestors are to be included) - // or `upstream` is an ancestor of or equal to `head`. - if (null === upstream || - upstream.id().tostrS() === headSha || - (yield NodeGit.Graph.descendantOf(repo, - headSha, - upstream.id().tostrS()))) { + if (null === upstream) { if (yield NodeGit.Graph.descendantOf(repo, branchSha, headSha)) { yield GitUtil.setHeadHard(repo, branch); return result; // RETURN diff --git a/node/test/util/submodule_rebase_util.js b/node/test/util/submodule_rebase_util.js index e8232ebfc..2dc9c261a 100644 --- a/node/test/util/submodule_rebase_util.js +++ b/node/test/util/submodule_rebase_util.js @@ -229,6 +229,12 @@ describe("rewriteCommits", function () { upstream: "3", conflictedCommit: null, }, + "excessive upstream": { + initial: "x=S:C2-1;C3-2;Cr-2;Bmaster=3;Br=r", + expected: "x=E:Crr-3 r=r;H=rr", + upstream: "1", + conflictedCommit: null, + }, }; Object.keys(cases).forEach(caseName => { it(caseName, co.wrap(function *() { From b77655859280e5f80084a59839e03c0796046810 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 5 Apr 2018 19:11:16 -0400 Subject: [PATCH 086/402] Change rebase to use sequencer Addresses https://github.com/twosigma/git-meta/projects/2#card-8117622 --- node/lib/cmd/rebase.js | 10 +- node/lib/util/rebase_util.js | 611 +++++++++++----------------------- node/test/util/rebase_util.js | 554 ++++++++++++++++++------------ 3 files changed, 545 insertions(+), 630 deletions(-) diff --git a/node/lib/cmd/rebase.js b/node/lib/cmd/rebase.js index 27d252543..f1b822d01 100644 --- a/node/lib/cmd/rebase.js +++ b/node/lib/cmd/rebase.js @@ -96,7 +96,10 @@ exports.executeableSubcommand = co.wrap(function *(args) { yield RebaseUtil.abort(repo); } else if (args.continue) { - yield RebaseUtil.continue(repo); + const result = RebaseUtil.continue(repo); + if (null !== result.errorMessage) { + throw new UserError(result.errorMessage); + } } else { if (null === args.commit) { @@ -108,6 +111,9 @@ exports.executeableSubcommand = co.wrap(function *(args) { `Could not resolve ${colors.red(args.commit)} to a commit.`); } const commit = yield repo.getCommit(committish.id()); - yield RebaseUtil.rebase(repo, commit); + const result = yield RebaseUtil.rebase(repo, commit); + if (null !== result.errorMessage) { + throw new UserError(result.errorMessage); + } } }); diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index a930fbff9..f775eb66e 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -35,435 +35,192 @@ const co = require("co"); const colors = require("colors"); const NodeGit = require("nodegit"); -const DeinitUtil = require("./deinit_util"); -const DoWorkQueue = require("../util/do_work_queue"); -const Open = require("./open"); +const Checkout = require("./checkout"); +const CherryPickUtil = require("./cherry_pick_util"); +const DoWorkQueue = require("./do_work_queue"); +const Reset = require("./reset"); const GitUtil = require("./git_util"); -const Hook = require("../util/hook"); -const RepoStatus = require("./repo_status"); -const RebaseFileUtil = require("./rebase_file_util"); +const Hook = require("./hook"); +const SequencerState = require("./sequencer_state"); +const SequencerStateUtil = require("./sequencer_state_util"); const StatusUtil = require("./status_util"); -const SubmoduleConfigUtil = require("./submodule_config_util"); const SubmoduleRebaseUtil = require("./submodule_rebase_util"); const SubmoduleUtil = require("./submodule_util"); const UserError = require("./user_error"); -/** - * Put the head of the specified `repo` on the specified `commitSha`. - */ -const setHead = co.wrap(function *(repo, commitSha) { - const commit = yield repo.getCommit(commitSha); - yield GitUtil.setHeadHard(repo, commit); -}); - -/** - * Return an object indicating the commits that were created during rebasing - * and/or an error message indicating that the rebase was stopped due to a - * conflict. - * - * @param {Open.Opener} opener - * @param {String} name of the submodule - * @param {String} from commit rebasing from - * @param {String} onto commit rebasing onto - * @return {Object} - * @return {Object} return.commits map from original to created commit - * @return {Strring|null} return.conflictedCommit sha of conflicted commit - */ -const rebaseSubmodule = co.wrap(function *(opener, name, from, onto) { - const fetcher = yield opener.fetcher(); - const repo = yield opener.getSubrepo(name); - yield fetcher.fetchSha(repo, name, from); - yield fetcher.fetchSha(repo, name, onto); - const fromCommit = yield repo.getCommit(from); - yield NodeGit.Reset.reset(repo, fromCommit, NodeGit.Reset.TYPE.HARD); - const head = yield repo.head(); - const fromAnnotated = yield NodeGit.AnnotatedCommit.fromRef(repo, head); - const ontoCommitId = NodeGit.Oid.fromString(onto); - const ontoAnnotated = - yield NodeGit.AnnotatedCommit.lookup(repo, ontoCommitId); - const rebase = yield NodeGit.Rebase.init(repo, - fromAnnotated, - ontoAnnotated, - null, - null); - console.log(`Submodule ${colors.blue(name)}: starting \ -rebase; rewinding to ${colors.green(ontoCommitId.tostrS())}.`); - const op = yield SubmoduleRebaseUtil.callNext(rebase); - return yield SubmoduleRebaseUtil.processRebase(repo, rebase, op); -}); +const CommitAndRef = SequencerState.CommitAndRef; /** - * Process the specified `entry` from the specified `index` for the specified - * `metaRepo` during a rebase from the specified `fromCommit` on the specified - * `ontoCommit`. Use the specified `opener` to open submodules as needed. - * Return an object indicating that an error occurred, that a submodule needs - * to be rebased, or neither. + * Accumulate specfied `intermediate` result into `result`, gathering new and + * rewritten submodule commits generated from a single rebase operation. * - * @return {Object} - * @return {String|null} return.error - * @return {String|null} return.subToRebase - * @return {String|undefined} return.rebaseFrom only if `subToRebase !== null` + * @param {Object} result + * @param {Object} result.submoduleCommits path to map from sha to sha + * @param {Object} intermediate + * @param {Object} intermediate.submoduleCommits path to map from sha to sha */ -const processMetaRebaseEntry = co.wrap(function *(metaRepo, - index, - entry, - opener, - fromCommit, - ontoCommit) { - - const id = entry.id; - const isSubmodule = entry.mode === NodeGit.TreeEntry.FILEMODE.COMMIT; - const fetcher = yield opener.fetcher(); - const stage = NodeGit.Index.entryStage(entry); - - const result = { - error: null, - subToRebase: null, - }; - - switch (stage) { - case RepoStatus.STAGE.NORMAL: - // If this is an unchanged, visible sub, make sure its sha is in the - // right place in case it was ffwded. - - const open = yield opener.isOpen(entry.path); - if (open) { - const name = entry.path; - const fromSha = id.tostrS(); - const subRepo = yield opener.getSubrepo(name); - const subHead = yield subRepo.getHeadCommit(); - if (subHead.id().tostrS() !== fromSha) { - yield fetcher.fetchSha(subRepo, name, fromSha); - yield setHead(subRepo, fromSha); - } - } - break; - case RepoStatus.STAGE.OURS: - if (isSubmodule) { - result.subToRebase = entry.path; - result.rebaseFrom = id.tostrS(); - } - else { - if (SubmoduleConfigUtil.modulesFileName === entry.path) { - const succeeded = yield SubmoduleUtil.mergeModulesFile( - metaRepo, - fromCommit, - ontoCommit); - if (succeeded) { - yield index.addByPath(SubmoduleConfigUtil.modulesFileName); - break; // BREAK - } - } - result.error = - `There is a conflict in ${colors.red(entry.path)}.\n`; - } - break; +function accumulateRebaseResult(result, intermediate) { + assert.isObject(result); + assert.isObject(intermediate); + + for (let name in intermediate.submoduleCommits) { + const commits = Object.assign(result.submoduleCommits[name] || {}, + intermediate.submoduleCommits[name]); + result.submoduleCommits[name] = commits; } - return result; -}); + result.errorMessage = intermediate.errorMessage; +} /** - * Close the submodules opened by the specified `opener` that have no entry in - * the specified `subCommits` map or the specified `conflicted` set. + * If the specified `seq` has a non-null ref that is a branch, make it the + * current branch in the specified `repo`. * - * @param {Open.Opener} opener - * @param {Object} subCommits sub name to commit map - * @param {Set} conflicted name of subs with conflicts. + * @param {NodeGit.Repository} repo + * @param {SequencerState} seq */ -const closeAutoOpenedSubmodules = co.wrap(function *(opener, - subCommits, - conflicted) { - const repo = opener.repo; - const opened = yield opener.getOpenedSubs(); - yield opened.map(co.wrap(function *(name) { - const commits = subCommits[name]; - if ((undefined === commits || 0 === Object.keys(commits).length) && - !conflicted.has(name)) { - console.log(`Closing ${colors.green(name)} -- no commit created.`); - yield DeinitUtil.deinit(repo, name); +const restoreHeadBranch = co.wrap(function *(repo, seq) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(seq, SequencerState); + + const originalRefName = seq.originalHead.ref; + if (null !== originalRefName) { + const ref = yield NodeGit.Reference.lookup(repo, originalRefName); + if (ref.isBranch()) { + const head = yield repo.getHeadCommit(); + yield ref.setTarget(head, "git-meta rebase"); + yield repo.setHead(originalRefName); } - })); + } }); /** - * Process the specified `op` for the specified `rebase` in the specified - * `metaRepo` that maps to the specified `ontoCommit`. Load the generated - * commits into the specified `result`. - * - * @param {NodeGit.Repository} metaRepo - * @param {NodeGit.Commit} ontoCommit - * @param {NodeGit.Rebase} rebase - * @param {NodeGit.RebaseOperation} op - * @param {Object} result - * @param {Object} result.submoduleCommits - * @param {Object} result.metaCommits + * Throw a `UserError` unlessn the specified `seq` is non-null and has type + * `REBASE`. + * @param {SequencerState} seq */ -const processMetaRebaseOp = co.wrap(function *(metaRepo, - ontoCommit, - rebase, - op, - result) { - // We're going to loop over the entries of the index for the rebase - // operation. We have several tasks (I'll repeat later): - // - // 1. Stage "normal", un-conflicted, non-submodule changes. This - // process requires that we set the submodule to the correct commit. - // 2. When a conflict is detected in a submodule, call `init` on the - // rebaser for that submodule. - // 3. Pass any change in a submodule off to the appropriate submodule - // rebaser. - - const fromCommit = yield metaRepo.getCommit(op.id()); - const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit( - metaRepo, - fromCommit); - const names = Object.keys(urls); - const ontoShas = yield SubmoduleUtil.getSubmoduleShasForCommit( - metaRepo, - names, - fromCommit); - const opener = new Open.Opener(metaRepo, fromCommit); - - const index = yield metaRepo.index(); - const subsToRebase = {}; // map from name to sha to rebase onto - - let errorMessage = ""; - - const entries = index.entries(); - for (let i = 0; i < entries.length; ++i) { - const ret = yield processMetaRebaseEntry(metaRepo, - index, - entries[i], - opener, - fromCommit, - ontoCommit); - if (null !== ret.error) { - errorMessage += ret.error + "\n"; - } - else if (null !== ret.subToRebase) { - subsToRebase[ret.subToRebase] = ret.rebaseFrom; - } - } - - // Clean up conflicts unless we found one in the meta-repo that was not - // a submodule change. - - if ("" === errorMessage) { - yield index.conflictCleanup(); - } - - const conflicted = new Set(); - - // Process submodule rebases. - - for (let name in subsToRebase) { - const from = subsToRebase[name]; - const ret = yield rebaseSubmodule(opener, - name, - ontoShas[name], - from); - yield index.addByPath(name); - if (name in result.submoduleCommits) { - Object.assign(result.submoduleCommits[name], ret.commits); - } - else { - result.submoduleCommits[name] = ret.commits; - } - - if (null !== ret.conflictedCommit) { - errorMessage += - SubmoduleRebaseUtil.subConflictErrorMessage(name) + "\n"; - conflicted.add(name); - } +function ensureRebaseInProgress(seq) { + if (null !== seq) { + assert.instanceOf(seq, SequencerState); } - yield closeAutoOpenedSubmodules(opener, - result.submoduleCommits, - conflicted); - - if ("" !== errorMessage) { - throw new UserError(errorMessage); - } - - // Write the index and new commit, recording a mapping from the - // original commit ID to the new one. - - yield index.write(); - const newCommit = yield SubmoduleUtil.cacheSubmodules(metaRepo, () => { - const signature = metaRepo.defaultSignature(); - const commit = rebase.commit(null, signature, null); - return Promise.resolve(commit); - }); - const newCommitSha = newCommit.tostrS(); - const originalSha = op.id().tostrS(); - if (originalSha !== newCommitSha) { - result.metaCommits[newCommitSha] = originalSha; + if (null === seq || SequencerState.TYPE.REBASE !== seq.type) { + throw new UserError("Error: no rebase in progress"); } -}); + return seq; +} /** - * Drive a rebase operation in the specified `metaRepo` from the specified - * `fromCommit` to the specified `ontoCommit`. Call the specified - * `initializer` to set up the rebase initially. - * - * Essentially, this function factors out the core rebase logic to be shared - * between normal rebase and continued rebases. + * Apply the remaining rebase operations described in the specified `seq` to + * the specified `repo`. Return an object describing any created commits. + * Before applying a commit, record a sequencer representing the current state. * - * @param {NodeGit.Repository} metaRepo - * @param {(openSubs, getSubmoduleRebaser) => NodeGit.Rebase} initializer - * @param {NodeGit.Commit} fromCommit - * @param {NodeGit.Commit} ontoCommit + * @param {NodeGit.Repository} repo + * @param {SequencerState} seq + * @return {Object} [return] + * @return {Object} return.metaCommits maps from new to rebased commits + * @return {Object} return.submoduleCommits maps from submodule name to + * a map from new to rebased commits + * @return {String|null} return.errorMessage */ -const driveRebase = co.wrap(function *(metaRepo, - initializer, - fromCommit, - ontoCommit) { - assert.instanceOf(metaRepo, NodeGit.Repository); - assert.isFunction(initializer); - assert.instanceOf(fromCommit, NodeGit.Commit); - assert.instanceOf(ontoCommit, NodeGit.Commit); - - const init = yield initializer(metaRepo); - const rebase = init.rebase; +exports.runRebase = co.wrap(function *(repo, seq) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(seq, SequencerState); const result = { metaCommits: {}, - submoduleCommits: init.submoduleCommits, + submoduleCommits: {}, + errorMessage: null, }; - - // Now, iterate over the rebase commits. We pull the operation out into a - // separate function to avoid problems associated with creating functions - // in loops. - - let idx = rebase.operationCurrent(); - const total = rebase.operationEntrycount(); - function makeCallNext() { - return SubmoduleRebaseUtil.callNext(rebase); - } - while (idx < total) { - const rebaseOper = rebase.operationByIndex(idx); - console.log(`Applying ${colors.green(rebaseOper.id().tostrS())}.`); - yield processMetaRebaseOp(metaRepo, - ontoCommit, - rebase, - rebaseOper, - result); - yield SubmoduleUtil.cacheSubmodules(metaRepo, makeCallNext); - ++idx; - } - - // If this was a fast-forward rebase, we need to set the heads of the - // submodules correctly. - - const wasFF = yield NodeGit.Graph.descendantOf(metaRepo, - ontoCommit.id(), - fromCommit.id()); - if (wasFF) { - const opener = new Open.Opener(metaRepo, ontoCommit); - const fetcher = yield opener.fetcher(); - const openSubs = Array.from(yield opener.getOpenSubs()); - const shas = yield SubmoduleUtil.getSubmoduleShasForCommit(metaRepo, - openSubs, - ontoCommit); - const fetchOpened = co.wrap(function *(name) { - const subRepo = yield opener.getSubrepo(name); - const head = yield subRepo.head(); - const sha = shas[name]; - if (head.target().tostrS() !== sha) { - yield fetcher.fetchSha(subRepo, name, sha); - yield setHead(subRepo, sha); - } - }); - yield DoWorkQueue.doInParallel(openSubs, fetchOpened); + for (let i = seq.currentCommit; i !== seq.commits.length; ++i) { + const nextSeq = seq.copy({ currentCommit: i, }); + yield SequencerStateUtil.writeSequencerState(repo.path(), nextSeq); + const sha = nextSeq.commits[i]; + const commit = yield repo.getCommit(sha); + SubmoduleRebaseUtil.logCommit(commit); + const cherryResult = yield CherryPickUtil.rewriteCommit(repo, commit); + if (null !== cherryResult.newMetaCommit) { + result.metaCommits[cherryResult.newMetaCommit] = sha; + } + accumulateRebaseResult(result, cherryResult); + if (null !== result.errorMessage) { + return result; + } } - yield SubmoduleRebaseUtil.callFinish(metaRepo, rebase); + yield restoreHeadBranch(repo, seq); + yield SequencerStateUtil.cleanSequencerState(repo.path()); + console.log("Finished rebase."); + yield Hook.execHook("post-rewrite", ["rebase"]); return result; }); /** - * Rebase the current branch onto the specified `commit` in the specified - * `metaRepo` having the specified `status`. The behavior is undefined unless - * the `metaRepo` is in a consistent state according to - * `Status.ensureCleanAndConsistent`. Return an object describing generated - * commits. + * Rebase the current branch onto the specified `onto` commit in the specified + * `repo` having the specified `status`. Throw a `UserError` if the rebase + * cannot proceed due to unclean state or because another operation is in + * progress. * * @async - * @param {NodeGit.Repository} metaRepo - * @param {NodeGit.Commit} commit + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} onto * @return {Object} [return] * @return {Object} return.metaCommits maps from new to rebased commits * @return {Object} return.submoduleCommits maps from submodule name to * a map from new to rebased commits */ -exports.rebase = co.wrap(function *(metaRepo, commit) { - assert.instanceOf(metaRepo, NodeGit.Repository); - assert.instanceOf(commit, NodeGit.Commit); - // It's important to note that we will be rebasing the sub-repos on top of - // commits identified in the meta-repo, not those from their upstream - // branches. Main steps: - - // 1. Check to see if meta-repo is up-to-date; if it is we can exit. - // 2. Start the rebase operation in the meta-repo. - // 3. When we encounter a conflict with a submodules, this indicates that - // we need to perform a rebase on that submodule as well. This - // operation is complicated by the need to sync meta-repo commits with - // commits to the submodules, which may or may not be one-to-one. - - let currentBranchName = "HEAD"; - try { - const currentBranch = yield metaRepo.getCurrentBranch(); - currentBranchName = currentBranch.name(); - } - catch (e) { +exports.rebase = co.wrap(function *(repo, onto) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(onto, NodeGit.Commit); + + // First, make sure we're in a state in which we can run a rebase. + + const status = yield StatusUtil.getRepoStatus(repo); + StatusUtil.ensureReady(status); + if (!status.isDeepClean(false)) { + throw new UserError(`\ +The repository has uncommitted changes. Please stash or commit them before +running rebase.`); } - const currentBranch = yield metaRepo.getBranch(currentBranchName); - const fromCommitId = currentBranch.target(); - const fromCommit = yield metaRepo.getCommit(fromCommitId); - const commitId = commit.id(); + const result = { + metaCommits: {}, + submoduleCommits: {}, + }; + + const headCommit = yield repo.getHeadCommit(); + const headRef = yield repo.head(); + const headSha = headCommit.id().tostrS(); + const ontoSha = onto.id().tostrS(); // First, see if 'commit' already exists in the current history. If so, we // can exit immediately. - if (yield GitUtil.isUpToDate(metaRepo, - fromCommitId.tostrS(), - commitId.tostrS())) { - console.log(`${colors.green(currentBranch.shorthand())} is \ -up-to-date.`); - return { - metaCommits: {}, - submoduleCommits: {}, - }; + if (yield GitUtil.isUpToDate(repo, headSha, ontoSha)) { + const name = headRef.shorthand(); + console.log(`${colors.green(name)} is up-to-date.`); + return result; // RETURN } - const initialize = co.wrap(function *() { - const fromAnnotedCommit = - yield NodeGit.AnnotatedCommit.fromRef(metaRepo, currentBranch); - const ontoAnnotatedCommit = - yield NodeGit.AnnotatedCommit.lookup(metaRepo, commitId); - return yield SubmoduleUtil.cacheSubmodules(metaRepo, - co.wrap(function *() { - const rebase = yield NodeGit.Rebase.init(metaRepo, - fromAnnotedCommit, - ontoAnnotatedCommit, - null, - null); - console.log(`Rewinding to ${colors.green(commitId.tostrS())}.`); - yield SubmoduleRebaseUtil.callNext(rebase); - return { - rebase: rebase, - submoduleCommits: {}, - }; - })); - }); - const result = yield driveRebase(metaRepo, initialize, fromCommit, commit); + const canFF = yield NodeGit.Graph.descendantOf(repo, ontoSha, headSha); + if (canFF) { + yield Reset.reset(repo, onto, Reset.TYPE.HARD); + console.log(`Fast-forwarded to ${GitUtil.shortSha(ontoSha)}`); + return result; // RETURN + } - // Run post-rewrite hook with "rebase" as args, means rebase command - // invoked this hook. + console.log("First, rewinding head to replay your work on top of it..."); - console.log("Finished rebase."); - yield Hook.execHook("post-rewrite", ["rebase"]); - return result; + yield Checkout.checkoutCommit(repo, onto, true); + + const commits = yield exports.listRebaseCommits(repo, headCommit, onto); + const headName = headRef.isBranch() ? headRef.name() : null; + const seq = new SequencerState({ + type: SequencerState.TYPE.REBASE, + originalHead: new CommitAndRef(headSha, headName), + target: new CommitAndRef(ontoSha, null), + currentCommit: 0, + commits, + }); + return yield exports.runRebase(repo, seq); }); /** @@ -477,13 +234,8 @@ up-to-date.`); exports.abort = co.wrap(function *(repo) { assert.instanceOf(repo, NodeGit.Repository); - yield SubmoduleUtil.cacheSubmodules(repo, co.wrap(function*() { - const rebase = yield NodeGit.Rebase.open(repo); - rebase.abort(); - })); - - const head = yield repo.head(); - console.log(`Set HEAD back to ${colors.green(head.target().tostrS())}.`); + const seq = yield SequencerStateUtil.readSequencerState(repo.path()); + ensureRebaseInProgress(seq); // This is a little "heavy-handed'. TODO: abort active rebases in only // those open submodueles whose rebases are associated with the one in the @@ -491,68 +243,83 @@ exports.abort = co.wrap(function *(repo) { // independent rebase going in an open submodules. const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo); - yield openSubs.map(co.wrap(function *(name) { + const abortSub = co.wrap(function *(name) { const subRepo = yield SubmoduleUtil.getRepo(repo, name); if (!subRepo.isRebasing()) { return; // RETURN } - const rebaseInfo = yield RebaseFileUtil.readRebase(subRepo.path()); const subRebase = yield NodeGit.Rebase.open(subRepo); subRebase.abort(); - console.log(`Submodule ${colors.blue(name)}: reset to \ -${colors.green(rebaseInfo.originalHead)}.`); - })); + }); + + yield DoWorkQueue.doInParallel(openSubs, abortSub); + + const commit = yield repo.getCommit(seq.originalHead.sha); + yield Reset.reset(repo, commit, Reset.TYPE.HARD); + yield restoreHeadBranch(repo, seq); + yield SequencerStateUtil.cleanSequencerState(repo.path()); }); /** - * Continue the rebase in progress on the specified `repo`. + * Continue the rebase in progress on the specified `repo`. Return an object + * describng the commits that were created an an error message if the operation + * could not be completed. * * @param {NodeGit.Repository} repo + * @return {Object} [return] + * @return {Object} return.metaCommits maps from new to rebased commits + * @return {Object} return.submoduleCommits maps from submodule name to + * a map from new to rebased commits + * @return {Object} return.newCommits commits made in non-rebasing + * submodules, path to sha */ exports.continue = co.wrap(function *(repo) { assert.instanceOf(repo, NodeGit.Repository); const status = yield StatusUtil.getRepoStatus(repo); - - const rebaseInfo = status.rebase; - if (null === rebaseInfo) { - throw new UserError("Error: no rebase in progress"); - } + const seq = status.sequencerState; + ensureRebaseInProgress(seq); if (status.isConflicted()) { throw new UserError("Cannot continue rebase due to conflicts."); } - const fromCommit = yield repo.getCommit(rebaseInfo.originalHead); - const ontoCommit = yield repo.getCommit(rebaseInfo.onto); + const currentSha = seq.commits[seq.currentCommit]; + const currentCommit = yield repo.getCommit(currentSha); + const index = yield repo.index(); - const initializer = co.wrap(function *(repo) { - console.log(`Continuing rebase from \ -${colors.green(rebaseInfo.originalHead)} onto \ -${colors.green(rebaseInfo.onto)}.`); - const rebase = yield SubmoduleUtil.cacheSubmodules(repo, () => { - return NodeGit.Rebase.open(repo); - }); - const index = yield repo.index(); - const idx = rebase.operationCurrent(); - const op = rebase.operationByIndex(idx); - const baseCommit = yield repo.getCommit(op.id()); - const result = yield SubmoduleRebaseUtil.continueSubmodules( - repo, - index, - status, - baseCommit); - if (null !== result.errorMessage) { - throw new UserError(result.errorMessage); - } - return { - rebase: rebase, - submoduleCommits: result.commits, - }; + // First, continue in-progress rebases in the submodules and generate a + // commit for the curren operation. + + const continueResult = yield SubmoduleRebaseUtil.continueSubmodules( + repo, + index, + status, + currentCommit); + const result = { + metaCommits: {}, + newCommits: continueResult.newCommits, + submoduleCommits: continueResult.commits, + errorMessage: continueResult.errorMessage, + }; + if (null !== continueResult.metaCommit) { + result.metaCommits[continueResult.metaCommit] = + seq.commits[seq.currentCommit]; + } + if (null !== result.errorMessage) { + // Stop if there was a problem finishing the current operation. + + return result; // RETURN + } + + // Then, call back to `runRebase` to complete any remaining commits. + + const nextSeq = seq.copy({ + currentCommit: seq.currentCommit + 1, }); - const result = - yield driveRebase(repo, initializer, fromCommit, ontoCommit); - console.log("Finished rebase."); + const nextResult = yield exports.runRebase(repo, nextSeq); + Object.assign(result.metaCommits, nextResult.metaCommits); + accumulateRebaseResult(result, nextResult); return result; }); diff --git a/node/test/util/rebase_util.js b/node/test/util/rebase_util.js index 180593d21..000d0bcce 100644 --- a/node/test/util/rebase_util.js +++ b/node/test/util/rebase_util.js @@ -32,15 +32,19 @@ const assert = require("chai").assert; const co = require("co"); +const colors = require("colors"); -const RebaseUtil = require("../../lib/util/rebase_util"); -const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); -const SubmoduleUtil = require("../../lib/util/submodule_util"); +const RebaseUtil = require("../../lib/util/rebase_util"); +const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const SequencerState = require("../../lib/util/sequencer_state"); +const SequencerStateUtil = require("../../lib/util/sequencer_state_util"); + +const CommitAndRef = SequencerState.CommitAndRef; +const REBASE = SequencerState.TYPE.REBASE; function makeRebaser(operation) { return co.wrap(function *(repos, maps) { const result = yield operation(repos, maps); - // Now build a map from the newly generated commits to the // logical names that will be used in the expected case. @@ -52,274 +56,394 @@ function makeRebaser(operation) { RepoASTTestUtil.mapSubCommits(commitMap, result.submoduleCommits, maps.commitMap); + const newCommits = result.newCommits || {}; + for (let path in newCommits) { + commitMap[newCommits[path]] = `N${path}`; + } return { commitMap: commitMap, }; }); } -describe("rebase", function () { -describe("rebase", function () { - - // Will append the leter 'M' to any created meta-repo commits, and the - // submodule name to commits created in respective submodules. - - - function rebaser(repoName, commit) { - const rebaseOper = co.wrap(function *(repos, maps) { - assert.property(repos, repoName); - const repo = repos[repoName]; - const reverseCommitMap = maps.reverseCommitMap; - assert.property(reverseCommitMap, commit); - const originalActualCommit = reverseCommitMap[commit]; - const originalCommit = - yield repo.getCommit(originalActualCommit); +describe("Rebase", function () { +describe("runRebase", function () { + const cases = { + "end": { + state: "x=S", + seq: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("1", null), + target: new CommitAndRef("1", null), + currentCommit: 0, + commits: [], + }), + }, + "one, started detached": { + state: ` +a=B:Cf-1;Cg-1;Bf=f;Bg=g|x=U:C3-2 s=Sa:f;C4-2 s=Sa:g;H=4;Bfoo=3`, + seq: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("3", null), + target: new CommitAndRef("4", null), + currentCommit: 0, + commits: ["3"], + }), + expected: "x=E:C3M-4 s=Sa:fs;H=3M;Os Cfs-g f=f!H=fs", + }, + "one, started on a branch": { + state: ` +a=B:Cf-1;Cg-1;Bf=f;Bg=g|x=U:C3-2 s=Sa:f;C4-2 s=Sa:g;H=4;Bfoo=3;Bold=3`, + seq: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("3", "refs/heads/foo"), + target: new CommitAndRef("4", null), + currentCommit: 0, + commits: ["3"], + }), + expected: "x=E:C3M-4 s=Sa:fs;*=foo;Bfoo=3M;Os Cfs-g f=f!H=fs", + }, + "sub can be ffwded": { + state: ` +a=B:Cf-1;Bf=f|x=U:C3-2 s=Sa:f;C4-2 t=Sa:f;H=4;Bfoo=3`, + seq: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("3", null), + target: new CommitAndRef("4", null), + currentCommit: 0, + commits: ["3"], + }), + expected: "x=E:C3M-4 s=Sa:f;H=3M", + }, + "two commits": { + state: ` +a=B|x=S:C2-1;Bmaster=2;Cf-1 s=Sa:1;Cg-1 t=Sa:1;Bf=f;Bg=g`, + seq: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("2", "refs/heads/master"), + target: new CommitAndRef("g", null), + currentCommit: 0, + commits: ["f", "g"], + }), + expected: "x=E:CgM-fM t=Sa:1;CfM-2 s=Sa:1;Bmaster=gM", + }, - return yield RebaseUtil.rebase(repo, originalCommit); + "conflict": { + state: ` +a=B:Ca-1;Cb-1 a=8;Ba=a;Bb=b|x=U:Cf-2 s=Sa:a;Cg-2 s=Sa:b;H=f;Bg=g`, + seq: new SequencerState({ + type: REBASE, + originalHead: new CommitAndRef("f", null), + target: new CommitAndRef("g", null), + currentCommit: 0, + commits: ["g"], + }), + expected: ` +x=E:QR f: g: 0 g;Os Edetached HEAD,b,a!I *a=~*a*8!W a=\ +<<<<<<< HEAD +a +======= +8 +>>>>>>> message +;`, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + const runnerOp = co.wrap(function *(repos, maps) { + const repo = repos.x; + const seq = SequencerStateUtil.mapCommits(c.seq, + maps.reverseCommitMap); + return yield RebaseUtil.runRebase(repo, seq); }); - - return makeRebaser(rebaseOper); - } + const runner = makeRebaser(runnerOp); + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.state, + c.expected, + runner, + c.fails); + })); + }); +}); +describe("rebase", function () { const cases = { + "already a rebase in progress": { + initial: "x=S:QM 1: 1: 0 1", + onto: "1", + fails: true, + }, + "dirty": { + initial: "a=B|x=U:Os W README.md=2", + onto: "1", + fails: true, + }, "trivially nothing to do": { initial: "x=S", - rebaser: rebaser("x", "1"), + onto: "1", }, "nothing to do, in past": { initial: "x=S:C2-1;Bmaster=2", - rebaser: rebaser("x", "1"), + onto: "1", + }, + "nothing to do, detached": { + initial: "x=S:C2-1;H=2", + onto: "1", }, "ffwd": { initial: "x=S:C2-1;Bfoo=2", - rebaser: rebaser("x", "2"), + onto: "2", expected: "x=E:Bmaster=2", }, "simple rebase": { - initial: "x=S:C2-1;C3-1;Bmaster=2;Bfoo=3", - rebaser: rebaser("x", "3"), - expected: "x=S:C2M-3 2=2;C3-1;Bmaster=2M;Bfoo=3", - }, - "rebase two commits": { - initial: "x=S:C2-1;C3-2;C4-1;Bmaster=3;Bfoo=4;Bx=3", - rebaser: rebaser("x", "4"), - expected: "x=E:C3M-2M 3=3;C2M-4 2=2;Bmaster=3M", + initial: ` +a=B:Cf-1;Cg-1;Bf=f;Bg=g|x=U:C3-2 s=Sa:f;C4-2 s=Sa:g;Bother=4;Bfoo=3;Bmaster=3`, + onto: "4", + expected: "x=E:C3M-4 s=Sa:fs;Bmaster=3M;Os Cfs-g f=f!H=fs", }, - "rebase two commits on two": { - initial: "x=S:C2-1;C3-2;C4-1;C5-4;Bmaster=3;Bfoo=5;Bx=3", - rebaser: rebaser("x", "5"), - expected: "x=E:C3M-2M 3=3;C2M-5 2=2;Bmaster=3M", + "two commits": { + initial: ` +a=B|x=S:C2-1;Bmaster=g;Cf-1 s=Sa:1;Cg-f t=Sa:1;Bonto=2;Bg=g`, + onto: "2", + expected: "x=E:CgM-fM t=Sa:1;CfM-2 s=Sa:1;Bmaster=gM", }, "up-to-date with sub": { initial: "a=Aa:Cb-a;Bfoo=b|x=U:C3-2 s=Sa:b;Bmaster=3;Bfoo=2", - rebaser: rebaser("x", "2"), + onto: "2", }, "ffwd with sub": { initial: "a=Aa:Cb-a;Bfoo=b|x=U:C3-2 s=Sa:b;Bmaster=2;Bfoo=3", - rebaser: rebaser("x", "3"), + onto: "3", expected: "x=E:Bmaster=3", }, "rebase change in closed sub": { - initial: "\ -a=Aa:Cb-a;Cc-a;Bmaster=b;Bfoo=c|\ -x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3", - rebaser: rebaser("x", "4"), + initial: ` +a=B:Ca-1;Cb-a;Cc-a;Bmaster=b;Bfoo=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3`, + onto: "4", expected: "x=E:C3M-4 s=Sa:bs;Bmaster=3M;Os Cbs-c b=b!H=bs", }, "rebase change in sub, sub already open": { - initial: "\ -a=Aa:Cb-a;Cc-a;Bmaster=b;Bfoo=c|\ -x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3;Os H=b", - rebaser: rebaser("x", "4"), + initial: ` +a=B:Ca-1;Cb-a;Cc-a;Bmaster=b;Bfoo=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3;Os H=b`, + onto: "4", expected: "x=E:C3M-4 s=Sa:bs;Bmaster=3M;Os Cbs-c b=b!H=bs", }, "rebase change in sub with two commits": { - initial: "\ -a=Aa:Cb-a;Cc-a;Bmaster=b;Bfoo=c|\ -x=U:C4-2;C5-4 s=Sa:b;C3-2 s=Sa:c;Bmaster=5;Bfoo=5;Bother=3;Os H=b", - rebaser: rebaser("x", "3"), + initial: ` +a=B:Ca-1;Cb-a;Cc-a;Bmaster=b;Bfoo=c| +x=U:C4-2 s=Sa:b;C3-2 s=Sa:c;Bmaster=4;Bfoo=4;Bother=3;Os H=b`, + onto: "3", expected: ` -x=E:C5M-4M s=Sa:bs;C4M-3 4=4;Bmaster=5M;Os Cbs-c b=b!H=bs`, +x=E:C4M-3 s=Sa:bs;Bmaster=4M;Os Cbs-c b=b!H=bs`, }, "rebase change in sub with two intervening commits": { initial: ` -a=Aa:Cb-a;Cc-a;Cd-c;Bmaster=b;Bfoo=d| -x=U:C4-2;C5-4 s=Sa:c;C6-5;C7-6 s=Sa:d;C3-2 s=Sa:b;Bmaster=7;Bfoo=7;Bother=3`, - rebaser: rebaser("x", "3"), +a=B:Ca-1;Cb-a;Cc-a;Cd-c;Bmaster=b;Bfoo=d| +x=U:C4-2 r=Sa:1;C5-4 s=Sa:c;C6-5 q=Sa:1;C7-6 s=Sa:d;C3-2 s=Sa:b;Bmaster=7; + Bfoo=7;Bother=3`, + onto: "3", expected: ` -x=E:C7M-6M s=Sa:ds;C6M-5M 6=6;C5M-4M s=Sa:cs;C4M-3 4=4;Bmaster=7M; +x=E:C7M-6M s=Sa:ds;C6M-5M q=Sa:1;C5M-4M s=Sa:cs;C4M-3 r=Sa:1;Bmaster=7M; Os Cds-cs d=d!Ccs-b c=c!H=ds`, }, "rebase change in sub with two intervening commits, open": { initial: ` -a=Aa:Cb-a;Cc-a;Cd-c;Bmaster=b;Bfoo=d| -x=U:C4-2;C5-4 s=Sa:c;C6-5;C7-6 s=Sa:d;C3-2 s=Sa:b;Bmaster=7;Bfoo=7;Bother=3; +a=B:Ca-1;Cb-a;Cc-a;Cd-c;Bmaster=b;Bfoo=d| +x=U:C4-2 t=Sa:a;C5-4 s=Sa:c;C6-5 u=Sa:1;C7-6 s=Sa:d;C3-2 s=Sa:b; + Bmaster=7;Bfoo=7;Bother=3; Os H=d`, - rebaser: rebaser("x", "3"), + onto: "3", expected: ` -x=E:C7M-6M s=Sa:ds;C6M-5M 6=6;C5M-4M s=Sa:cs;C4M-3 4=4;Bmaster=7M; +x=E:C7M-6M s=Sa:ds;C6M-5M u=Sa:1;C5M-4M s=Sa:cs;C4M-3 t=Sa:a;Bmaster=7M; Os Cds-cs d=d!Ccs-b c=c!H=ds`, }, "ffwd, but not sub (should ffwd anyway)": { initial: "\ a=Aa:Cb-a;Cc-a;Bmaster=b;Bfoo=c|\ x=U:C3-2 s=Sa:b;C4-3 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3", - rebaser: rebaser("x", "4"), + onto: "4", expected: "x=E:Bmaster=4", }, "no ffwd, but can ffwd sub": { - initial: "\ -a=Aa:Cb-a;Cc-b;Bmaster=b;Bfoo=c|\ -x=U:C3-2 3=3,s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3;Os", - rebaser: rebaser("x", "4"), - expected: "x=E:C3M-4 3=3;Bmaster=3M;Os H=c", + initial: ` +a=B:Ca-1;Cb-a;Cc-b;Bmaster=b;Bfoo=c| +x=U:C3-2 u=Sa:a,s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3;Os`, + onto: "4", + expected: "x=E:C3M-4 u=Sa:a;Bmaster=3M;Os H=c", }, "ffwd sub 2X": { initial: ` -a=Aa:Cb-a;Cc-b;Bmaster=b;Bfoo=c| +a=B:Ca-1;Cb-a;Cc-b;Bmaster=b;Bfoo=c| x=U:Cr-2;C3-2 s=Sa:b;C4-3 s=Sa:c;Bmaster=4;Bother=r;Os;Bfoo=4`, - rebaser: rebaser("x", "r"), + onto: "r", expected: "x=E:C3M-r s=Sa:b;C4M-3M s=Sa:c;Bmaster=4M;Os H=c", }, - "ffwd-ed sub is closed after rebase": { - initial: "\ -a=Aa:Cb-a;Cc-b;Bmaster=b;Bfoo=c|\ -x=U:C3-2 3=3,s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3", - rebaser: rebaser("x", "4"), - expected: "x=E:C3M-4 3=3;Bmaster=3M", + "up-to-date sub is closed after rebase": { + initial: ` +a=B:Ca-1;Cb-a;Cc-b;Bmaster=b;Bfoo=c| +x=U:C3-2 u=Sa:1,s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3`, + onto: "4", + expected: "x=E:C3M-4 u=Sa:1;Bmaster=3M", }, "rebase two changes in sub": { - initial: "\ -a=Aa:Cb-a;Cc-b;Cd-a;Bmaster=c;Bfoo=d|\ -x=U:C3-2 s=Sa:c;C4-2 s=Sa:d;Bmaster=3;Bfoo=4;Bother=3", - rebaser: rebaser("x", "4"), - expected: "\ -x=E:C3M-4 s=Sa:cs;Bmaster=3M;Os Ccs-bs c=c!Cbs-d b=b!H=cs", + initial: ` +a=B:Ca-1;Cb-a;Cc-b;Cd-a;Bmaster=c;Bfoo=d| +x=U:C3-2 s=Sa:c;C4-2 s=Sa:d;Bmaster=3;Bfoo=4;Bother=3`, + onto: "4", + expected: ` +x=E:C3M-4 s=Sa:cs;Bmaster=3M;Os Ccs-bs c=c!Cbs-d b=b`, }, "rebase with ffwd changes in sub and meta": { - initial: "\ -a=B:Bmaster=3;C2-1 s=Sb:q;C3-2 s=Sb:r,rar=wow|\ -b=B:Cq-1;Cr-q;Bmaster=r|\ -x=Ca:Bmaster=2;Os", - rebaser: rebaser("x", "3"), + initial: ` +a=B:Bmaster=3;C2-1 s=Sb:q;C3-2 s=Sb:r,rar=wow| +b=B:Cq-1;Cr-q;Bmaster=r| +x=Ca:Bmaster=2;Os`, + onto: "3", expected: "x=E:Bmaster=3;Os H=r", }, "make sure unchanged repos stay closed": { - initial: "\ -a=B|\ -b=B:Cj-1;Ck-1;Bmaster=j;Bfoo=k|\ -x=S:C2-1 s=Sa:1,t=Sb:1;C3-2 t=Sb:j;C4-2 t=Sb:k;Bmaster=3;Bfoo=4;Bold=3", - rebaser: rebaser("x", "4"), - expected: "\ -x=E:C3M-4 t=Sb:jt;Bmaster=3M;Ot H=jt!Cjt-k j=j", + initial: ` +a=B| +b=B:Cj-1;Ck-1;Bmaster=j;Bfoo=k| +x=S:C2-1 s=Sa:1,t=Sb:1;C3-2 t=Sb:j;C4-2 t=Sb:k;Bmaster=3;Bfoo=4;Bold=3`, + onto: "4", + expected: ` +x=E:C3M-4 t=Sb:jt;Bmaster=3M;Ot H=jt!Cjt-k j=j`, }, "make sure unchanged repos stay closed -- onto-only change": { - initial: "\ -a=B|\ -b=B:Cj-1;Ck-1;Bmaster=j;Bfoo=k|\ -x=S:C2-1 s=Sa:1,t=Sb:1;C3-2;C4-2 t=Sb:k;Bmaster=3;Bfoo=4;Bold=3", - rebaser: rebaser("x", "4"), - expected: "\ -x=E:C3M-4 3=3;Bmaster=3M", + initial: ` +a=B| +b=B:Cj-1;Ck-1;Bmaster=j;Bfoo=k| +x=S:C2-1 s=Sa:1,t=Sb:1;C3-2 q=Sa:k;C4-2 t=Sb:k;Bmaster=3;Bfoo=4;Bold=3`, + onto: "4", + expected: ` +x=E:C3M-4 q=Sa:k;Bmaster=3M`, }, "make sure unchanged repos stay closed -- local-only change": { - initial: "\ -a=B|\ -b=B:Cj-1;Ck-1;Bmaster=j;Bfoo=k|\ -x=S:C2-1 s=Sa:1,t=Sb:1;C3-2;C4-2 t=Sb:k;Bmaster=4;Bfoo=3;Bold=4", - rebaser: rebaser("x", "3"), - expected: "\ -x=E:C4M-3 t=Sb:k;Bmaster=4M", + initial: ` +a=B| +b=B:Cj-1;Ck-1;Bmaster=j;Bfoo=k| +x=S:C2-1 s=Sa:1,t=Sb:1;C3-2;C4-2 t=Sb:k;Bmaster=4;Bfoo=3;Bold=4`, + onto: "3", + expected: ` +x=E:C4M-3 t=Sb:k;Bmaster=4M`, }, "unchanged repos stay closed -- different onto and local": { - initial: "\ -a=B:Cj-1;Bmaster=j|\ -b=B:Ck-1;Bmaster=k|\ -x=S:C2-1 s=Sa:1,t=Sb:1;C3-2 s=Sa:j;C4-2 t=Sb:k;Bmaster=3;Bfoo=4;Bold=3", - rebaser: rebaser("x", "4"), - expected: "\ -x=E:C3M-4 s=Sa:j;Bmaster=3M", - }, - "maintain submodule branch": { - initial: "\ -a=B:Ca-1;Cb-1;Bx=a;By=b|\ -x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4;Bold=3;Os Bmaster=a!*=master", - rebaser: rebaser("x", "4"), - expected: "\ -x=E:C3M-4 s=Sa:as;Bmaster=3M;Os Bmaster=as!Cas-b a=a!*=master", + initial: ` +a=B:Cj-1;Bmaster=j| +b=B:Ck-1;Bmaster=k| +x=S:C2-1 s=Sa:1,t=Sb:1;C3-2 s=Sa:j;C4-2 t=Sb:k;Bmaster=3;Bfoo=4;Bold=3`, + onto: "4", + expected: ` +x=E:C3M-4 s=Sa:j;Bmaster=3M`, }, "adding subs on both": { - initial: "\ -q=B|r=B|s=B|x=S:C2-1 s=Ss:1;C3-2 q=Sq:1;C4-2 r=Sr:1;Bmaster=3;Bfoo=4;Bold=3", - rebaser: rebaser("x", "4"), - expected: "\ -x=E:C3M-4 q=Sq:1;Bmaster=3M", - }, - "adding subs then changing": { - initial: "\ -q=B|\ -r=B|\ -s=B|\ -x=S:C2-1 s=Ss:1;C3-2 q=Sq:1;C31-3 q=Sr:1;C4-2 r=Sr:1;C41-4 r=Ss:1;\ -Bmaster=31;Bfoo=41;Bold=31", - rebaser: rebaser("x", "41"), - expected: "\ -x=E:C3M-41 q=Sq:1;C31M-3M q=Sr:1;Bmaster=31M", + initial: ` +q=B|r=B|s=B|x=S:C2-1 s=Ss:1;C3-2 q=Sq:1;C4-2 r=Sr:1;Bmaster=3;Bfoo=4;Bold=3`, + onto: "4", + expected: ` +x=E:C3M-4 q=Sq:1;Bmaster=3M`, }, "open sub ffwd'd": { initial: ` a=B:CX-1;Bmaster=X| -x=U:C3-2 a=b;C4-2 s=Sa:X;Bmaster=3;Bfoo=4;Bold=3;Os`, - rebaser: rebaser("x", "4"), +x=U:C3-2 a=Sa:1;C4-2 s=Sa:X;Bmaster=3;Bfoo=4;Bold=3;Os`, + onto: "4", expected: ` -x=E:C3M-4 a=b;Bmaster=3M;Os H=X`, +x=E:C3M-4 a=Sa:1;Bmaster=3M;Os H=X`, }, + "conflict": { + initial: ` +a=B:Ca-1 t=t;Cb-1 t=u;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4;Bold=3`, + onto: "4", + expected: ` +x=E:H=4;QR 3:refs/heads/master 4: 0 3; + Os Edetached HEAD,a,b!I *t=~*u*t!W t=\ +<<<<<<< HEAD +u +======= +t +>>>>>>> message +;`, + errorMessage: `\ +Submodule ${colors.red("s")} is conflicted. +`, + }, +// TODO: I could not get libgit2 to remember or restore submodule branches when +// used in the three-way mode; it stores it in "onto_name", not in "head-name". +// "maintain submodule branch": { +// initial: ` +//a=B:Ca-1;Cb-1;Bx=a;By=b| +//x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4;Bold=3;Os Bmaster=a!*=master`, +// onto: "4", +// expected: ` +//x=E:C3M-4 s=Sa:as;Bmaster=3M;Os Bmaster=as!Cas-b a=a!*=master`, +// }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; + // Will append the leter 'M' to any created meta-repo commits, and the + // submodule name to commits created in respective submodules. + + const rebaseOp = co.wrap(function *(repos, maps) { + const repo = repos.x; + const reverseCommitMap = maps.reverseCommitMap; + const onto = yield repo.getCommit(reverseCommitMap[c.onto]); + const errorMessage = c.errorMessage || null; + const result = yield RebaseUtil.rebase(repo, onto); + assert.equal(result.errorMessage, errorMessage); + return result; + }); + const rebase = makeRebaser(rebaseOp); it(caseName, co.wrap(function *() { yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, c.expected, - c.rebaser, + rebase, c.fails); })); }); - it("conflict stays open", co.wrap(function *() { - const input = ` -a=B:Ca-1 t=t;Cb-1 t=u;Ba=a;Bb=b| -x=U:C31-2 s=Sa:a;C41-2 s=Sa:b;Bmaster=31;Bfoo=41;Bold=31`; - const w = yield RepoASTTestUtil.createMultiRepos(input); - const repo = w.repos.x; - const reverseCommitMap = w.reverseCommitMap; - const originalActualCommit = reverseCommitMap["41"]; - const originalCommit = yield repo.getCommit(originalActualCommit); - let threw = false; - try { - yield RebaseUtil.rebase(repo, originalCommit); - } - catch (e) { - threw = true; - } - assert(threw, "should have thrown"); - const open = yield SubmoduleUtil.isVisible(repo, "s"); - assert(open, "should be open"); - })); }); describe("abort", function () { const cases = { - "simple, see if workdir is cleaned up": { + "no sequencer, fails": { + initial: "x=S", + fails: true, + }, + "sequencer not a rebase, fails": { + initial: "x=S:QM 1: 1: 0 1", + fails: true, + }, + "see if head is reset": { initial: ` -x=S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;W x=q`, - expected: `x=E:E;W x=~`, +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;H=4;Bmaster=3;Bfoo=4;QR 3: 4: 0 2`, + expected: `x=E:H=3;Q` }, - "with rebase in submodule": { + "check that branch is restored": { initial: ` -a=B:Cq-1;Cr-1;Bmaster=q;Bfoo=r| -x=U:C3-2 s=Sa:q;C4-2 s=Sa:r; - Bmaster=3;Bfoo=4; - Erefs/heads/master,3,4; - Os Erefs/heads/foo,q,r!Bfoo=q!*=foo`, - expected: `x=E:E;Os Bfoo=q!*=foo`, +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;H=4;Bmaster=3;Bfoo=4; + QR 3:refs/heads/master 4: 0 2`, + expected: `x=E:*=master;Q` + }, + "submodule wd cleaned up": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;H=4;Bmaster=3;Bfoo=4; + QR 3: 4: 0 2; + Os W README.md=88`, + expected: `x=E:H=3;Q;Os H=a` + }, + "submodule rebase cleaned up": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;H=4;Bmaster=3;Bfoo=4; + QR 3: 4: 0 2; + Os Edetached HEAD,a,b`, + expected: `x=E:H=3;Q;Os H=a` }, }; Object.keys(cases).forEach(caseName => { @@ -337,95 +461,110 @@ x=U:C3-2 s=Sa:q;C4-2 s=Sa:r; }); describe("continue", function () { const cases = { - "meta-only": { + "no sequencer, fails": { + initial: "x=S", + fails: true, + }, + "sequencer not a rebase, fails": { + initial: "x=S:QM 1: 1: 0 1", + fails: true, + }, + "conflict fails": { initial: ` -x=S:C2-1 q=r;C3-1 q=s;Bmaster=2;Erefs/heads/master,2,3;I q=z`, - expected: ` -x=S:C2M-3 q=z;Bmaster=2M;E`, +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;H=4;Bmaster=3;Bfoo=4; + QR 3: 4: 0 2; + Os I *a=1*2*3!W a=2`, + fails: true, }, - "two, meta-only": { + "continue finishes with new commit": { initial: ` -x=S:C2-1;C3-1;C4-3;Bmaster=4;Erefs/heads/master,4,2;I qq=hh,3=3`, +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4;H=4;Bold=3; + QR 3:refs/heads/master 4: 0 3; + Os I foo=bar`, expected: ` -x=S:C4M-3M 4=4;C3M-2 3=3,qq=hh;Bmaster=4M;E`, +x=E:C3M-4 s=Sa:Ns;Bmaster=3M;*=master;Q;Os CNs-b foo=bar!H=Ns;`, }, - "meta, has to open": { + "continue makes a new conflict with current op": { initial: ` -a=B:Ca-1;Cb-1;Bmaster=a;Bfoo=b| -x=U:C3-2 s=Sa:a; - C4-2;C5-4 s=Sa:b; - Bmaster=5;Bfoo=5; - I 4=4; - Erefs/heads/master,5,3`, +a=B:Ca-1;Cb-a c=d;Cc-1;Bb=b;Bc=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;H=4; + QR 3: 4: 0 3; + Os I foo=bar!Edetached HEAD,b,c!H=c!Bb=b!Bc=c`, + errorMessage: `\ +Conflict in ${colors.red("s")} +`, expected: ` -x=E:C5M-4M s=Sa:bs;C4M-3 4=4;Bmaster=5M;E;Os Cbs-a b=b!H=bs;I 4=~`, +x=E:Os Cas-c foo=bar!H=as!Edetached HEAD,b,c!Bb=b!Bc=c!I *c=~*c*d! + W c=^<<<<<`, }, "with rebase in submodule": { initial: ` a=B:Cq-1;Cr-1;Bmaster=q;Bfoo=r| x=U:C3-2 s=Sa:q;C4-2 s=Sa:r; - Bmaster=3;Bfoo=4;Bold=3; - Erefs/heads/master,3,4; + Bmaster=4;Bfoo=4;Bold=3; + QR 3:refs/heads/master 4: 0 3; Os EHEAD,q,r!I q=q!Bq=q!Br=r`, expected: ` -x=E:E;C3M-4 s=Sa:qs;Bmaster=3M;Os Cqs-r q=q!H=qs!E!Bq=q!Br=r` +x=E:Q;C3M-4 s=Sa:qs;Bmaster=3M;Os Cqs-r q=q!H=qs!E!Bq=q!Br=r` }, "with rebase in submodule, other open subs": { initial: ` a=B:Cq-1;Cr-1;Bmaster=q;Bfoo=r| x=S:C2-1 a=Sa:1,s=Sa:1,z=Sa:1;C3-2 s=Sa:q;C4-2 s=Sa:r; - Bmaster=3;Bfoo=4;Bold=3; - Erefs/heads/master,3,4; + Bmaster=4;Bfoo=4;Bold=3; + QR 3:refs/heads/master 4: 0 3; Oa;Oz; Os EHEAD,q,r!I q=q!Bq=q!Br=r`, expected: ` -x=E:E;C3M-4 s=Sa:qs;Bmaster=3M;Os Cqs-r q=q!H=qs!E!Bq=q!Br=r;Oa;Oz` +x=E:Q;C3M-4 s=Sa:qs;Bmaster=3M;Os Cqs-r q=q!H=qs!E!Bq=q!Br=r;Oa;Oz` }, "with rebase in submodule, staged commit in another submodule": { initial: ` a=B:Cq-1;Cr-1;Cs-q;Bmaster=q;Bfoo=r;Bbar=s| x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q,t=Sa:s;C4-2 s=Sa:r,t=Sa:q; - Bmaster=3;Bfoo=4;Bold=3; - Erefs/heads/master,3,4; + Bmaster=4;Bfoo=4;Bold=3; + QR 3:refs/heads/master 4: 0 3; Os EHEAD,q,r!I q=q!Bq=q!Br=r; Ot H=s; I t=Sa:s`, expected: ` -x=E:E;C3M-4 s=Sa:qs,t=Sa:s;Bmaster=3M; +x=E:Q;C3M-4 s=Sa:qs,t=Sa:s;Bmaster=3M; Os Cqs-r q=q!H=qs!E!Bq=q!Br=r;I t=~;Ot` }, "with rebase in submodule, workdir commit in another submodule": { initial: ` a=B:Cq-1;Cr-1;Cs-q;Bmaster=q;Bfoo=r;Bbar=s| x=S:C2-1 s=Sa:1,t=Sa:1;C3-2 s=Sa:q,t=Sa:s;C4-2 s=Sa:r,t=Sa:q; - Bmaster=3;Bfoo=4;Bold=3; - Erefs/heads/master,3,4; + Bmaster=4;Bfoo=4;Bold=3; + QR 3:refs/heads/master 4: 0 3; Os EHEAD,q,r!I q=q!Bq=q!Br=r; Ot H=s`, expected: ` -x=E:E;C3M-4 s=Sa:qs,t=Sa:s;Bmaster=3M; +x=E:Q;C3M-4 s=Sa:qs,t=Sa:s;Bmaster=3M; Os Cqs-r q=q!H=qs!E!Bq=q!Br=r; Ot H=s` }, "staged fix in submodule": { initial: ` a=B:Ca-1 q=r;Cb-1 q=s;Bmaster=a;Bfoo=b| -x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Erefs/heads/master,3,4;Bold=3; +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=4;QR 3:refs/heads/master 4: 0 3;Bold=3; Os EHEAD,a,b!I q=z!Ba=a!Bb=b`, expected: ` -x=E:C3M-4 s=Sa:as;E;Bmaster=3M;Os Cas-b q=z!H=as!Ba=a!Bb=b`, +x=E:C3M-4 s=Sa:as;Q;Bmaster=3M;Os Cas-b q=z!H=as!Ba=a!Bb=b`, }, "multiple in subs": { initial: ` a=B:Ca1-1 f=g;Ca2-1 f=h;Bmaster=a1;Bfoo=a2| b=B:Cb1-1 q=r;Cb2-1 q=s;Bmaster=b1;Bfoo=b2| x=S:C2-1 s=Sa:1,t=Sb:1;C3-2 s=Sa:a1,t=Sb:b1;C4-2 s=Sa:a2,t=Sb:b2; - Bmaster=3;Bfoo=4;Bold=3; - Erefs/heads/master,3,4; + Bmaster=4;Bfoo=4;Bold=3; + QR 3:refs/heads/master 4: 0 3; Os EHEAD,a1,a2!I f=z!Ba1=a1!Ba2=a2; Ot EHEAD,b1,b2!I q=t!Bb1=b1!Bb2=b2`, expected: ` -x=E:C3M-4 s=Sa:a1s,t=Sb:b1t;E;Bmaster=3M; +x=E:C3M-4 s=Sa:a1s,t=Sb:b1t;Q;Bmaster=3M; Os Ca1s-a2 f=z!H=a1s!Ba1=a1!Ba2=a2; Ot Cb1t-b2 q=t!H=b1t!Bb1=b1!Bb2=b2` }, @@ -434,7 +573,10 @@ x=E:C3M-4 s=Sa:a1s,t=Sb:b1t;E;Bmaster=3M; const c = cases[caseName]; it(caseName, co.wrap(function *() { const continuer = makeRebaser(co.wrap(function *(repos) { - return yield RebaseUtil.continue(repos.x); + const errorMessage = c.errorMessage || null; + const result = yield RebaseUtil.continue(repos.x); + assert.equal(result.errorMessage, errorMessage); + return result; })); yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, c.expected, From cd718ade6aa14aa0297f4b0a4b40ad784f6881ed Mon Sep 17 00:00:00 2001 From: Shihui Xiang Date: Mon, 9 Apr 2018 19:52:25 -0400 Subject: [PATCH 087/402] git meta swallows hook output --- node/lib/util/hook.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node/lib/util/hook.js b/node/lib/util/hook.js index d752ee54c..43c36132e 100644 --- a/node/lib/util/hook.js +++ b/node/lib/util/hook.js @@ -54,6 +54,7 @@ exports.execHook = co.wrap(function *(name, args=[]) { try { process.chdir(rootDirectory); const result = yield ChildProcess.execFile(absPath, args); + console.log(result.stdout); return result.stdout; } catch (e) { // ignore the exception From 227e12824c05e89554ee19841982fb7700216642 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 10 Apr 2018 09:20:21 -0400 Subject: [PATCH 088/402] return null for `errorMessage` when no rebase --- node/lib/util/rebase_util.js | 1 + node/test/util/rebase_util.js | 3 +++ 2 files changed, 4 insertions(+) diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index f775eb66e..d45443744 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -184,6 +184,7 @@ running rebase.`); const result = { metaCommits: {}, submoduleCommits: {}, + errorMessage: null, }; const headCommit = yield repo.getHeadCommit(); diff --git a/node/test/util/rebase_util.js b/node/test/util/rebase_util.js index 000d0bcce..06dfc2114 100644 --- a/node/test/util/rebase_util.js +++ b/node/test/util/rebase_util.js @@ -393,6 +393,9 @@ Submodule ${colors.red("s")} is conflicted. const onto = yield repo.getCommit(reverseCommitMap[c.onto]); const errorMessage = c.errorMessage || null; const result = yield RebaseUtil.rebase(repo, onto); + if (null !== result.errorMessage) { + assert.isString(result.errorMessage); + } assert.equal(result.errorMessage, errorMessage); return result; }); From 2244dca20e25c59f19e32ea4dee57317806b3187 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 10 Apr 2018 09:32:58 -0400 Subject: [PATCH 089/402] Add missing `yield` when invoking continue --- node/lib/cmd/rebase.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/rebase.js b/node/lib/cmd/rebase.js index f1b822d01..d8ee74979 100644 --- a/node/lib/cmd/rebase.js +++ b/node/lib/cmd/rebase.js @@ -96,7 +96,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { yield RebaseUtil.abort(repo); } else if (args.continue) { - const result = RebaseUtil.continue(repo); + const result = yield RebaseUtil.continue(repo); if (null !== result.errorMessage) { throw new UserError(result.errorMessage); } From 0221785bd482b740c65605fff8f318005b603fcb Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 10 Apr 2018 17:43:38 -0400 Subject: [PATCH 090/402] fix/improve help --- node/lib/cmd/submodule.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/node/lib/cmd/submodule.js b/node/lib/cmd/submodule.js index 7d3ede73b..ed8e14c50 100644 --- a/node/lib/cmd/submodule.js +++ b/node/lib/cmd/submodule.js @@ -84,12 +84,13 @@ that submodule, followed by its name. ` help: "create references in sub-repos matching refs in the meta-repo", description: `\ Create references in sub-repos pointing to the commits indicated by the \ -reference having that name in the meta-repo. The default behavior is to \ -map every reference in the meta-repo into every open sub-repo.`, +reference having that name in the meta-repo.`, }); addRefsParser.addArgument(["path"], { - help: "if provided, open only submodules in selected paths", + help: ` +if provided, apply to submodules in selected paths only, otherwise apply to \ +all`, nargs: "*", }); From cc8770d1f3d0194c34dfa6ec7543f9057e3f2e63 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 12 Apr 2018 16:37:11 -0400 Subject: [PATCH 091/402] Stop diagnosing non-git-meta rebases --- node/lib/util/print_status_util.js | 22 -------------------- node/test/util/print_status_util.js | 32 ----------------------------- 2 files changed, 54 deletions(-) diff --git a/node/lib/util/print_status_util.js b/node/lib/util/print_status_util.js index 47b88f568..78e2ca64c 100644 --- a/node/lib/util/print_status_util.js +++ b/node/lib/util/print_status_util.js @@ -42,7 +42,6 @@ const path = require("path"); const GitUtil = require("./git_util"); const SequencerState = require("./sequencer_state"); -const Rebase = require("./rebase"); const RepoStatus = require("./repo_status"); const TextUtil = require("./text_util"); @@ -344,23 +343,6 @@ exports.accumulateStatus = function (status) { }; }; -/** - * Return a message describing the specified `rebase`. - * - * @param {Rebase} - * @return {String} - */ -exports.printRebase = function (rebase) { - assert.instanceOf(rebase, Rebase); - const shortSha = GitUtil.shortSha(rebase.onto); - return `${colors.red("rebase in progress; onto ", shortSha)} -You are currently rebasing branch '${rebase.headName}' on '${shortSha}'. - (after resolving conflicts mark the corrected paths - with 'git meta add', then run "git meta rebase --continue") - (use "git meta rebase --abort" to check out the original branch) -`; -}; - /** * Return the command to which the specified sequencer `type` corresponds. * @@ -423,10 +405,6 @@ exports.printRepoStatus = function (status, cwd) { let result = ""; - if (null !== status.rebase) { - result += exports.printRebase(status.rebase); - } - result += exports.printCurrentBranch(status); if (null !== status.sequencerState) { diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js index accf93264..0fafc2f8e 100644 --- a/node/test/util/print_status_util.js +++ b/node/test/util/print_status_util.js @@ -34,7 +34,6 @@ const assert = require("chai").assert; const colors = require("colors"); const NodeGit = require("nodegit"); -const Rebase = require("../../lib/util/rebase"); const RepoStatus = require("../../lib/util/repo_status"); const PrintStatusUtil = require("../../lib/util/print_status_util"); const SequencerState = require("../../lib/util/sequencer_state"); @@ -638,30 +637,6 @@ describe("PrintStatusUtil", function () { }); }); - describe("printRebase", function () { - const cases = { - "basic": { - input: new Rebase("master", "xxx", "ffffffffffffffff"), - check: /rebase in progress/ - }, - "branch": { - input: new Rebase("master", "xxx", "ffffffffffffffff"), - check: /master/ - }, - "sha": { - input: new Rebase("master", "xxx", "ffffffffffffffff"), - check: /ffff/, - }, - }; - Object.keys(cases).forEach(caseName => { - const c = cases[caseName]; - it(caseName, function () { - const result = PrintStatusUtil.printRebase(c.input); - assert.match(result, c.check); - }); - }); - }); - describe("getSequencerCommand", function () { const cases = { "merge": { @@ -768,13 +743,6 @@ A rebase is in progress. nothing to commit, working tree clean `, }, - "rebase": { - input: new RepoStatus({ - currentBranchName: "master", - rebase: new Rebase("x", "y", "z"), - }), - regex: /rebas/, - }, "detached": { input: new RepoStatus({ headCommit: "ffffaaaaffffaaaa", From 1a2911f31b30c7f0b794247855951dbe2b1ab7bd Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 13 Apr 2018 21:13:39 -0400 Subject: [PATCH 092/402] Moved config utils into a seperate module We will need to use these (transitively) from deinit_util, on which git_util already (transitively), which would create a cycle. --- node/lib/util/config_util.js | 85 +++++++++++++++++++++++ node/lib/util/git_util.js | 45 ++---------- node/lib/util/pull.js | 29 +++----- node/lib/util/synthetic_branch_util.js | 5 +- node/test/util/config_util.js | 96 ++++++++++++++++++++++++++ node/test/util/git_util.js | 17 ----- 6 files changed, 201 insertions(+), 76 deletions(-) create mode 100644 node/lib/util/config_util.js create mode 100644 node/test/util/config_util.js diff --git a/node/lib/util/config_util.js b/node/lib/util/config_util.js new file mode 100644 index 000000000..2157370c3 --- /dev/null +++ b/node/lib/util/config_util.js @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const NodeGit = require("nodegit"); + +/** + * This module contains methods for interacting with git configuration entries. + */ + +/** + * Return the string in the specified `config` for the specified `key`, or null + * if `key` does not exist in `config`. + * + * @param {NodeGit.Config} config + * @param {String} key + * @return {String|null} + */ +exports.getConfigString = co.wrap(function *(config, key) { + assert.instanceOf(config, NodeGit.Config); + assert.isString(key); + + try { + return yield config.getStringBuf(key); + } + catch (e) { + // Unfortunately, no other way to handle a missing config entry + } + return null; +}); + +/** + * Returns whether a config variable is, according to git's reckoning, + * true. That is, it's set to 'true', 'yes', or 'on'. If the variable is not + * se at all, return null. + * @async + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} configVar + * @return {Bool|null} + * @throws if the configuration variable doesn't exist +*/ +exports.configIsTrue = co.wrap(function*(repo, configVar) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(configVar); + + const config = yield repo.config(); + const configured = yield exports.getConfigString(config, configVar); + if (null === configured) { + return configured; // RETURN + } + return configured === "true" || configured === "yes" || + configured === "on"; +}); + + diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index ce9c1abdf..9bf1115cb 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -42,8 +42,9 @@ const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); -const DoWorkQueue = require("../util/do_work_queue"); -const UserError = require("../util/user_error"); +const ConfigUtil = require("./config_util"); +const DoWorkQueue = require("./do_work_queue"); +const UserError = require("./user_error"); /** * If the directory identified by the specified `dir` contains a ".git" @@ -111,24 +112,6 @@ exports.findBranch = co.wrap(function *(repo, branchName) { } }); -/** - * Return the string in the specified `config` for the specified `key`, or null - * if `key` does not exist in `config`. - * - * @param {NodeGit.Config} config - * @param {String} key - * @return {String|null} - */ -exports.getConfigString = co.wrap(function *(config, key) { - try { - return yield config.getStringBuf(key); - } - catch (e) { - // Unfortunately, no other way to handle a missing config entry - } - return null; -}); - /** * Return the tracking information for the specified `branch`, or null if it * has none. @@ -157,15 +140,15 @@ exports.getTrackingInfo = co.wrap(function *(repo, branch) { // Try to read a 'pushRemote' for the branch. - let pushRemote = yield exports.getConfigString( + let pushRemote = yield ConfigUtil.getConfigString( config, `branch.${branch.shorthand()}.pushRemote`); // If no 'pushRemote', try to read a 'pushDefault' for the repo. if (null === pushRemote) { - pushRemote = yield exports.getConfigString(config, - "remote.pushDefault"); + pushRemote = yield ConfigUtil.getConfigString(config, + "remote.pushDefault"); } if (1 === parts.length) { @@ -933,22 +916,6 @@ exports.getParentCommit = co.wrap(function *(repo, commit) { return yield repo.getCommit(parentId); }); -/** - * Returns whether a config variable is, according to git's reckoning, - * true. That is, it's set to 'true', 'yes', or 'on'. - * @async - * @param {NodeGit.Repository} repo - * @param {NodeGit.Commit} configVar - * @return boolean - * @throws if the configuration variable doesn't exist -*/ -exports.configIsTrue = co.wrap(function*(repo, configVar) { - const config = yield repo.config(); - const configured = yield config.getStringBuf(configVar); - return (configured === "true" || configured === "yes" || - configured === "on"); -}); - /** * Returns the URL for the specified remote. If the remote is already * a URL, it is returned unmodified. diff --git a/node/lib/util/pull.js b/node/lib/util/pull.js index 443cead5f..9d07c6e84 100644 --- a/node/lib/util/pull.js +++ b/node/lib/util/pull.js @@ -35,10 +35,11 @@ const co = require("co"); const colors = require("colors"); const NodeGit = require("nodegit"); -const GitUtil = require("../util/git_util"); -const RebaseUtil = require("../util/rebase_util"); -const StatusUtil = require("../util/status_util"); -const UserError = require("../util/user_error"); +const ConfigUtil = require("./config_util"); +const GitUtil = require("./git_util"); +const RebaseUtil = require("./rebase_util"); +const StatusUtil = require("./status_util"); +const UserError = require("./user_error"); /** * Pull the specified `source` branch from the remote having the specified @@ -86,8 +87,7 @@ ${colors.red(source)} in the remote ${colors.yellow(remoteName)}.`); /** - * Return true if the user has requested a rebase (explicitly or - * via config). + * Return true if the user has requested a rebase (explicitly or via config). * * @param {Object} args * @param {Boolean} args.rebase @@ -101,17 +101,10 @@ exports.userWantsRebase = co.wrap(function*(args, repo, branch) { return args.rebase; } - try { - const configVar = `branch.${branch.shorthand()}.rebase`; - return yield GitUtil.configIsTrue(repo, configVar); - } catch (e) { - // no branch config, try global config - } - - try { - return yield GitUtil.configIsTrue(repo, "pull.rebase"); - } catch (e) { - // no config, default is false - return false; + const branchVar = `branch.${branch.shorthand()}.rebase`; + const branchVal = yield ConfigUtil.configIsTrue(repo, branchVar); + if (null !== branchVal) { + return branchVal; } + return (yield ConfigUtil.configIsTrue(repo, "pull.rebase")) || false; }); diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 4d5f06637..54b448997 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -35,6 +35,7 @@ the meta-repo should use this hook. */ +const ConfigUtil = require("./config_util"); const NodeGit = require("nodegit"); const GitUtil = require("./git_util"); const SubmoduleUtil = require("./submodule_util"); @@ -73,11 +74,11 @@ exports.urlToLocalPath = function *(repo, url) { const config = yield repo.config(); const subrepoUrlBase = - (yield GitUtil.getConfigString(config, "gitmeta.subrepourlbase")) || ""; + (yield ConfigUtil.getConfigString(config, "gitmeta.subrepourlbase")) || ""; const subrepoRootPath = yield config.getStringBuf("gitmeta.subreporootpath"); let subrepoSuffix = - (yield GitUtil.getConfigString(config, "gitmeta.subreposuffix")) || ""; + (yield ConfigUtil.getConfigString(config, "gitmeta.subreposuffix")) || ""; if (!url.startsWith(subrepoUrlBase)) { throw "Your git configuration gitmeta.subrepoUrlBase, '" + subrepoUrlBase + "', must be a prefix of all submodule " + diff --git a/node/test/util/config_util.js b/node/test/util/config_util.js new file mode 100644 index 000000000..80019b3c6 --- /dev/null +++ b/node/test/util/config_util.js @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const fs = require("fs-promise"); +const path = require("path"); + +const ConfigUtil = require("../../lib/util/config_util"); +const TestUtil = require("../../lib/util/test_util"); + +describe("ConfigUtil", function () { +describe("getConfigString", function () { + it("breathing test", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const configPath = path.join(repo.path(), "config"); + yield fs.appendFile(configPath, `\ +[foo] + bar = baz +`); + const config = yield repo.config(); + const goodResult = + yield ConfigUtil.getConfigString(config, "foo.bar"); + assert.equal(goodResult, "baz"); + const badResult = yield ConfigUtil.getConfigString(config, "yyy.zzz"); + assert.isNull(badResult); + })); +}); +describe("configIsTrue", function () { + const cases = { + "missing": { + expected: null, + }, + "true": { + value: "true", + expected: true, + }, + "false": { + value: "false", + expected: false, + }, + "yes": { + value: "yes", + expected: true, + }, + "on": { + value: "on", + expected: true, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + if ("value" in c) { + const configPath = path.join(repo.path(), "config"); + yield fs.appendFile(configPath, `\ +[foo] + bar = ${c.value} +`); + } + const result = yield ConfigUtil.configIsTrue(repo, "foo.bar"); + assert.equal(result, c.expected); + })); + }); +}); +}); diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index c0bedfeaa..6d10d69b4 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -47,23 +47,6 @@ const UserError = require("../../lib/util/user_error"); const WriteRepoASTUtil = require("../../lib/util/write_repo_ast_util"); describe("GitUtil", function () { - describe("getConfigString", function () { - it("exists", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo("S"); - const repo = written.repo; - const configPath = path.join(repo.path(), "config"); - yield fs.appendFile(configPath, `\ -[foo] - bar = baz -`); - const config = yield repo.config(); - const goodResult = - yield GitUtil.getConfigString(config, "foo.bar"); - assert.equal(goodResult, "baz"); - const badResult = yield GitUtil.getConfigString(config, "yyy.zzz"); - assert.isNull(badResult); - })); - }); describe("getTrackingInfo", function () { const cases = { "no tracking": { From cc70f08b47cec67d8ab99e19ecce463694c215f9 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 13 Apr 2018 23:57:59 -0400 Subject: [PATCH 093/402] Added `SparseCheckoutUtil` To check for and set sparse mode Addresses https://github.com/twosigma/git-meta/projects/5#card-8956905 --- node/lib/util/sparse_checkout_util.js | 96 +++++++++++++++++++++++++ node/test/util/sparse_checkout_util.js | 97 ++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 node/lib/util/sparse_checkout_util.js create mode 100644 node/test/util/sparse_checkout_util.js diff --git a/node/lib/util/sparse_checkout_util.js b/node/lib/util/sparse_checkout_util.js new file mode 100644 index 000000000..8d74d6663 --- /dev/null +++ b/node/lib/util/sparse_checkout_util.js @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const fs = require("fs-promise"); +const NodeGit = require("nodegit"); +const path = require("path"); + +const ConfigUtil = require("./config_util"); + +/** + * This module contains methods for interacting with Git's sparse checkout + * facility, that is not supported by libgit2. + */ + +/** + * Return the path to the sparse checkout file for the specified `repo`. + * + * @param {NodeGit.Repository} repo + * @return {String} + */ +exports.getSparseCheckoutPath = function (repo) { + assert.instanceOf(repo, NodeGit.Repository); + + return path.join(repo.path(), "info", "sparse-checkout"); +}; + +/** + * Return true if the specified `repo` is in sparse mode and false otherwise. + * A repo is in sparse mode iff: `core.sparsecheckout` is true and the contents + * of `.git/info/sparse-checkout` is exactly ".gitmodules\n". We can do + * something more general purpose later if we deem it useful. + * + * @param {NodeGit.Repository} repo + * @return {Bool} + */ +exports.inSparseMode = co.wrap(function *(repo) { + assert.instanceOf(repo, NodeGit.Repository); + + if (!(yield ConfigUtil.configIsTrue(repo, "core.sparsecheckout"))) { + return false; + } + let content; + try { + content = yield fs.readFile(exports.getSparseCheckoutPath(repo), + "utf8"); + } catch (e) { + return false; // RETURN + } + return content === ".gitmodules\n"; +}); + +/** + * Configure the specified `repo` to be in sparse-checkout mode -- + * specifically, our sparse checkout mode where everything but `.gitmodules` is + * excluded. + * + * @param {NodeGit.Repository} repo + */ +exports.setSparseMode = co.wrap(function *(repo) { + assert.instanceOf(repo, NodeGit.Repository); + + const config = yield repo.config(); + yield config.setString("core.sparsecheckout", "true"); + yield fs.writeFile(exports.getSparseCheckoutPath(repo), ".gitmodules\n"); +}); diff --git a/node/test/util/sparse_checkout_util.js b/node/test/util/sparse_checkout_util.js new file mode 100644 index 000000000..135f566c7 --- /dev/null +++ b/node/test/util/sparse_checkout_util.js @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const fs = require("fs-promise"); +const path = require("path"); + +const SparseCheckoutUtil = require("../../lib/util/sparse_checkout_util"); +const TestUtil = require("../../lib/util/test_util"); + +describe("SparseCheckoutUtil", function () { +describe("getSparseCheckoutPath", function () { + it("breathing", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const result = SparseCheckoutUtil.getSparseCheckoutPath(repo); + assert.equal(result, + path.join(repo.path(), "info", "sparse-checkout")); + })); +}); +describe("inSparseMode", function () { + it("nothing configured", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const result = yield SparseCheckoutUtil.inSparseMode(repo); + assert.equal(result, false); + })); + it("configured but no file", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const config = yield repo.config(); + yield config.setString("core.sparsecheckout", "true"); + const result = yield SparseCheckoutUtil.inSparseMode(repo); + assert.equal(result, false); + })); + it("configured but wrong file", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const config = yield repo.config(); + yield config.setString("core.sparsecheckout", "true"); + yield fs.writeFile(SparseCheckoutUtil.getSparseCheckoutPath(repo), + "foo\n"); + const result = yield SparseCheckoutUtil.inSparseMode(repo); + assert.equal(result, false); + })); + it("configured good file", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const config = yield repo.config(); + yield config.setString("core.sparsecheckout", "true"); + yield fs.writeFile(SparseCheckoutUtil.getSparseCheckoutPath(repo), + ".gitmodules\n"); + const result = yield SparseCheckoutUtil.inSparseMode(repo); + assert.equal(result, true); + })); + it("good file, not configured", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + yield fs.writeFile(SparseCheckoutUtil.getSparseCheckoutPath(repo), + ".gitmodules\n"); + const result = yield SparseCheckoutUtil.inSparseMode(repo); + assert.equal(result, false); + })); +}); +describe("setSparseMode", function () { + it("breathing test", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + yield SparseCheckoutUtil.setSparseMode(repo); + const isSet = yield SparseCheckoutUtil.inSparseMode(repo); + assert.equal(isSet, true); + })); +}); +}); From 61112b261f2e348cbd86c22bede0db84e0ef50c0 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sat, 14 Apr 2018 00:42:37 -0400 Subject: [PATCH 094/402] When in sparse mode, rm submodule dir after close Addresses https://github.com/twosigma/git-meta/projects/5#card-8955901 --- node/lib/util/deinit_util.js | 25 +++++++++++------- node/test/util/deinit_util.js | 48 +++++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/node/lib/util/deinit_util.js b/node/lib/util/deinit_util.js index da5eb5e30..0729eed21 100644 --- a/node/lib/util/deinit_util.js +++ b/node/lib/util/deinit_util.js @@ -39,6 +39,14 @@ const rimraf = require("rimraf"); const path = require("path"); const fs = require("fs-promise"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); + +function doRimRaf(fileName) { + return new Promise(callback => { + return rimraf(fileName, {}, callback); + }); +} + /** * De-initialize the repository having the specified `submoduleName` in the * specified `repo`. @@ -69,15 +77,14 @@ exports.deinit = co.wrap(function *(repo, submoduleName) { const submodulePath = path.join(rootDir, submoduleName); const files = yield fs.readdir(submodulePath); - // Use 'rimraf' on each top level entry in the submodule'. - - const removeFiles = files.map(filename => { - return new Promise(callback => { - return rimraf(path.join(submodulePath, filename), {}, callback); - }); - }); - - yield removeFiles; + const sparse = yield SparseCheckoutUtil.inSparseMode(repo); + if (sparse) { + yield doRimRaf(submodulePath); + } else { + yield files.map(co.wrap(function *(filename) { + yield doRimRaf(path.join(submodulePath, filename)); + })); + } // Using a very stupid algorithm here to find and remove the submodule // entry. This logic could be smarter (maybe use regexes) and more diff --git a/node/test/util/deinit_util.js b/node/test/util/deinit_util.js index c983333f4..434a9f728 100644 --- a/node/test/util/deinit_util.js +++ b/node/test/util/deinit_util.js @@ -32,10 +32,13 @@ const assert = require("chai").assert; const co = require("co"); +const fs = require("fs-promise"); +const path = require("path"); const NodeGit = require("nodegit"); -const DeinitUtil = require("../../lib/util/deinit_util"); -const TestUtil = require("../../lib/util/test_util"); +const DeinitUtil = require("../../lib/util/deinit_util"); +const SparseCheckoutUtil = require("../../lib/util/sparse_checkout_util"); +const TestUtil = require("../../lib/util/test_util"); describe("deinit_util", function () { @@ -84,4 +87,45 @@ describe("deinit_util", function () { const closedStatus = yield NodeGit.Submodule.status(repo, "x/y", 0); assert(closedStatus & WD_UNINITIALIZED); })); + it("sparse mode", co.wrap(function *() { + + // Create and set up repos. + + const repo = yield TestUtil.createSimpleRepository(); + const baseSubRepo = yield TestUtil.createSimpleRepository(); + const baseSubPath = baseSubRepo.workdir(); + const subHead = yield baseSubRepo.getHeadCommit(); + + // Set up the submodule. + + const sub = yield NodeGit.Submodule.addSetup(repo, + baseSubPath, + "x/y", + 1); + const subRepo = yield sub.open(); + const origin = yield subRepo.getRemote("origin"); + yield origin.connect(NodeGit.Enums.DIRECTION.FETCH, + new NodeGit.RemoteCallbacks(), + function () {}); + yield subRepo.fetch("origin", {}); + subRepo.setHeadDetached(subHead.id().tostrS()); + yield sub.addFinalize(); + + // Commit the submodule it. + + yield TestUtil.makeCommit(repo, ["x/y", ".gitmodules"]); + + yield SparseCheckoutUtil.setSparseMode(repo); + yield DeinitUtil.deinit(repo, "x/y"); + + // Verify that directory is gone + const subPath = path.join(repo.workdir(), "x", "y"); + let failed = false; + try { + yield fs.readdir(subPath); + } catch (e) { + failed = true; + } + assert(failed); + })); }); From cf5110d2424c39515b352e5e650ae840d306eaec Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 6 Apr 2018 12:07:06 -0400 Subject: [PATCH 095/402] Factor cherry-pick to support merges --- node/lib/util/cherry_pick_util.js | 164 ++++++++++++++---------- node/lib/util/git_util.js | 27 ++++ node/lib/util/submodule_rebase_util.js | 4 +- node/lib/util/submodule_util.js | 35 +++-- node/lib/util/synthetic_branch_util.js | 2 +- node/test/util/cherry_pick_util.js | 42 ++++-- node/test/util/git_util.js | 31 +++++ node/test/util/rebase_util.js | 3 + node/test/util/submodule_rebase_util.js | 2 +- node/test/util/submodule_util.js | 29 ++++- 10 files changed, 243 insertions(+), 96 deletions(-) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 0e4e25582..8e36e053b 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -136,31 +136,40 @@ exports.changeSubmodules = co.wrap(function *(repo, }); /** - * Return true if the specified `commit` in the specified `repo` contains any - * URL changes and false otherwise. A URL change is an alteration to a - * submodule's URL in the `.gitmodules` file that is not an addition or - * removal. + * Return true if there are URL changes between the specified `commit` and + * `baseCommit` in the specified `repo` and false otherwise. A URL change is + * an alteration to a submodule's URL in the `.gitmodules` file that is not an + * addition or removal. If `undefined === baseCommit`, then use the first + * parent of `commit` as the base. * * @param {NodeGit.Repository} repo * @param {NodeGit.Commit} commit + * @param {NodeGit.Commit} [baseCommit] * @return {Bool} */ -exports.containsUrlChanges = co.wrap(function *(repo, commit) { +exports.containsUrlChanges = co.wrap(function *(repo, commit, baseCommit) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); + if (undefined !== baseCommit) { + assert.instanceOf(baseCommit, NodeGit.Commit); + } else { + const parents = yield commit.getParents(); + if (0 !== parents.length) { + baseCommit = parents[0]; + } + } - let parentUrls = {}; - const parents = yield commit.getParents(); - if (0 !== parents.length) { - parentUrls = - yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, parents[0]); + let baseUrls = {}; + if (undefined !== baseCommit) { + baseUrls = + yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, baseCommit); } const commitUrls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, commit); - for (let name in parentUrls) { - const parentUrl = parentUrls[name]; + for (let name in baseUrls) { + const baseUrl = baseUrls[name]; const commitUrl = commitUrls[name]; - if (undefined !== commitUrl && parentUrl !== commitUrl) { + if (undefined !== commitUrl && baseUrl !== commitUrl) { return true; // RETURN } } @@ -187,51 +196,39 @@ const getTreeEntry = co.wrap(function *(tree, path) { return null; }); -/** - * Return the tree corresponding to the merge-base for the specified `lhs` and - * `rhs` commits in the specified `repo`, or null if there is no ancestor - * commit in common between `lhs` and `rhs`. - * - * @param {NodeGit.Repository} repo - * @param {NodeGit.Commit} lhs - * @param {NodeGit.Commit} rhs - * @return {NodeGit.Tree|null} - */ -const getMergeBaseTree = co.wrap(function *(repo, lhs, rhs) { - let baseId; - try { - baseId = yield NodeGit.Merge.base(repo, lhs.id(), rhs.id()); - } catch (e) { - // only way to detect lack of base - return null; - } - const mergeBase = yield repo.getCommit(baseId); - return yield mergeBase.getTree(); -}); - /** * Determine how to apply the submodule changes introduced in the * specified `commit` to the commit on the head of the specified `repo`. * Return an object describing what changes to make, including which submodules * cannot be updated at all due to a conflicts, such as a change being - * introduced to a submodule that does not exist in HEAD. Throw a `UserError` - * if non-submodule changes are detected. + * introduced to a submodule that does not exist in HEAD. If the specified + * `fromBase` is true, comput the changes from the merge base between `commit` + * and HEAD; otherwise, compute them between `commit` and its first parent. + * Throw a `UserError` if non-submodule changes are detected. The behavior is + * undefined if there is no merge base between HEAD and `commit`. * * @param {NodeGit.Repository} repo * @param {NodeGit.Commit} commit + * @param {Bool} fromBase * @return {Object} return * @return {Object} return.changes from sub name to `SubmoduleChange` * @return {Object} return.simpleChanges from sub name to `Submodule` or null * @return {Object} return.conflicts map from sub name to `Conflict` */ -exports.computeChanges = co.wrap(function *(repo, commit) { +exports.computeChanges = co.wrap(function *(repo, commit, fromBase) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); + assert.isBoolean(fromBase); + const head = yield repo.getHeadCommit(); const headTree = yield head.getTree(); - const baseTree = yield getMergeBaseTree(repo, head, commit); + const mergeBase = yield GitUtil.getMergeBase(repo, head, commit); + assert.isNotNull(mergeBase); + const baseTree = yield mergeBase.getTree(); + const changeBase = fromBase ? mergeBase : null; const changes = yield SubmoduleUtil.getSubmoduleChanges(repo, commit, + changeBase, false); const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, commit); @@ -318,7 +315,8 @@ exports.computeChanges = co.wrap(function *(repo, commit) { * Pick the specified `subs` in the specified `metaRepo` having the specified * `metaIndex`. Stage new submodule commits in `metaRepo`. Return an object * describing any commits that were generated and conflicted commits. Use the - * specified `opener` to acces submodule repos. * + * specified `opener` to access submodule repos. + * * @param {NodeGit.Repository} metaRepo * @param {Open.Opener} opener * @param {NodeGit.Index} metaIndex @@ -387,6 +385,63 @@ Conflicting entries for submodule ${colors.red(name)} return errorMessage; }); +/** + * Throw a user error if there are URL-only changes between the specified + * `commit` and `baseCommit` in the specified `repo`. If + * `undefined === baseCommit`, compare against the first parent of `commit`. + * + * TODO: independent test + * + * TODO: Dealing with these would be a huge hassle and is probably not worth it + * at the moment since the recommended policy for monorepo implementations is + * to prevent users from making URL changes anyway. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + * @param {NodeGit.Commit} [baseCommit] + */ +exports.ensureNoURLChanges = co.wrap(function *(repo, commit, baseCommit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(commit, NodeGit.Commit); + if (undefined !== baseCommit) { + assert.instanceOf(baseCommit, NodeGit.Commit); + } + + const hasUrlChanges = + yield exports.containsUrlChanges(repo, commit, baseCommit); + if (hasUrlChanges) { + + throw new UserError(`\ +Applying commits with submodule URL changes is not currently supported. +Please try with normal git commands.`); + } +}); + +/** + * Close submodules that have been opened by the specified `opener` but that + * have no mapped commits or conflicts in the specified `changes`. + * + * TODO: independent test + * + * @param {Open.Opener} opener + * @param {Object} changes + * @param {Object} changes.commits from sub path to map from sha to sha + * @param {Object} changes.conflicts from sub path to sha causing conflict + */ +exports.closeSubs = co.wrap(function *(opener, changes) { + const repo = opener.repo; + const closeSub = co.wrap(function *(path) { + const commits = changes.commits[path]; + if ((undefined === commits || 0 === Object.keys(commits).length) && + !(path in changes.conflicts)) { + console.log(`Closing ${colors.green(path)}`); + yield DeinitUtil.deinit(repo, path); + } + }); + const opened = Array.from(yield opener.getOpenedSubs()); + DoWorkQueue.doInParallel(opened, closeSub); +}); + /** * Rewrite the specified `commit` on top of HEAD in the specified `repo` using * the specified `opener` to open submodules as needed. The behavior is @@ -407,18 +462,9 @@ exports.rewriteCommit = co.wrap(function *(repo, commit) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); - const hasUrlChanges = yield exports.containsUrlChanges(repo, commit); - if (hasUrlChanges) { - // TODO: Dealing with these would be a huge hassle and is probably not - // worth it at the moment since the recommended policy for monorepo - // implementations is to prevent users from making URL changes anyway. - - throw new UserError(`\ -Applying commits with submodule URL changes is not currently supported. -Please try with normal 'git cherry-pick'.`); - } + yield exports.ensureNoURLChanges(repo, commit); - const changes = yield exports.computeChanges(repo, commit); + const changes = yield exports.computeChanges(repo, commit, false); const index = yield repo.index(); // Perform simple changes that don't require picks -- addition, deletions, @@ -437,22 +483,10 @@ Please try with normal 'git cherry-pick'.`); const picks = yield exports.pickSubs(repo, opener, index, changes.changes); const conflicts = picks.conflicts; - // Close any subs that were unnecessarily opened (i.e., because no commit - // was generated). - - const closeSub = co.wrap(function *(path) { - const commits = picks.commits[path]; - if ((undefined === commits || 0 === Object.keys(commits).length) && - !(path in picks.conflicts)) { - console.log(`Closing ${colors.green(path)}`); - yield DeinitUtil.deinit(repo, path); - } - }); - const opened = Array.from(yield opener.getOpenedSubs()); - DoWorkQueue.doInParallel(opened, closeSub); + yield exports.closeSubs(opener, picks); Object.keys(conflicts).sort().forEach(name => { - errorMessage += `Submodule ${colors.red(name)} is conflicted.\n`; + errorMessage += SubmoduleRebaseUtil.subConflictErrorMessage(name); }); const result = { diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index ce9c1abdf..44aba66d7 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -973,3 +973,30 @@ exports.getUrlFromRemoteName = co.wrap(function *(repo, remoteName) { return yield exports.getRemoteUrl(repo, remote); } }); + +/** + * Return the merge base between the specifed `x` and `y` commits in the + * specified `repo`, or null if there is no base. + * + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} x + * @param {NodeGit.Commit} y + * @return {NodeGit.Commit|null} + */ +exports.getMergeBase = co.wrap(function *(repo, x, y) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(x, NodeGit.Commit); + assert.instanceOf(y, NodeGit.Commit); + + let baseId; + try { + baseId = yield NodeGit.Merge.base(repo, x.id(), y.id()); + } catch (e) { + // only way to detect lack of base + return null; + } + return yield repo.getCommit(baseId); +}); + + diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index b7afb2d20..85f74a7d1 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -271,7 +271,7 @@ exports.rewriteCommits = co.wrap(function *(repo, branch, upstream) { * @return {String} */ exports.subConflictErrorMessage = function (name) { - return `Conflict in ${colors.red(name)}`; + return `Submodule ${colors.red(name)} is conflicted.\n`; }; /** @@ -343,7 +343,7 @@ ${colors.green(rebaseInfo.onto)}.`); const result = yield continueRebase(subRepo); commits[name] = result.commits; if (null !== result.conflictedCommit) { - errorMessage += exports.subConflictErrorMessage(name) + "\n"; + errorMessage += exports.subConflictErrorMessage(name); } else { yield index.addByPath(name); diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index db132808b..0fab6bbc9 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -385,35 +385,47 @@ Deletion of meta-repo file ${colors.red(path)} is not supported.`); }; /** - * Return a summary of the submodule SHAs changed by the specified `commitId` + * Return a summary of the submodule SHAs changed by the specified `commit` * in the specified `repo`, and flag denoting whether or not the `.gitmodules` * file was changed. If 'commit' contains changes to the meta-repo and the - * specified 'allowMetaChanges' is not true, throw a 'UserError'. + * specified 'allowMetaChanges' is not true, throw a 'UserError'. If the + * specified `baseCommit` is provided, calculate changes between it and + * `commit`; otherwise, calculate changes between `commit` and its first + * parent. * * @asycn - * @param {NodeGit.Repository} repo - * @param {NodeGit.Commit} commit + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + * @param {NodeGit.Commit|null} baseCommit * @param {Bool} allowMetaChanges * @return {Object} map from name to `SubmoduleChange` */ exports.getSubmoduleChanges = co.wrap(function *(repo, commit, + baseCommit, allowMetaChanges) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); + if (null !== baseCommit) { + assert.instanceOf(baseCommit, NodeGit.Commit); + } assert.isBoolean(allowMetaChanges); // We calculate the changes of a commit against its first parent. If it // has no parents, then the calculation is against an empty tree. - let parentTree = null; - const parents = yield commit.getParents(); - if (0 !== parents.length) { - parentTree = yield parents[0].getTree(); + let baseTree = null; + if (null !== baseCommit) { + baseTree = yield baseCommit.getTree(); + } else { + const parents = yield commit.getParents(); + if (0 !== parents.length) { + baseTree = yield parents[0].getTree(); + } } const tree = yield commit.getTree(); - const diff = yield NodeGit.Diff.treeToTree(repo, parentTree, tree, null); + const diff = yield NodeGit.Diff.treeToTree(repo, baseTree, tree, null); return yield exports.getSubmoduleChangesFromDiff(diff, allowMetaChanges); }); @@ -720,10 +732,7 @@ exports.mergeModulesFile = co.wrap(function *(repo, const getSubs = Conf.getSubmodulesFromCommit; const fromNext = yield getSubs(repo, fromCommit); - const baseId = yield NodeGit.Merge.base(repo, - fromCommit.id(), - ontoCommit.id()); - const mergeBase = yield repo.getCommit(baseId); + const mergeBase = yield GitUtil.getMergeBase(repo, fromCommit, ontoCommit); const baseSubs = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, mergeBase); diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 4d5f06637..ade3c4c3d 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -131,7 +131,7 @@ function* checkSubmodules(repo, commit) { commit, null); const getChanges = SubmoduleUtil.getSubmoduleChanges; - const changes = yield getChanges(repo, commit, true); + const changes = yield getChanges(repo, commit, null, true); const allChanges = [ Object.keys(changes).filter(changeName => { const change = changes[changeName]; diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index 994915c66..bbb08e49e 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -211,14 +211,26 @@ describe("containsUrlChanges", function () { input: "S:C2-1 s=Sa:1;C3-2 s=Sb:1;H=3", expected: true, }, + "with ancestor": { + input: "S:C2-1 s=Sa:1;C3-2 s=Sb:1;C4-3;H=4", + expected: true, + base: "2", + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; it(caseName, co.wrap(function *() { const written = yield RepoASTTestUtil.createRepo(c.input); const repo = written.repo; + const oldCommitMap = written.oldCommitMap; + let base; + if ("base" in c) { + assert.property(oldCommitMap, c.base); + base = yield repo.getCommit(oldCommitMap[c.base]); + } const head = yield repo.getHeadCommit(); - const result = yield CherryPickUtil.containsUrlChanges(repo, head); + const result = + yield CherryPickUtil.containsUrlChanges(repo, head, base); assert.equal(result, c.expected); })); }); @@ -248,6 +260,20 @@ x=U:C3-2 s=Sa:a;Ct-2 s=Sa:b;Bt=t;Bmaster=3`, "s": new Submodule("a", "1"), }, }, + "addition in ancestor": { + input: "a=B|x=S:Ct-2 s=Sa:1;C2-1 t=Sa:1;Bt=t", + simpleChanges: { + "s": new Submodule("a", "1"), + }, + }, + "addition in ancestor, but from base": { + input: "a=B|x=S:Ct-2 s=Sa:1;C2-1 t=Sa:1;Bt=t", + simpleChanges: { + "s": new Submodule("a", "1"), + "t": new Submodule("a", "1"), + }, + fromBase: true, + }, "double addition": { input: ` a=B:Ca-1;Cb-1;Ba=a;Bb=b| @@ -306,14 +332,6 @@ x=U:C3-2 s=Sa:a;Ct-2 s;Bt=t;Bmaster=3`, null), }, }, - "change, but gone on HEAD and no ancestor": { - input: "a=B:Ca-1;Ba=a|x=S:C2 s=Sa:1;Ct-2 s=Sa:a;Bt=t", - conflicts: { - "s": new Conflict(null, - null, - new ConflictEntry(FILEMODE.COMMIT, "a")), - }, - }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; @@ -325,8 +343,10 @@ x=U:C3-2 s=Sa:a;Ct-2 s;Bt=t;Bmaster=3`, const reverseCommitMap = w.reverseCommitMap; const urlMap = w.urlMap; const target = yield repo.getCommit(reverseCommitMap.t); - const result = yield CherryPickUtil.computeChanges(repo, target); - + const fromBase = c.fromBase || false; + const result = yield CherryPickUtil.computeChanges(repo, + target, + fromBase); const changes = {}; for (let name in result.changes) { const change = result.changes[name]; diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index c0bedfeaa..c7dac2f1e 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -1188,4 +1188,35 @@ describe("GitUtil", function () { assert.equal(result.id().tostrS(), head.id().tostrS()); })); }); + + describe("getMergeBase", function () { + const cases = { + "base": { + input: "S:Cx-1;Cy-1;Bfoo=x;Bmaster=y", + expected: "1", + }, + "no base": { + input: "S:Cx-1;Cy;Bfoo=x;Bmaster=y", + expected: null, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(c.input); + const repo = written.repo; + const oldMap = written.oldCommitMap; + const x = yield repo.getCommit(oldMap.x); + const y = yield repo.getCommit(oldMap.y); + const result = yield GitUtil.getMergeBase(repo, x, y); + if (null === c.expected) { + assert.isNull(result); + } else { + assert.isNotNull(result); + const sha = written.commitMap[result.id().tostrS()]; + assert.equal(c.expected, sha); + } + })); + }); + }); }); diff --git a/node/test/util/rebase_util.js b/node/test/util/rebase_util.js index 06dfc2114..c395670b5 100644 --- a/node/test/util/rebase_util.js +++ b/node/test/util/rebase_util.js @@ -578,6 +578,9 @@ x=E:C3M-4 s=Sa:a1s,t=Sb:b1t;Q;Bmaster=3M; const continuer = makeRebaser(co.wrap(function *(repos) { const errorMessage = c.errorMessage || null; const result = yield RebaseUtil.continue(repos.x); + if (null !== result.errorMessage) { + assert.isString(result.errorMessage); + } assert.equal(result.errorMessage, errorMessage); return result; })); diff --git a/node/test/util/submodule_rebase_util.js b/node/test/util/submodule_rebase_util.js index 2dc9c261a..513d70073 100644 --- a/node/test/util/submodule_rebase_util.js +++ b/node/test/util/submodule_rebase_util.js @@ -345,7 +345,7 @@ x=E:I t=Sa:zt; Ot Czt-r z=8!H=zt!E!Bz=z; `, errorMessage: `\ -Conflict in ${colors.red("s")} +Submodule ${colors.red("s")} is conflicted. `, }, "made a commit in a sub without a rebase": { diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index f54c4d653..8decd9795 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -553,21 +553,45 @@ describe("SubmoduleUtil", function () { }, allowMetaChanges: false, }, + "added one in ancestor": { + state: "S:C3-2 w=Sa:1;C2-1 x=Sa:1;H=3", + from: "3", + result: { + "w": new SubmoduleChange(null, "1"), + }, + allowMetaChanges: false, + }, + "added one in ancestor, include base": { + state: "S:C3-2 w=Sa:1;C2-1 x=Sa:1;H=3", + from: "3", + base: "1", + result: { + "x": new SubmoduleChange(null, "1"), + "w": new SubmoduleChange(null, "1"), + }, + allowMetaChanges: false, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; it(caseName, co.wrap(function *() { const written = yield RepoASTTestUtil.createRepo(c.state); const repo = written.repo; - const fromSha = written.oldCommitMap[c.from]; + const old = written.oldCommitMap; + const fromSha = old[c.from]; const fromId = NodeGit.Oid.fromString(fromSha); const commit = yield repo.getCommit(fromId); let changes; let exception; + let baseCommit = null; + if ("base" in c) { + baseCommit = yield repo.getCommit(old[c.base]); + } try { changes = yield SubmoduleUtil.getSubmoduleChanges( repo, commit, + baseCommit, c.allowMetaChanges); } catch (e) { @@ -578,10 +602,9 @@ describe("SubmoduleUtil", function () { assert.equal(false, shouldFail); } else { - if (!(exception instanceof UserError)) { + if (!shouldFail || !(exception instanceof UserError)) { throw exception; } - assert.equal(true, shouldFail); return; // RETURN } From 3c09cc3467b1f828e71d73aefd17efd179061265 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 12 Apr 2018 13:10:54 -0400 Subject: [PATCH 096/402] Fixed detection of open submodules For some reason, a closed submodule that is conflicted does not show as "uninitialized". We're now using `IN_WD`, which seems a little clearer anyway. --- node/lib/util/read_repo_ast_util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/lib/util/read_repo_ast_util.js b/node/lib/util/read_repo_ast_util.js index 57f11c523..62a12a1c4 100644 --- a/node/lib/util/read_repo_ast_util.js +++ b/node/lib/util/read_repo_ast_util.js @@ -482,8 +482,8 @@ exports.readRAST = co.wrap(function *(repo) { for (let i = 0; i < subNames.length; ++i) { const subName = subNames[i]; const status = yield NodeGit.Submodule.status(repo, subName, 0); - const WD_UNINITIALIZED = (1 << 7); // means "closed" - if (!(status & WD_UNINITIALIZED)) { + const IN_WD = NodeGit.Submodule.STATUS.IN_WD; + if (status & IN_WD) { const sub = yield NodeGit.Submodule.lookup(repo, subName); const subRepo = yield sub.open(); const subAST = yield exports.readRAST(subRepo); From 910a2207d208e46f5fcbb4e37eec3f0669b2c75d Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sat, 7 Apr 2018 21:24:46 -0400 Subject: [PATCH 097/402] Change merge to stop using libgit2 on the metarepo Instead, we use the same technique as rebase and cherry-pick to determine what operations to perform on the submodules. --- node/lib/cmd/merge.js | 6 +- node/lib/util/merge_util.js | 383 +++++++++++++++-------------------- node/test/util/merge_util.js | 211 ++++++++++++++----- 3 files changed, 329 insertions(+), 271 deletions(-) diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index 873b0cf1a..3ef617978 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -163,7 +163,11 @@ Merge of '${args.commit}' return GitUtil.editMessage(repo, message); }; const commit = yield repo.getCommit(commitish.id()); - yield MergeUtil.merge(repo, commit, mode, args.message, editMessage); + const result = + yield MergeUtil.merge(repo, commit, mode, args.message, editMessage); + if (null !== result.errorMessage) { + throw new UserError(result.errorMessage); + } // Run post-merge hook if merge successfully. // Fixme: --squash is not supported yet, once supported, need to parse 0/1 diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index ff157a1a8..f0b7c198e 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -37,8 +37,8 @@ const colors = require("colors"); const NodeGit = require("nodegit"); const Checkout = require("./checkout"); +const CherryPickUtil = require("./cherry_pick_util"); const Commit = require("./commit"); -const DeinitUtil = require("./deinit_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); const Open = require("./open"); @@ -46,7 +46,7 @@ const RepoStatus = require("./repo_status"); const SequencerState = require("./sequencer_state"); const SequencerStateUtil = require("./sequencer_state_util"); const StatusUtil = require("./status_util"); -const SubmoduleConfigUtil = require("./submodule_config_util"); +const SubmoduleRebaseUtil = require("./submodule_rebase_util"); const SubmoduleUtil = require("./submodule_util"); const UserError = require("./user_error"); @@ -170,29 +170,110 @@ exports.fastForwardMerge = co.wrap(function *(repo, mode, commit, message) { }); /** - * Perform a fast-forward in the submodule having the specified path. Use the - * specified 'opener' to get the submodule repo, if it's open, and move it to - * the specied 'sha'; stage the change in the specified 'index'. + * Merge the specified `subs` in the specified `repo` having the specified + * `index`. Stage submodule commits in `metaRepo`. Return an object + * describing any commits that were generated and conflicted commits. Use the + * specified `opener` to acces submodule repos. Use the specified `message` to + * write commit messages. + * @param {NodeGit.Repository} metaRepo + * @param {Open.Opener} opener + * @param {NodeGit.Index} metaIndex + * @param {Object} subs map from name to SubmoduleChange + * @return {Object} + * @return {Object} return.commits map from name to map from new to old ids + * @return {Object} return.conflicts map from name to commit causing conflict */ -const doFastForward = co.wrap(function *(opener, index, path, toSha) { +const mergeSubmodules = co.wrap(function *(repo, + opener, + index, + subs, + message) { + assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(opener, Open.Opener); assert.instanceOf(index, NodeGit.Index); - assert.isString(path); - assert.isString(toSha); - console.log(`Submodule ${colors.blue(path)}: fast-forward to \ -${colors.green(toSha)}.`); - const open = yield opener.isOpen(path); - if (open) { - const repo = yield opener.getSubrepo(path); - const fetcher = yield opener.fetcher(); - yield fetcher.fetchSha(repo, path, toSha); - const commit = yield repo.getCommit(toSha); - yield NodeGit.Reset.reset(repo, commit, NodeGit.Reset.TYPE.HARD); - yield index.addByPath(path); - } - yield index.conflictRemove(path); -}); + assert.isObject(subs); + assert.isString(message); + + const result = { + conflicts: {}, + commits: {}, + }; + const sig = repo.defaultSignature(); + const fetcher = yield opener.fetcher(); + const mergeSubmodule = co.wrap(function *(name) { + const subRepo = yield opener.getSubrepo(name); + const change = subs[name]; + + const fromSha = change.newSha; + yield fetcher.fetchSha(subRepo, name, fromSha); + const subHead = yield subRepo.getHeadCommit(); + const headSha = subHead.id().tostrS(); + const fromCommit = yield subRepo.getCommit(fromSha); + + // See if up-to-date + + if (yield NodeGit.Graph.descendantOf(subRepo, headSha, fromSha)) { + return; // RETURN + } + + // See if can fast-forward + + if (yield NodeGit.Graph.descendantOf(subRepo, fromSha, headSha)) { + yield GitUtil.setHeadHard(subRepo, fromCommit); + yield index.addByPath(name); + return result; // RETURN + } + + console.log(`Submodule ${colors.blue(name)}: merging commit \ +${colors.green(fromSha)}.`); + + // Start the merge. + + let subIndex = yield NodeGit.Merge.commits(subRepo, + subHead, + fromCommit, + null); + + yield NodeGit.Checkout.index(subRepo, subIndex, { + checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, + }); + + // Abort if conflicted. + + if (subIndex.hasConflicts()) { + const seq = new SequencerState({ + type: MERGE, + originalHead: new CommitAndRef(subHead.id().tostrS(), null), + target: new CommitAndRef(fromSha, null), + currentCommit: 0, + commits: [fromSha], + message: message, + }); + yield SequencerStateUtil.writeSequencerState(subRepo.path(), seq); + result.conflicts[name] = fromSha; + return; // RETURN + } + // Otherwise, finish off the merge. + + subIndex = yield subRepo.index(); + const treeId = yield subIndex.writeTreeTo(subRepo); + const mergeCommit = yield subRepo.createCommit("HEAD", + sig, + sig, + message, + treeId, + [subHead, fromCommit]); + result.commits[name] = mergeCommit.tostrS(); + + // Clean up the conflict for this submodule and stage our change. + + yield index.addByPath(name); + yield index.conflictRemove(name); + }); + yield DoWorkQueue.doInParallel(Object.keys(subs), mergeSubmodule); + return result; +}); /** * Merge the specified `commit` in the specified `repo` having the specified @@ -204,6 +285,8 @@ ${colors.green(toSha)}.`); * to request a message. Throw a `UserError` exception if a fast-forward merge * is requested and cannot be completed. Throw a `UserError` if there are * conflicts, or if local modifications prevent the merge from happening. + * Throw a `UserError` if there are no commits in common between `commit` and + * the HEAD commit of `repo`. * * @async * @param {NodeGit.Repository} repo @@ -211,9 +294,10 @@ ${colors.green(toSha)}.`); * @param {MODE} mode * @param {String|null} commitMessage * @param {() -> Promise(String)} editMessage - * @return {Object|null} - * @return {String} return.metaCommit - * @return {Object} return.submoduleCommits map from submodule to commit + * @return {Object} + * @return {String|null} return.metaCommit + * @return {Object} return.submoduleCommits map from submodule to commit + * @return {String|null} return.errorMessage */ exports.merge = co.wrap(function *(repo, commit, @@ -228,38 +312,47 @@ exports.merge = co.wrap(function *(repo, } assert.isFunction(editMessage); - const status = yield StatusUtil.getRepoStatus(repo, { - showMetaChanges: true, - }); - - StatusUtil.ensureReady(status); - - // Cannot merge if any staged changes. + const head = yield repo.getHeadCommit(); + const baseCommit = yield GitUtil.getMergeBase(repo, commit, head); - if (!status.isIndexDeepClean()) { - throw new UserError("Cannot merge due to staged changes."); + if (null === baseCommit) { + throw new UserError(`\ +No commits in common with \ +${colors.red(GitUtil.shortSha(commit.id().tostrS()))}`); } + yield CherryPickUtil.ensureNoURLChanges(repo, commit, baseCommit); + const result = { metaCommit: null, submoduleCommits: {}, + errorMessage: null, }; - const commitSha = commit.id().tostrS(); + const status = yield StatusUtil.getRepoStatus(repo); + StatusUtil.ensureReady(status); + if (!status.isDeepClean(false)) { + // TODO: Git will refuse to run if there are staged changes, but will + // attempt a merge if there are just workdir changes. We should + // support this in the future, but it basically requires us to dry-run + // the merges in all the submodules. + + throw new UserError(`\ +The repository has uncommitted changes. Please stash or commit them before +running merge.`); + } - const head = yield repo.getHeadCommit(); + const commitSha = commit.id().tostrS(); if (head.id().tostrS() === commit.id().tostrS()) { console.log("Nothing to do."); - return null; // RETURN + return result; } const canFF = yield NodeGit.Graph.descendantOf(repo, commitSha, head.id().tostrS()); - let message = ""; - if (!canFF || MODE.FORCE_COMMIT === mode) { if (null === commitMessage) { const raw = yield editMessage(); @@ -290,179 +383,38 @@ ${colors.red(commitSha)}.`); return result; } - const sig = repo.defaultSignature(); - // Kick off the merge. It is important to note is that `Merge.commit` does - // not directly modify the working directory or index. The `index` - // object it returns is magical, virtual, does not operate on HEAD or - // anything, has no effect. - - const mergeIndex = yield SubmoduleUtil.cacheSubmodules(repo, () => { - return NodeGit.Merge.commits(repo, head, commit, null); - }); - - const checkoutOpts = { - checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE - }; - yield SubmoduleUtil.cacheSubmodules(repo, () => { - return NodeGit.Checkout.index(repo, mergeIndex, checkoutOpts); - }); + const changes = yield CherryPickUtil.computeChanges(repo, commit, true); const index = yield repo.index(); - - let errorMessage = ""; - - const subCommits = result.submoduleCommits; - const subUrls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, - head); - const headTree = yield head.getTree(); const opener = new Open.Opener(repo, null); - const subFetcher = yield opener.fetcher(); - let hasModulesFile = false; + // Perform simple changes that don't require picks -- addition, deletions, + // and fast-forwards. - const mergeEntry = co.wrap(function *(entry) { - const path = entry.path; - const stage = NodeGit.Index.entryStage(entry); - - if (path === SubmoduleConfigUtil.modulesFileName) { - hasModulesFile = true; - return; // RETURN - } - else if (RepoStatus.STAGE.THEIRS === stage && !(path in subUrls)) { - errorMessage += `Conflict in ${colors.red(path)}`; - return; // RETURN - } - - // We don't need to do anything with an entry unless it is a conflicted - // submodule. - - if (!(path in subUrls)) { - return; // RETURN - } - - const subSha = entry.id.tostrS(); - const subCommitId = NodeGit.Oid.fromString(subSha); - const subEntry = yield headTree.entryByPath(path); - const subHeadSha = subEntry.sha(); - - // If the submodule has a "normal" stage, that means it can be - // trivially fast-forwarded if there is a change. - - if (RepoStatus.STAGE.NORMAL === stage) { - if (subSha !== subHeadSha) { - yield doFastForward(opener, index, path, subSha); - } - return; // RETURN - } - - // Otherwise, if there is a conflict in the submodule, we'll handle it - // during the THEIRS entry. - - else if (RepoStatus.STAGE.THEIRS !== stage) { - return; // RETURN - } - - const subRepo = yield opener.getSubrepo(path); - - // Fetch commit to merge. - - yield subFetcher.fetchSha(subRepo, path, subSha); - - const subCommit = yield subRepo.getCommit(subCommitId); - - const upToDate = yield NodeGit.Graph.descendantOf(subRepo, - subHeadSha, - subSha); - if (upToDate) { - console.log("We're up-to-date with", path); - yield index.addByPath(path); - yield index.conflictRemove(path); - return; // RETURN - } - - const canSubFF = yield NodeGit.Graph.descendantOf(subRepo, - subSha, - subHeadSha); - if (canSubFF) { - yield doFastForward(opener, index, path, subSha); - return; // RETURN - } - - // If we already have the commit being merged in, we don't need to do - // anything. - - console.log(`Submodule ${colors.blue(path)}: merging commit \ -${colors.green(subSha)}.`); - - // Start the merge. - - const subHead = yield subRepo.getCommit(subHeadSha); - let subIndex = yield NodeGit.Merge.commits(subRepo, - subHead, - subCommit, - null); - - yield NodeGit.Checkout.index(subRepo, subIndex, { - checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, - }); + yield CherryPickUtil.changeSubmodules(repo, + opener, + index, + changes.simpleChanges); - // Abort if conflicted. + // Render any conflicts - if (subIndex.hasConflicts()) { - const seq = new SequencerState({ - type: MERGE, - originalHead: new CommitAndRef(subHead.id().tostrS(), null), - target: new CommitAndRef(subCommit.id().tostrS(), null), - currentCommit: 0, - commits: [subCommit.id().tostrS()], - message: message, - }); - yield SequencerStateUtil.writeSequencerState(subRepo.path(), seq); - errorMessage += `Submodule ${colors.red(path)} is conflicted:\n`; - const entries = subIndex.entries(); - for (let i = 0; i < entries.length; ++i) { - const subEntry = entries[i]; - const subStage = NodeGit.Index.entryStage(subEntry); - if (RepoStatus.STAGE.OURS === subStage) { - errorMessage += `\t${colors.red(subEntry.path)}\n`; - } - } - return; // RETURN - } + let errorMessage = + yield CherryPickUtil.writeConflicts(repo, index, changes.conflicts); - // Otherwise, finish off the merge. + // Then do the submodule merges - subIndex = yield subRepo.index(); - const treeId = yield subIndex.writeTreeTo(subRepo); - const mergeCommit = yield subRepo.createCommit("HEAD", - sig, - sig, - message, - treeId, - [subHead, subCommit]); - subCommits[path] = mergeCommit.tostrS(); + const merges = + yield mergeSubmodules(repo, opener, index, changes.changes, message); + result.submoduleCommits = merges.commits; + const conflicts = merges.conflicts; - // Clean up the conflict for this submodule and stage our change. + yield CherryPickUtil.closeSubs(opener, merges); - yield index.addByPath(path); - yield index.conflictRemove(path); + Object.keys(conflicts).sort().forEach(name => { + errorMessage += SubmoduleRebaseUtil.subConflictErrorMessage(name); }); - const entries = index.entries(); - yield DoWorkQueue.doInParallel(entries, mergeEntry); - - if (hasModulesFile) { - const good = yield SubmoduleUtil.mergeModulesFile(repo, head, commit); - if (!good) { - errorMessage += `Conflicting submodule additions/removals.`; - } - else { - yield index.addByPath(SubmoduleConfigUtil.modulesFileName); - yield index.conflictRemove(SubmoduleConfigUtil.modulesFileName); - } - } - // We must write the index here or the staging we've done erlier will go // away. yield index.write(); @@ -481,34 +433,23 @@ ${colors.green(subSha)}.`); message: message, }); yield SequencerStateUtil.writeSequencerState(repo.path(), seq); - throw new UserError(errorMessage); - } + result.errorMessage = errorMessage; + } else { - console.log(`Merging meta-repo commit ${colors.green(commitSha)}.`); + console.log(`Merging meta-repo commit ${colors.green(commitSha)}.`); - const id = yield index.writeTreeTo(repo); + const id = yield index.writeTreeTo(repo); - // And finally, commit it. + // And finally, commit it. - const metaCommit = yield repo.createCommit("HEAD", - sig, - sig, - message, - id, - [head, commit]); - - result.metaCommit = metaCommit.tostrS(); - - // Close subs that were opened if no commits were generated to them. - - const closeSub = co.wrap(function *(path) { - if (!(path in subCommits)) { - console.log(`Closing ${colors.green(path)} -- no commit created.`); - yield DeinitUtil.deinit(repo, path); - } - }); - const opened = Array.from(yield opener.getOpenedSubs()); - DoWorkQueue.doInParallel(opened, closeSub); + const metaCommit = yield repo.createCommit("HEAD", + sig, + sig, + message, + id, + [head, commit]); + result.metaCommit = metaCommit.tostrS(); + } return result; }); diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index 8714be8e0..baf1d2b22 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -32,6 +32,7 @@ const assert = require("chai").assert; const co = require("co"); +const colors = require("colors"); const MergeUtil = require("../../lib//util/merge_util"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); @@ -186,13 +187,46 @@ x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, const MODE = MergeUtil.MODE; const cases = { + "no merge base": { + initial: "x=S:Cx s=Sa:1;Bfoo=x", + fromCommit: "x", + fails: true, + }, + "not ready": { + initial: "x=S:QR 1: 1: 0 1", + fromCommit: "1", + fails: true, + }, + "url changes": { + initial: "a=B|b=B|x=U:C3-2 s=Sb:1;Bfoo=3", + fromCommit: "3", + fails: true, + }, + "ancestor url changes": { + initial: "a=B|b=B|x=U:C4-3 q=Sa:1;C3-2 s=Sb:1;Bfoo=4", + fromCommit: "4", + fails: true, + }, + "dirty": { + initial: "a=B|x=U:C3-1 t=Sa:1;Bfoo=3;Os W README.md=8", + fromCommit: "3", + fails: true, + }, + "dirty index": { + initial: "a=B|x=U:C3-1 t=Sa:1;Bfoo=3;Os I README.md=8", + fromCommit: "3", + fails: true, + }, "trivial -- nothing to do": { initial: "x=S", fromCommit: "1", - expected: null, + }, + "trivial -- nothing to do, has untracked change": { + initial: "a=B|x=U:Os W foo=8", + fromCommit: "2", }, "staged change": { - initial: "x=S:I foo=bar", + initial: "a=B|x=U:Os I foo=bar", fromCommit: "1", fails: true, }, @@ -206,51 +240,120 @@ x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, fromCommit: "1", fails: true, }, + "fast forward": { + initial: "a=B|x=S:C2-1 s=Sa:1;Bfoo=2", + fromCommit: "2", + expected: "a=B|x=E:Bmaster=2", + }, + "fast forward, but forced commit": { + initial: "a=B|x=S:C2-1 s=Sa:1;Bfoo=2", + fromCommit: "2", + mode: MergeUtil.MODE.FORCE_COMMIT, + expected: "a=B|x=E:Bmaster=x;Cx-1,2 s=Sa:1", + }, "one merge": { - initial: "x=S:C2-1;C3-1;Bmaster=2;Bfoo=3", - fromCommit: "3", - expected: "x=E:Cx-2,3 3=3;Bmaster=x", + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 s=Sa:s;Bmaster=x;Os Cs-a,b b=b", + }, + "one merge, but ff only": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + mode: MergeUtil.MODE.FF_ONLY, + fails: true, + }, + "one merge with ancestor": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C5-4 t=Sa:b;C4-2 s=Sa:b;Bmaster=3;Bfoo=5`, + fromCommit: "5", + expected: ` +x=E:Cx-3,5 t=Sa:b,s=Sa:s;Bmaster=x;Os Cs-a,b b=b`, }, "one merge with editor": { - initial: "x=S:C2-1;C3-1;Bmaster=2;Bfoo=3", - fromCommit: "3", + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", editMessage: () => Promise.resolve("foo\nbar\n# baz\n"), - expected: "x=E:Cfoo\nbar\n#x-2,3 3=3;Bmaster=x", + expected: ` +x=E:Cfoo\nbar\n#x-3,4 s=Sa:s;Bmaster=x;Os Cfoo\nbar\n#s-a,b b=b`, message: null, }, "one merge with empty message": { - initial: "x=S:C2-1;C3-1;Bmaster=2;Bfoo=3", - fromCommit: "3", + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", editMessage: () => Promise.resolve(""), message: null, }, - "non-ffmerge with ffwd submodule change": { + "non-ffmerge with trivial ffwd submodule change": { initial: ` -a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| -x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, +a=Aa:Cb-a;Bb=b| +x=U:C3-2 t=Sa:b;C4-2 s=Sa:b;Bmaster=3;Bfoo=4;Os`, fromCommit: "4", - expected: "x=E:Cx-3,4 s=Sa:c;Os H=c;Bmaster=x", + expected: "x=E:Cx-3,4 s=Sa:b;Os H=b;Bmaster=x", }, - "non-ffmerge with ffwd submodule change on lhs": { + "sub is same": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:b,t=Sa:b;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + expected: "x=E:Cx-3,4 t=Sa:b;Bmaster=x", + }, + "sub is same, closed": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:b,t=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 t=Sa:b;Bmaster=x", + }, + "sub is behind": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:a;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + expected: "x=E:Cx-3,4 ;Bmaster=x", + }, + "sub is behind, closed": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:a;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 ;Bmaster=x", + }, + "non-ffmerge with ffwd submodule change": { initial: ` a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| -x=U:C3-2 s=Sa:b;C4-2;Bmaster=3;Bfoo=4`, +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, fromCommit: "4", - expected: "x=E:Cx-3,4 4=4;Bmaster=x", + expected: "x=E:Cx-3,4 s=Sa:c;Os H=c;Bmaster=x", }, - "non-ffmerge with ffwd submodule change, auto-close": { + "non-ffmerge with ffwd submodule change, closed": { initial: ` a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4`, fromCommit: "4", expected: "x=E:Cx-3,4 s=Sa:c;Bmaster=x", }, - "non-ffmerge with ffwd submodule change, doesn't auto-close": { + "non-ffmerge with deeper ffwd submodule change": { + initial: ` +a=Aa:Cb-a;Bb=b;Cc-b;Cd-c;Bd=d| +x=U:C3-2 s=Sa:b;C5-4 s=Sa:d;C4-2 s=Sa:c;Bmaster=3;Bfoo=5`, + fromCommit: "5", + expected: "x=E:Cx-3,5 s=Sa:d;Bmaster=x", + }, + "non-ffmerge with ffwd submodule change on lhs": { initial: ` a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| -x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, +x=U:C3-2 s=Sa:b;C4-2 q=Sa:a;Bmaster=3;Bfoo=4`, fromCommit: "4", - expected: "x=E:Cx-3,4 s=Sa:c;Bmaster=x;Os", + expected: "x=E:Cx-3,4 q=Sa:a;Bmaster=x", }, "non-ffmerge with non-ffwd submodule change": { initial: ` @@ -269,23 +372,23 @@ x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, "submodule commit is up-to-date": { initial:` a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| -x=U:C3-2 s=Sa:c;C4-2 s=Sa:b,x=y;Bmaster=3;Bfoo=4;Os`, +x=U:C3-2 s=Sa:c;C4-2 s=Sa:b,t=Sa:a;Bmaster=3;Bfoo=4;Os`, fromCommit: "4", - expected: "x=E:Cx-3,4 x=y;Os H=c;Bmaster=x", + expected: "x=E:Cx-3,4 t=Sa:a;Os H=c;Bmaster=x", }, "submodule commit is up-to-date, was not open": { initial:` a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| -x=U:C3-2 s=Sa:c;C4-2 s=Sa:b,x=y;Bmaster=3;Bfoo=4`, +x=U:C3-2 s=Sa:c;C4-2 s=Sa:b,t=Sa:a;Bmaster=3;Bfoo=4`, fromCommit: "4", - expected: "x=E:Cx-3,4 x=y;Bmaster=x", + expected: "x=E:Cx-3,4 t=Sa:a;Bmaster=x", }, "submodule commit is same": { initial: ` a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| -x=U:C3-2 s=Sa:c;C4-2 s=Sa:c,x=y;Bmaster=3;Bfoo=4`, +x=U:C3-2 s=Sa:c;C4-2 s=Sa:c,q=Sa:a;Bmaster=3;Bfoo=4`, fromCommit: "4", - expected: "x=E:Cx-3,4 x=y;Bmaster=x", + expected: "x=E:Cx-3,4 q=Sa:a;Bmaster=x", }, "added in merge": { initial: ` @@ -303,32 +406,24 @@ x=S:C2-1 s=Sa:1;C3-1 t=Sa:1;Bmaster=2;Bfoo=3`, }, "conflicted add": { initial: ` -a=B|b=B| -x=S:C2-1 s=Sa:1;C3-1 s=Sb:1;Bmaster=2;Bfoo=3`, - fromCommit: "3", - expected: "x=E:Qmessage\n#M 2: 3: 0 3", - fails: true, - }, - "conflict in meta": { - initial: "x=S:C2-1 foo=bar;C3-1 foo=baz;Bmaster=2;Bfoo=3", +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=S:C2-1 s=Sa:a;C3-1 s=Sa:b;Bmaster=2;Bfoo=3`, fromCommit: "3", - expected: ` -x=E:Qmessage\n#M 2: 3: 0 3;I *foo=~*bar*baz;W foo=<<<<<<< ours -bar -======= -baz ->>>>>>> theirs -; + expected: `x=E:Qmessage\n#M 2: 3: 0 3;I *s=~*S:a*S:b`, + errorMessage: `\ +Conflicting entries for submodule ${colors.red("s")} `, - fails: true, }, "conflict in submodule": { initial: ` a=B:Ca-1 README.md=8;Cb-1 README.md=9;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, fromCommit: "4", + errorMessage: `\ +Submodule ${colors.red("s")} is conflicted. +`, expected: ` -x=E:Qmessage\n#M 3: 4: 0 4;I *s=S:1*S:a*S:b; +x=E:Qmessage\n#M 3: 4: 0 4; Os Qmessage\n#M a: b: 0 b!I *README.md=hello world*8*9!W README.md=\ <<<<<<< ours 8 @@ -337,7 +432,6 @@ Os Qmessage\n#M a: b: 0 b!I *README.md=hello world*8*9!W README.md=\ >>>>>>> theirs ; `, - fails: true, }, "new commit in sub in target branch but not in HEAD branch": { initial: ` @@ -356,6 +450,20 @@ x=U:C3-2 t=Sa:1;C4-3 s=Sa:a;C5-3 t=Sa:b;Bmaster=4;Bfoo=5`, expected: ` x=E:Cx-4,5 t=Sa:b;Bmaster=x` }, + "merge in a branch with a removed sub": { + initial: ` +a=B:Ca-1;Ba=a| +x=U:C3-2 t=Sa:1;C4-2 s;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: `x=E:Cx-3,4 s;Bmaster=x`, + }, + "merge to a branch with a removed sub": { + initial: ` +a=B:Ca-1;Ba=a| +x=U:C3-2 t=Sa:1;C4-2 s;Bmaster=4;Bfoo=3`, + fromCommit: "3", + expected: `x=E:Cx-4,3 t=Sa:1;Bmaster=x`, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; @@ -381,8 +489,10 @@ x=E:Cx-4,5 t=Sa:b;Bmaster=x` mode, message, editMessage); + const errorMessage = c.errorMessage || null; + assert.equal(result.errorMessage, errorMessage); if (upToDate) { - assert.isNull(result); + assert.isNull(result.metaCommit); return; // RETURN } return mapReturnedCommits(result, maps); @@ -395,9 +505,6 @@ x=E:Cx-4,5 t=Sa:b;Bmaster=x` }); }); describe("continue", function () { - // TODO: test abort from conflicts. Need conflict support to make this - // work. - const cases = { "no merge": { initial: "x=S", @@ -477,6 +584,12 @@ a=B| x=U:Qx#M 1: 1: 0 1;Os Cs-1!H=s!Bs=s`, expected: `x=E:Q;Os H=1!Cs-1!Bs=s`, }, + "from conflicts": { + initial: ` +a=B| +x=U:Qx#M 1: 1: 0 1;Os Cs-1!H=s!Bs=s!I *README.md=a*b*c`, + expected: `x=E:Q;Os H=1!Cs-1!Bs=s`, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From eb94223c557180b11cbd43dc2c6bfea85f92566d Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 15 Apr 2018 20:28:04 -0400 Subject: [PATCH 098/402] Remove dead code No longer need logic for handling conflicted modules files. --- node/lib/util/submodule_config_util.js | 76 -------------- node/lib/util/submodule_util.js | 46 --------- node/test/util/submodule_config_util.js | 130 ------------------------ node/test/util/submodule_util.js | 62 ----------- 4 files changed, 314 deletions(-) diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index d8836f600..30db75bc6 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -458,82 +458,6 @@ exports.initSubmoduleAndRepo = co.wrap(function *(repoUrl, return result; }); -/** - * Return a dictionary mapping from submodule name to URL for the describes the - * submodule state resulting from merging the specified `lhs` and `rhs` - * dictionaries, who have the specified `mergeBase` dictionary as their merge - * base; or, `null` if there is a conflict between the two that cannot be - * resolved. - * TODO: indicate which submodules are in conflict - * - * @param {Object} lhs - * @param {Object} rhs - * @param {Object} mergeBase - * @return {Object|null} - */ -exports.mergeSubmoduleConfigs = function (lhs, rhs, mergeBase) { - assert.isObject(lhs); - assert.isObject(rhs); - assert.isObject(mergeBase); - - let result = {}; - let lhsValue; - let rhsValue; - let mergeBaseValue; - - // First, loop through `lhs`. For each value, if we do not find a - // conflict, and the value hasn't been removed in `rhs`, copy it into - // `result`. - - for (let key in lhs) { - lhsValue = lhs[key]; - rhsValue = rhs[key]; - mergeBaseValue = mergeBase[key]; - - // If the value has changed between left and right, neither is the same - // as what was in the mergeBase, we have a conflict. - - if (lhsValue !== rhsValue && - rhsValue !== mergeBaseValue && - lhsValue !== mergeBaseValue) { - return null; - } - - // If the value exists in `rhs` (it wasn't deleted), or it wasn't in - // `mergeBase` (meaning it wasn't in `rhs` because `lhs` added it), - // then copy it to `result`. - - if (undefined !== rhsValue || undefined === mergeBaseValue) { - result[key] = lhsValue; - } - - } - for (let key in rhs) { - lhsValue = result[key]; // use 'result' as it may be smaller - rhsValue = rhs[key]; - mergeBaseValue = mergeBase[key]; - - // We will have a conflict only when the value doesn't exist in 'lhs' - // -- otherwise, it would have been detected already. So, a conflict - // exists when it's gone from the `lhs` (deleted), but present in the - // `rhs`, and there is a value in `mergeBase` that's different from - // `rhs`. - - if (undefined === lhsValue && - undefined !== mergeBaseValue && - rhsValue !== mergeBaseValue) { - return null; - } - - // Otherwise, we want to copy the value over if it's a change. - - else if (rhsValue !== mergeBaseValue) { - result[key] = rhsValue; - } - } - return result; -}; - /** * Return the text for a `.gitmodules` file containing the specified * `submodules` definitions. diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 0fab6bbc9..8c583f449 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -706,49 +706,3 @@ exports.cacheSubmodules = co.wrap(function *(repo, operation) { repo.submoduleCacheClear(); return result; }); - -/** - * Attempt to handle a conflicted `.gitmodules` file in the specified `repo` - * with changes from the specified `fromCommit` and `ontoCommit` commits. If - * successful, write the result to the .gitmodules file and return true; - * otherwise, return false. - * - * @param {NodeGit.Repository} repo - * @param {NodeGit.Commit} fromCommit - * @param {NodeGit.Commit} ontoCommit - * @return {Boolean} - */ -exports.mergeModulesFile = co.wrap(function *(repo, - fromCommit, - ontoCommit) { - assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(fromCommit, NodeGit.Commit); - assert.instanceOf(ontoCommit, NodeGit.Commit); - // If there is a conflict in the '.gitmodules' file, attempt to resolve it - // by comparing the current change against the original onto commit and the - // merge base between the base and onto commits. - - const Conf = SubmoduleConfigUtil; - const getSubs = Conf.getSubmodulesFromCommit; - const fromNext = yield getSubs(repo, fromCommit); - - const mergeBase = yield GitUtil.getMergeBase(repo, fromCommit, ontoCommit); - const baseSubs = - yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, mergeBase); - - const ontoSubs = yield SubmoduleConfigUtil.getSubmodulesFromCommit( - repo, - ontoCommit); - - const merged = Conf.mergeSubmoduleConfigs(fromNext, ontoSubs, baseSubs); - // If it was resolved, write out and stage the new - // modules state. - - if (null !== merged) { - const newConf = Conf.writeConfigText(merged); - yield fs.writeFile(path.join(repo.workdir(), Conf.modulesFileName), - newConf); - return true; - } - return false; -}); diff --git a/node/test/util/submodule_config_util.js b/node/test/util/submodule_config_util.js index 0a513164f..4c7902830 100644 --- a/node/test/util/submodule_config_util.js +++ b/node/test/util/submodule_config_util.js @@ -653,136 +653,6 @@ foo assert.equal(read, data); })); }); - - describe("mergeSubmoduleConfigs", function () { - const cases = { - "trivial": { - lhs: {}, - rhs: {}, - mergeBase: {}, - expected: {}, - }, - "add from left": { - lhs: { foo: "bar"}, - rhs: {}, - mergeBase: {}, - expected: { foo: "bar",}, - }, - "add from right": { - lhs: {}, - rhs: { foo: "bar"}, - mergeBase: {}, - expected: { foo: "bar",}, - }, - "removed from both": { - lhs: {}, - rhs: {}, - mergeBase: { foo: "bar" }, - expected: {}, - }, - "removed from left": { - lhs: {}, - rhs: { foo: "bar" }, - mergeBase: { foo: "bar" }, - expected: {}, - }, - "removed from right": { - lhs: { foo: "bar" }, - rhs: {}, - mergeBase: { foo: "bar" }, - expected: {}, - }, - "change from left": { - lhs: { foo: "baz" }, - rhs: { foo: "bar" }, - mergeBase: { foo: "bar"}, - expected: { foo: "baz" }, - }, - "change from right": { - lhs: { foo: "bar" }, - rhs: { foo: "baz" }, - mergeBase: { foo: "bar"}, - expected: { foo: "baz" }, - }, - "same change from both": { - lhs: { foo: "baz" }, - rhs: { foo: "baz" }, - mergeBase: { foo: "bar"}, - expected: { foo: "baz" }, - }, - "both deleted": { - lhs: {}, - rhs: {}, - mergeBase: { foo: "zot" }, - expected: {}, - }, - "conflict -- different add": { - lhs: { foo: "bar" }, - rhs: { foo: "baz" }, - mergeBase: {}, - expected: null, - }, - "conflict -- different change": { - lhs: { foo: "bar" }, - rhs: { foo: "baz" }, - mergeBase: { foo: "bak" }, - expected: null, - }, - "conflict -- lhs delete, rhs change": { - lhs: {}, - rhs: { foo: "bar" }, - mergeBase: { foo: "boo" }, - expected: null, - }, - "conflict -- lhs change, rhs delete": { - lhs: { foo: "bar" }, - rhs: {}, - mergeBase: { foo: "boo" }, - expected: null, - }, - "conflict -- both add different": { - lhs: { foo: "bar" }, - rhs: { foo: "baz" }, - mergeBase: {}, - expected: null, - }, - "multiple, arbitrary, mixed changes": { - lhs: { - foo: "bar", - baz: "bam", - ack: "woo", - wee: "meh", - }, - rhs: { - foo: "bar", - ack: "waz", - wee: "meh", - }, - mergeBase: { - foo: "gaz", - ack: "woo", - wee: "fleh", - yah: "arg", - }, - expected: { - foo: "bar", - baz: "bam", - ack: "waz", - wee: "meh", - }, - }, - }; - Object.keys(cases).forEach(caseName => { - it(caseName, function () { - const c = cases[caseName]; - const result = SubmoduleConfigUtil.mergeSubmoduleConfigs( - c.lhs, - c.rhs, - c.mergeBase); - assert.deepEqual(result, c.expected); - }); - }); - }); describe("writeConfigText", function () { const cases = { "base": {}, diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index 8decd9795..c69a93dfb 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -971,66 +971,4 @@ describe("SubmoduleUtil", function () { assert(false, "should have thrown"); })); }); - describe("mergeModulesFile", function () { - // This is largely tested in - // `SubmoduleConfigUtil.mergeSubmoduleConfigs`. - const cases = { - "trivial": { - input: "S:Cx-1;Bx=x;Cy-1;By=y", - expected: {}, - result: true, - }, - "one left": { - input: "S:Cx-1 s=Sa:1;Bx=x;Cy-1;By=y", - expected: { - s: "a", - }, - result: true, - }, - "one right": { - input: "S:Cx-1;Bx=x;Cy-1 s=Sa:1;By=y", - expected: { - s: "a", - }, - result: true, - }, - "one each": { - input: "S:Cx-1 s=Sa:1;Cy-1 t=Sa:1;By=y;Bx=x", - expected: { - s: "a", - t: "a", - }, - result: true, - }, - "conflict": { - input: ` -S:Cx-1 s=Sa:1;Cy-1 s=Sb:1;By=y;Bx=x;Cz-1 z=Sq:1;Bmaster=z`, - expected: { - z: "q", - }, - result: false, - }, - }; - Object.keys(cases).forEach(caseName => { - const c = cases[caseName]; - it(caseName, co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo(c.input); - const repo = written.repo; - const fromSha = written.oldCommitMap.x; - const ontoSha = written.oldCommitMap.y; - const from = yield repo.getCommit(fromSha); - const onto = yield repo.getCommit(ontoSha); - const result = yield SubmoduleUtil.mergeModulesFile(repo, - from, - onto); - assert.equal(result, c.result); - const configPath = path.join( - repo.workdir(), - SubmoduleConfigUtil.modulesFileName); - const data = yield fs.readFile(configPath, "utf8"); - const onDisk = SubmoduleConfigUtil.parseSubmoduleConfig(data); - assert.deepEqual(c.expected, onDisk); - })); - }); - }); }); From 23351ee85f50033c1675a56ad9629bd72082fb20 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sat, 14 Apr 2018 11:54:59 -0400 Subject: [PATCH 099/402] Created `updateHead` method It points HEAD and associated branch (if any) to a new commit. --- node/lib/util/git_util.js | 20 ++++++++++++++++++++ node/test/util/git_util.js | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 9e8db2a82..8e4fe73ed 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -966,4 +966,24 @@ exports.getMergeBase = co.wrap(function *(repo, x, y) { return yield repo.getCommit(baseId); }); +/* + * Update the HEAD of of the specified `repo` to point to the specified + * `commit`. If HEAD points to a branch, update that branch to point to + * `commit` as well, and supply the specified `reason` for the reflog. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + * @param {String} reason + */ +exports.updateHead = co.wrap(function *(repo, commit, reason) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(commit, NodeGit.Commit); + assert.isString(reason); + if (repo.headDetached()) { + repo.setHeadDetached(commit.id()); + } else { + const headRef = yield repo.head(); + yield headRef.setTarget(commit, reason); + } +}); diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index 4ffbda9a2..ee824546b 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -1171,7 +1171,6 @@ describe("GitUtil", function () { assert.equal(result.id().tostrS(), head.id().tostrS()); })); }); - describe("getMergeBase", function () { const cases = { "base": { @@ -1202,4 +1201,41 @@ describe("GitUtil", function () { })); }); }); + describe("updateHead", function () { + const cases = { + "noop": { + input: "x=S", + to: "1", + }, + "another": { + input: "x=S:C2-1;Bfoo=2", + to: "2", + expected: "x=E:Bmaster=2;I 2", + }, + "from detached": { + input: "x=S:H=1;C2-1;Bmaster=2", + to: "2", + expected: "x=E:H=2;I 2", + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + const updateHead = co.wrap(function *(repos, maps) { + const repo = repos.x; + const rev = maps.reverseCommitMap; + const commit = yield repo.getCommit(rev[c.to]); + + // TODO: test reason propagation to reflog; we don't have good + // support for this in the test facility though, and it's + // pretty hard to mess up. + + yield GitUtil.updateHead(repo, commit, "a reason"); + }); + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.input, + c.expected, + updateHead); + })); + }); + }); }); From c79c96c58a9ad7ca7c2f6208dae78b7541c612b1 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 15 Apr 2018 16:10:27 -0400 Subject: [PATCH 100/402] Remove existing config entry before adding new * Moved `deinit_util` into `submodule_config_util`. It's mostly concerned with the config file anyway, but I needed to use it from `SubmoduleConfigUtil` and didn't want to create a cycle. * Factored out a method for removing entries from the config file. * And wrote a couple of test cases for it -- it was basically untested before. --- node/lib/util/cherry_pick_util.js | 5 +- node/lib/util/close_util.js | 18 +-- node/lib/util/deinit_util.js | 130 --------------------- node/lib/util/open.js | 3 +- node/lib/util/submodule_config_util.js | 117 ++++++++++++++++++- node/test/util/close_util.js | 4 +- node/test/util/deinit_util.js | 131 --------------------- node/test/util/read_repo_ast_util.js | 13 +-- node/test/util/submodule_config_util.js | 147 +++++++++++++++++++++++- 9 files changed, 276 insertions(+), 292 deletions(-) delete mode 100644 node/lib/util/deinit_util.js delete mode 100644 node/test/util/deinit_util.js diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 8e36e053b..362b919e8 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -39,7 +39,6 @@ const path = require("path"); const rimraf = require("rimraf"); const ConflictUtil = require("./conflict_util"); -const DeinitUtil = require("./deinit_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); const Open = require("./open"); @@ -108,7 +107,7 @@ exports.changeSubmodules = co.wrap(function *(repo, for (let name in submodules) { const sub = submodules[name]; if (null === sub) { - yield DeinitUtil.deinit(repo, name); + yield SubmoduleConfigUtil.deinit(repo, name); changes[name] = null; delete urls[name]; yield rmrf(name); @@ -435,7 +434,7 @@ exports.closeSubs = co.wrap(function *(opener, changes) { if ((undefined === commits || 0 === Object.keys(commits).length) && !(path in changes.conflicts)) { console.log(`Closing ${colors.green(path)}`); - yield DeinitUtil.deinit(repo, path); + yield SubmoduleConfigUtil.deinit(repo, path); } }); const opened = Array.from(yield opener.getOpenedSubs()); diff --git a/node/lib/util/close_util.js b/node/lib/util/close_util.js index d3047868c..90210bb11 100644 --- a/node/lib/util/close_util.js +++ b/node/lib/util/close_util.js @@ -35,12 +35,12 @@ const NodeGit = require("nodegit"); const co = require("co"); const colors = require("colors"); -const DeinitUtil = require("../util/deinit_util"); -const DoWorkQueue = require("../util/do_work_queue"); -const Hook = require("../util/hook"); -const StatusUtil = require("../util/status_util"); -const SubmoduleUtil = require("../util/submodule_util"); -const UserError = require("../util/user_error"); +const DoWorkQueue = require("../util/do_work_queue"); +const Hook = require("../util/hook"); +const StatusUtil = require("../util/status_util"); +const SubmoduleConfigUtil = require("../util/submodule_config_util"); +const SubmoduleUtil = require("../util/submodule_util"); +const UserError = require("../util/user_error"); /** @@ -96,7 +96,11 @@ Pass ${colors.magenta("--force")} to close it anyway. return; // RETURN } } - yield DeinitUtil.deinit(repo, name); + // TODO: something smarter so that we're not doing an O(N^2) operation + // here. Probably not going to matter for now as few users will every + // have enough submodules open that it will cause a problem. + + yield SubmoduleConfigUtil.deinit(repo, name); subsClosedSuccessfully.push(name); }); yield DoWorkQueue.doInParallel(subsToClose, closer); diff --git a/node/lib/util/deinit_util.js b/node/lib/util/deinit_util.js deleted file mode 100644 index 0729eed21..000000000 --- a/node/lib/util/deinit_util.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (c) 2016, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -/** - * This module contains methods for de-initializing local sub repositories. - */ - -const co = require("co"); -const rimraf = require("rimraf"); -const path = require("path"); -const fs = require("fs-promise"); - -const SparseCheckoutUtil = require("./sparse_checkout_util"); - -function doRimRaf(fileName) { - return new Promise(callback => { - return rimraf(fileName, {}, callback); - }); -} - -/** - * De-initialize the repository having the specified `submoduleName` in the - * specified `repo`. - * - * @async - * @param {NodeGit.Repository} repo - * @param {String} submoduleName - */ -exports.deinit = co.wrap(function *(repo, submoduleName) { - - // This operation is a major pain, first because libgit2 does not provide - // any direct methods to do the equivalent of 'git deinit', and second - // because nodegit does not expose the method that libgit2 does provide to - // delete an entry from the config file. - - // De-initting a submodule requires the following things: - // 1. Confirms there are no unpushed (to any remote) commits - // or uncommited changes (including new files). - // 2. Remove all files under the path of the submodule, but not the - // directory itself, which would look to Git as if we were trying - // to remove the submodule. - // 3. Remove the entry for the submodule from the '.git/config' file. - // 4. Remove the directory .git/modules/ - - // We will clear out the path for the submodule. - - const rootDir = repo.workdir(); - const submodulePath = path.join(rootDir, submoduleName); - const files = yield fs.readdir(submodulePath); - - const sparse = yield SparseCheckoutUtil.inSparseMode(repo); - if (sparse) { - yield doRimRaf(submodulePath); - } else { - yield files.map(co.wrap(function *(filename) { - yield doRimRaf(path.join(submodulePath, filename)); - })); - } - - // Using a very stupid algorithm here to find and remove the submodule - // entry. This logic could be smarter (maybe use regexes) and more - // efficition (stream in and out). - - const configPath = path.join(rootDir, ".git", "config"); - const configText = fs.readFileSync(configPath, "utf8"); - const configLines = configText.split("\n"); - const newConfigLines = []; - const searchString = "[submodule \"" + submoduleName + "\"]"; - - let found = false; - let inSubmoduleConfig = false; - - // Loop through the file and push, onto 'newConfigLines' any lines that - // aren't part of the bad submodule section. - - for (let i = 0; i < configLines.length; ++i) { - let line = configLines[i]; - if (!found && !inSubmoduleConfig && line === searchString) { - inSubmoduleConfig = true; - found = true; - } - else if (inSubmoduleConfig) { - // If we see a line starting with "[" while we're in the submodule - // section, we can get out of it. - - if (0 !== line.length && line[0] === "[") { - inSubmoduleConfig = false; - } - } - if (!inSubmoduleConfig) { - newConfigLines.push(line); - } - } - - // If we didn't find the submodule, don't write the data back out. - - if (found) { - newConfigLines.push(""); // one more new line - fs.writeFileSync(configPath, newConfigLines.join("\n")); - } -}); diff --git a/node/lib/util/open.js b/node/lib/util/open.js index 0d21492b6..54d2d18b1 100644 --- a/node/lib/util/open.js +++ b/node/lib/util/open.js @@ -38,7 +38,6 @@ const colors = require("colors"); const NodeGit = require("nodegit"); const GitUtil = require("./git_util"); -const DeinitUtil = require("./deinit_util"); const SubmoduleUtil = require("./submodule_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); const SubmoduleFetcher = require("./submodule_fetcher"); @@ -88,7 +87,7 @@ exports.openOnCommit = co.wrap(function *(fetcher, yield fetcher.fetchSha(submoduleRepo, submoduleName, submoduleSha); } catch (e) { - yield DeinitUtil.deinit(metaRepo, submoduleName); + yield SubmoduleConfigUtil.deinit(metaRepo, submoduleName); throw e; } diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index 30db75bc6..51f4ade66 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -50,9 +50,17 @@ const co = require("co"); const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); +const rimraf = require("rimraf"); const url = require("url"); -const UserError = require("./user_error"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); +const UserError = require("./user_error"); + +function doRimRaf(fileName) { + return new Promise(callback => { + return rimraf(fileName, {}, callback); + }); +} const CONFIG_FILE_NAME = "config"; @@ -70,6 +78,106 @@ const CONFIG_FILE_NAME = "config"; */ exports.modulesFileName = ".gitmodules"; +/** + * Remove the first found configuration entry for the submodule having the + * specified `submoduleName` in the config file in the specified `repoPath`; do + * nothing if no entry is found. + * + * @param {String} repoPath + * @param {String} submoduleName + */ +exports.clearSubmoduleConfigEntry = + co.wrap(function *(repoPath, submoduleName) { + assert.isString(repoPath); + assert.isString(submoduleName); + + // Using a very stupid algorithm here to find and remove the submodule + // entry. This logic could be smarter (maybe use regexes) and more + // efficition (stream in and out). Note that we use only synchronous + // when mutating the config file to avoid race conditions. + + const configPath = path.join(repoPath, "config"); + const configText = fs.readFileSync(configPath, "utf8"); + const configLines = configText.split("\n"); + const newConfigLines = []; + const searchString = "[submodule \"" + submoduleName + "\"]"; + + let found = false; + let inSubmoduleConfig = false; + + // Loop through the file and push, onto 'newConfigLines' any lines that + // aren't part of the bad submodule section. + + for (const line of configLines) { + if (!found && !inSubmoduleConfig && line === searchString) { + inSubmoduleConfig = true; + found = true; + } + else if (inSubmoduleConfig) { + // If we see a line starting with "[" while we're in the submodule + // section, we can get out of it. + + if (0 !== line.length && line[0] === "[") { + inSubmoduleConfig = false; + } + } + if (!inSubmoduleConfig) { + newConfigLines.push(line); + } + } + + // If we didn't find the submodule, don't write the data back out. + + if (found) { + newConfigLines.push(""); // one more new line + fs.writeFileSync(configPath, newConfigLines.join("\n")); + } + + // Silence warning about no yield statement. + yield (Promise.resolve()); +}); + +/** + * De-initialize the repository having the specified `submoduleName` in the + * specified `repo`. + * + * @async + * @param {NodeGit.Repository} repo + * @param {String} submoduleName + */ +exports.deinit = co.wrap(function *(repo, submoduleName) { + + // This operation is a major pain, first because libgit2 does not provide + // any direct methods to do the equivalent of 'git deinit', and second + // because nodegit does not expose the method that libgit2 does provide to + // delete an entry from the config file. + + // De-initting a submodule requires the following things: + // 1. Confirms there are no unpushed (to any remote) commits + // or uncommited changes (including new files). + // 2. Remove all files under the path of the submodule, but not the + // directory itself, which would look to Git as if we were trying + // to remove the submodule. + // 3. Remove the entry for the submodule from the '.git/config' file. + // 4. Remove the directory .git/modules/ + + // We will clear out the path for the submodule. + + const rootDir = repo.workdir(); + const submodulePath = path.join(rootDir, submoduleName); + const files = yield fs.readdir(submodulePath); + + const sparse = yield SparseCheckoutUtil.inSparseMode(repo); + if (sparse) { + yield doRimRaf(submodulePath); + } else { + yield files.map(co.wrap(function *(filename) { + yield doRimRaf(path.join(submodulePath, filename)); + })); + } + yield exports.clearSubmoduleConfigEntry(repo.path(), submoduleName); +}); + /** * Return the relative path from the working directory of a submodule having * the specified `subName` to its .git directory. @@ -312,7 +420,7 @@ exports.getConfigLines = function (name, url) { /** * Write the entry to set up the the submodule having the specified `name` to - * and `url` to the `.git/config` file for the repo at the specified + * and `url` to the `config` file for the repo at the specified * `repoPath`. The behavior is undefined if there is already an entry for * `name` in the config file. * @@ -330,7 +438,8 @@ exports.initSubmodule = co.wrap(function *(repoPath, name, url) { assert.isString(repoPath); assert.isString(name); assert.isString(url); - const configPath = path.join(repoPath, ".git", CONFIG_FILE_NAME); + yield exports.clearSubmoduleConfigEntry(repoPath, name); + const configPath = path.join(repoPath, CONFIG_FILE_NAME); const lines = exports.getConfigLines(name, url); // Do this sync to avoid race conditions for now. @@ -399,7 +508,7 @@ exports.initSubmoduleAndRepo = co.wrap(function *(repoUrl, // Update the `.git/config` file. const repoPath = metaRepo.workdir(); - yield exports.initSubmodule(repoPath, name, url); + yield exports.initSubmodule(metaRepo.path(), name, url); // Then, initialize the repository. We pass `initExt` the right set of // flags so that it will set it up as a git link. diff --git a/node/test/util/close_util.js b/node/test/util/close_util.js index 3b2921d0f..f95fd7f44 100644 --- a/node/test/util/close_util.js +++ b/node/test/util/close_util.js @@ -39,8 +39,8 @@ const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); describe("close_util", function () { describe("close", function () { // Don't need to test that deinitialization works, that's tested in - // deinit_util; just need to see that we handle paths and dirty - // sumobulde situations. + // submodule_config_util; just need to see that we handle paths and + // dirty submodule situations. const cases = { "trivial": { diff --git a/node/test/util/deinit_util.js b/node/test/util/deinit_util.js deleted file mode 100644 index 434a9f728..000000000 --- a/node/test/util/deinit_util.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2016, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -const assert = require("chai").assert; -const co = require("co"); -const fs = require("fs-promise"); -const path = require("path"); -const NodeGit = require("nodegit"); - -const DeinitUtil = require("../../lib/util/deinit_util"); -const SparseCheckoutUtil = require("../../lib/util/sparse_checkout_util"); -const TestUtil = require("../../lib/util/test_util"); - -describe("deinit_util", function () { - - // Going to do a simple test here to verify that after closing a submodule: - // - // - the submodule dir contains only the `.git` line file. - // - the git repo is in a clean state - - it("breathing", co.wrap(function *() { - - // Create and set up repos. - - const repo = yield TestUtil.createSimpleRepository(); - const baseSubRepo = yield TestUtil.createSimpleRepository(); - const baseSubPath = baseSubRepo.workdir(); - const subHead = yield baseSubRepo.getHeadCommit(); - - // Set up the submodule. - - const sub = yield NodeGit.Submodule.addSetup(repo, - baseSubPath, - "x/y", - 1); - const subRepo = yield sub.open(); - const origin = yield subRepo.getRemote("origin"); - yield origin.connect(NodeGit.Enums.DIRECTION.FETCH, - new NodeGit.RemoteCallbacks(), - function () {}); - yield subRepo.fetch("origin", {}); - subRepo.setHeadDetached(subHead.id().tostrS()); - yield sub.addFinalize(); - - // Commit the submodule it. - - yield TestUtil.makeCommit(repo, ["x/y", ".gitmodules"]); - - // Verify that the status currently indicates a visible submodule. - - const addedStatus = yield NodeGit.Submodule.status(repo, "x/y", 0); - const WD_UNINITIALIZED = (1 << 7); // means "closed" - assert(!(addedStatus & WD_UNINITIALIZED)); - - // Then close it and recheck status. - - yield DeinitUtil.deinit(repo, "x/y"); - const closedStatus = yield NodeGit.Submodule.status(repo, "x/y", 0); - assert(closedStatus & WD_UNINITIALIZED); - })); - it("sparse mode", co.wrap(function *() { - - // Create and set up repos. - - const repo = yield TestUtil.createSimpleRepository(); - const baseSubRepo = yield TestUtil.createSimpleRepository(); - const baseSubPath = baseSubRepo.workdir(); - const subHead = yield baseSubRepo.getHeadCommit(); - - // Set up the submodule. - - const sub = yield NodeGit.Submodule.addSetup(repo, - baseSubPath, - "x/y", - 1); - const subRepo = yield sub.open(); - const origin = yield subRepo.getRemote("origin"); - yield origin.connect(NodeGit.Enums.DIRECTION.FETCH, - new NodeGit.RemoteCallbacks(), - function () {}); - yield subRepo.fetch("origin", {}); - subRepo.setHeadDetached(subHead.id().tostrS()); - yield sub.addFinalize(); - - // Commit the submodule it. - - yield TestUtil.makeCommit(repo, ["x/y", ".gitmodules"]); - - yield SparseCheckoutUtil.setSparseMode(repo); - yield DeinitUtil.deinit(repo, "x/y"); - - // Verify that directory is gone - const subPath = path.join(repo.workdir(), "x", "y"); - let failed = false; - try { - yield fs.readdir(subPath); - } catch (e) { - failed = true; - } - assert(failed); - })); -}); diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index b07a01c64..a793fab91 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -37,7 +37,6 @@ const NodeGit = require("nodegit"); const path = require("path"); const ConflictUtil = require("../../lib/util/conflict_util"); -const DeinitUtil = require("../../lib/util/deinit_util"); const Rebase = require("../../lib/util/rebase"); const RepoAST = require("../../lib/util/repo_ast"); const ReadRepoASTUtil = require("../../lib/util/read_repo_ast_util"); @@ -544,7 +543,7 @@ describe("readRAST", function () { const commit = yield TestUtil.makeCommit(repo, ["x/y", ".gitmodules"]); - yield DeinitUtil.deinit(repo, "x/y"); + yield SubmoduleConfigUtil.deinit(repo, "x/y"); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ changes: {"README.md":""}, @@ -592,7 +591,7 @@ describe("readRAST", function () { const index = yield repo.index(); yield index.addByPath(".gitmodules"); yield index.write(); - yield DeinitUtil.deinit(repo, "x/y"); + yield SubmoduleConfigUtil.deinit(repo, "x/y"); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ changes: {"README.md":""}, @@ -643,7 +642,7 @@ describe("readRAST", function () { const subRepo = yield submodule.open(); const anotherSubCommit = yield TestUtil.generateCommit(subRepo); const lastCommit = yield TestUtil.makeCommit(repo, ["x/y"]); - yield DeinitUtil.deinit(repo, "x/y"); + yield SubmoduleConfigUtil.deinit(repo, "x/y"); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ @@ -804,7 +803,7 @@ describe("readRAST", function () { baseSubPath, "x/y", subHead.id().tostrS()); - yield DeinitUtil.deinit(repo, "x/y"); + yield SubmoduleConfigUtil.deinit(repo, "x/y"); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ @@ -849,7 +848,7 @@ describe("readRAST", function () { const nextSubCommit = yield TestUtil.generateCommit(subRepo); const index = yield repo.index(); yield index.addAll("x/y", -1); - yield DeinitUtil.deinit(repo, "x/y"); + yield SubmoduleConfigUtil.deinit(repo, "x/y"); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ @@ -1527,7 +1526,7 @@ describe("readRAST", function () { const modules = ".gitmodules"; const nextCommit = yield TestUtil.makeCommit(repo, ["a", modules]); const nextSha = nextCommit.id().tostrS(); - yield DeinitUtil.deinit(repo, "a"); + yield SubmoduleConfigUtil.deinit(repo, "a"); yield fs.writeFile(path.join(repo.workdir(), modules), diff --git a/node/test/util/submodule_config_util.js b/node/test/util/submodule_config_util.js index 4c7902830..57927a696 100644 --- a/node/test/util/submodule_config_util.js +++ b/node/test/util/submodule_config_util.js @@ -37,13 +37,133 @@ const NodeGit = require("nodegit"); const path = require("path"); const rimraf = require("rimraf"); -const DeinitUtil = require("../../lib/util/deinit_util"); +const SparseCheckoutUtil = require("../../lib/util/sparse_checkout_util"); const SubmoduleConfigUtil = require("../../lib/util/submodule_config_util"); const TestUtil = require("../../lib/util/test_util"); const UserError = require("../../lib/util/user_error"); describe("SubmoduleConfigUtil", function () { + describe("clearSubmoduleConfigEntry", function () { + function configPath(repo) { + return path.join(repo.path(), "config"); + } + function getConfigContent(repo) { + return fs.readFileSync(configPath(repo), "utf8"); + } + it("noop", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const content = getConfigContent(repo); + yield SubmoduleConfigUtil.clearSubmoduleConfigEntry(repo.path(), + "foo"); + const result = getConfigContent(repo); + assert.equal(content, result); + })); + it("remove breathing", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const baseSubRepo = yield TestUtil.createSimpleRepository(); + const baseSubPath = baseSubRepo.workdir(); + const content = getConfigContent(repo); + yield NodeGit.Submodule.addSetup(repo, baseSubPath, "x/y", 1); + yield SubmoduleConfigUtil.clearSubmoduleConfigEntry(repo.path(), + "x/y"); + const result = getConfigContent(repo); + assert.equal(content, result); + })); + }); + + describe("deinit", function () { + + // Going to do a simple test here to verify that after closing a + // submodule: + // + // - the submodule dir contains only the `.git` line file. + // - the git repo is in a clean state + + it("breathing", co.wrap(function *() { + + // Create and set up repos. + + const repo = yield TestUtil.createSimpleRepository(); + const baseSubRepo = yield TestUtil.createSimpleRepository(); + const baseSubPath = baseSubRepo.workdir(); + const subHead = yield baseSubRepo.getHeadCommit(); + + // Set up the submodule. + + const sub = yield NodeGit.Submodule.addSetup(repo, + baseSubPath, + "x/y", + 1); + const subRepo = yield sub.open(); + const origin = yield subRepo.getRemote("origin"); + yield origin.connect(NodeGit.Enums.DIRECTION.FETCH, + new NodeGit.RemoteCallbacks(), + function () {}); + yield subRepo.fetch("origin", {}); + subRepo.setHeadDetached(subHead.id().tostrS()); + yield sub.addFinalize(); + + // Commit the submodule it. + + yield TestUtil.makeCommit(repo, ["x/y", ".gitmodules"]); + + // Verify that the status currently indicates a visible submodule. + + const addedStatus = yield NodeGit.Submodule.status(repo, "x/y", 0); + const WD_UNINITIALIZED = (1 << 7); // means "closed" + assert(!(addedStatus & WD_UNINITIALIZED)); + + // Then close it and recheck status. + + yield SubmoduleConfigUtil.deinit(repo, "x/y"); + const closedStatus = + yield NodeGit.Submodule.status(repo, "x/y", 0); + assert(closedStatus & WD_UNINITIALIZED); + })); + it("sparse mode", co.wrap(function *() { + + // Create and set up repos. + + const repo = yield TestUtil.createSimpleRepository(); + const baseSubRepo = yield TestUtil.createSimpleRepository(); + const baseSubPath = baseSubRepo.workdir(); + const subHead = yield baseSubRepo.getHeadCommit(); + + // Set up the submodule. + + const sub = yield NodeGit.Submodule.addSetup(repo, + baseSubPath, + "x/y", + 1); + const subRepo = yield sub.open(); + const origin = yield subRepo.getRemote("origin"); + yield origin.connect(NodeGit.Enums.DIRECTION.FETCH, + new NodeGit.RemoteCallbacks(), + function () {}); + yield subRepo.fetch("origin", {}); + subRepo.setHeadDetached(subHead.id().tostrS()); + yield sub.addFinalize(); + + // Commit the submodule it. + + yield TestUtil.makeCommit(repo, ["x/y", ".gitmodules"]); + + yield SparseCheckoutUtil.setSparseMode(repo); + yield SubmoduleConfigUtil.deinit(repo, "x/y"); + + // Verify that directory is gone + const subPath = path.join(repo.workdir(), "x", "y"); + let failed = false; + try { + yield fs.readdir(subPath); + } catch (e) { + failed = true; + } + assert(failed); + })); + }); + describe("computeRelativeGitDir", function () { const cases = { "simple": { @@ -459,9 +579,24 @@ describe("SubmoduleConfigUtil", function () { describe("initSubmodule", function () { it("breathing", co.wrap(function *() { const repoPath = yield TestUtil.makeTempDir(); - yield fs.mkdir(path.join(repoPath, ".git")); - const configPath = path.join(repoPath, ".git", "config"); + const configPath = path.join(repoPath, "config"); + yield fs.writeFile(configPath, "foo\n"); + yield SubmoduleConfigUtil.initSubmodule(repoPath, "xxx", "yyy"); + const data = yield fs.readFile(configPath, { + encoding: "utf8" + }); + const expected =`\ +foo +[submodule "xxx"] +\turl = yyy +`; + assert.equal(data, expected); + })); + it("already there", co.wrap(function *() { + const repoPath = yield TestUtil.makeTempDir(); + const configPath = path.join(repoPath, "config"); yield fs.writeFile(configPath, "foo\n"); + yield SubmoduleConfigUtil.initSubmodule(repoPath, "xxx", "zzz"); yield SubmoduleConfigUtil.initSubmodule(repoPath, "xxx", "yyy"); const data = yield fs.readFile(configPath, { encoding: "utf8" @@ -518,7 +653,7 @@ foo sig, sig, "my message"); - yield DeinitUtil.deinit(repo, subName); + yield SubmoduleConfigUtil.deinit(repo, subName); const repoPath = repo.workdir(); const result = yield SubmoduleConfigUtil.initSubmoduleAndRepo( originUrl, @@ -561,7 +696,7 @@ foo const sub = yield NodeGit.Submodule.lookup(repo, "foo"); const subRepo = yield sub.open(); NodeGit.Remote.setUrl(subRepo, "origin", "/bar"); - yield DeinitUtil.deinit(repo, "foo"); + yield SubmoduleConfigUtil.deinit(repo, "foo"); const newSub = yield SubmoduleConfigUtil.initSubmoduleAndRepo("", repo, @@ -629,7 +764,7 @@ foo sig, sig, "my message"); - yield DeinitUtil.deinit(repo, "foo"); + yield SubmoduleConfigUtil.deinit(repo, "foo"); // Remove `foo` dir, otherwise, we will not need to re-init the // repo and the template will not be executed. From ef2e17e876bfdf14c87f9d606f28d89ccc02e3d5 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 15 Apr 2018 20:23:27 -0400 Subject: [PATCH 101/402] Stop using `NodeGit.Reset` in the meta-repo For `Reset.reset`. Addresses https://github.com/twosigma/git-meta/projects/5#card-8956168 --- node/lib/util/rebase_util.js | 22 +--- node/lib/util/reset.js | 193 +++++++++++++++++------------ node/test/util/cherry_pick_util.js | 4 +- node/test/util/pull.js | 2 +- node/test/util/rebase_util.js | 6 +- node/test/util/reset.js | 172 +++++++++++++++---------- 6 files changed, 226 insertions(+), 173 deletions(-) diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index d45443744..d7fbbc236 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -37,7 +37,6 @@ const NodeGit = require("nodegit"); const Checkout = require("./checkout"); const CherryPickUtil = require("./cherry_pick_util"); -const DoWorkQueue = require("./do_work_queue"); const Reset = require("./reset"); const GitUtil = require("./git_util"); const Hook = require("./hook"); @@ -45,7 +44,6 @@ const SequencerState = require("./sequencer_state"); const SequencerStateUtil = require("./sequencer_state_util"); const StatusUtil = require("./status_util"); const SubmoduleRebaseUtil = require("./submodule_rebase_util"); -const SubmoduleUtil = require("./submodule_util"); const UserError = require("./user_error"); const CommitAndRef = SequencerState.CommitAndRef; @@ -237,26 +235,8 @@ exports.abort = co.wrap(function *(repo) { const seq = yield SequencerStateUtil.readSequencerState(repo.path()); ensureRebaseInProgress(seq); - - // This is a little "heavy-handed'. TODO: abort active rebases in only - // those open submodueles whose rebases are associated with the one in the - // meta-repo. It's possible (though unlikely) that the user could have an - // independent rebase going in an open submodules. - - const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo); - const abortSub = co.wrap(function *(name) { - const subRepo = yield SubmoduleUtil.getRepo(repo, name); - if (!subRepo.isRebasing()) { - return; // RETURN - } - const subRebase = yield NodeGit.Rebase.open(subRepo); - subRebase.abort(); - }); - - yield DoWorkQueue.doInParallel(openSubs, abortSub); - const commit = yield repo.getCommit(seq.originalHead.sha); - yield Reset.reset(repo, commit, Reset.TYPE.HARD); + yield Reset.reset(repo, commit, Reset.TYPE.MERGE); yield restoreHeadBranch(repo, seq); yield SequencerStateUtil.cleanSequencerState(repo.path()); }); diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index 7e0b79b45..2b4dad00c 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -32,22 +32,26 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const fs = require("fs-promise"); +const mkdirp = require("mkdirp"); const NodeGit = require("nodegit"); - -const DoWorkQueue = require("../util/do_work_queue"); -const GitUtil = require("./git_util"); -const Open = require("./open"); -const StatusUtil = require("./status_util"); -const SubmoduleFetcher = require("./submodule_fetcher"); -const SubmoduleUtil = require("./submodule_util"); -const UserError = require("./user_error"); +const path = require("path"); + +const DoWorkQueue = require("./do_work_queue"); +const GitUtil = require("./git_util"); +const Open = require("./open"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); +const StatusUtil = require("./status_util"); +const SubmoduleConfigUtil = require("./submodule_config_util"); +const SubmoduleFetcher = require("./submodule_fetcher"); +const SubmoduleUtil = require("./submodule_util"); +const UserError = require("./user_error"); const TYPE = { - SOFT: "soft", - MIXED: "mixed", - HARD: "hard", - MERGE: "merge", + SOFT: "SOFT", + MIXED: "MIXED", + HARD: "HARD", + MERGE: "MERGE", }; Object.freeze(TYPE); exports.TYPE = TYPE; @@ -58,6 +62,7 @@ exports.TYPE = TYPE; * @return {NodeGit.Reset.TYPE} */ function getType(type) { + assert.property(TYPE, type); switch (type) { case TYPE.SOFT : return NodeGit.Reset.TYPE.SOFT; case TYPE.MIXED: return NodeGit.Reset.TYPE.MIXED; @@ -69,49 +74,75 @@ function getType(type) { case TYPE.MERGE: return NodeGit.Reset.TYPE.HARD; } - assert(false, `Bad type: ${type}`); } /** - * Throw a `UserError` if any open submodule as listed by the specified - * `opener` has unstaged changes and does not exist in the specified `shas`. + * Reset the specified `repo` of having specified `index` to have the contents + * of the tree of the specified `commit`. Update the `.gitmodules` file in the + * worktree to haave the same contents as in the index. If the repo is not in + * sparse mode, create empty directories for added submodules and remove the + * directories of deleted submodules as indicated in the specified `changes`. * - * @param {Open.Opener} opener - * @param {Object} shas map from submodule path to SHA + * @param {NodeGit.Repository} repo + * @param {NodeGit.Index} index + * @param {NodeGit.Commit} commit + * @param {Object} changes from path to `SubmoduleChange` */ -exports.validateClean = co.wrap(function *(opener, shas) { - assert.instanceOf(opener, Open.Opener); - assert.isObject(shas); - - const openSubs = Array.from(yield opener.getOpenSubs()); - const badSubs = []; - const checkSub = co.wrap(function *(name) { - if (name in shas) { - // If it exists in `shas` we don't need to check it - return; // RETURN +exports.resetMetaRepo = co.wrap(function *(repo, index, commit, changes) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(index, NodeGit.Index); + assert.instanceOf(commit, NodeGit.Commit); + assert.isObject(changes); + + const tree = yield commit.getTree(); + yield index.readTree(tree); + + // Render modules file + + const modulesFileName = SubmoduleConfigUtil.modulesFileName; + const modulesPath = path.join(repo.workdir(), modulesFileName); + const modulesEntry = index.getByPath(modulesFileName); + if (undefined !== modulesEntry) { + const oid = modulesEntry.id; + const blob = yield repo.getBlob(oid); + const data = blob.toString(); + yield fs.writeFile(modulesPath, data); + } else { + // If it's not in the index, remove it. + try { + yield fs.unlink(modulesPath); + } catch (e) { + if ("ENOENT" !== e.code) { + throw e; + } } - const repo = yield opener.getSubrepo(name); - const status = yield StatusUtil.getRepoStatus(repo, { - showMetaChanges: true, + } + + // Tidy up directories when not in sparse mode. We don't need to do this + // when in sparse mode because in sparse mode we have a directory for a + // submodule iff it's open. Thus, when a new submodule comes into + // existence we do not need to make a directory for it. When a submodule + // is deleted and it's not open, there's no directory to clean up; when it + // is open, we don't want to remove the directory anyway -- this is a + // behavior that libgit2 gets wrong. + + if (!(yield SparseCheckoutUtil.inSparseMode(repo))) { + const tidySub = co.wrap(function *(name) { + const change = changes[name]; + if (null === change.oldSha) { + mkdirp.sync(path.join(repo.workdir(), name)); + } else if (null === change.newSha) { + try { + yield fs.rmdir(path.join(repo.workdir(), name)); + } catch (e) { + // If we can't remove the directory, it's OK. Git warns + // here, but I think that would just be noise as most of + // the time this happens when someone rebases a change that + // created a submodule. + } + } }); - if (!status.isWorkdirClean(true)) { - badSubs.push(name); - } - }); - yield DoWorkQueue.doInParallel(openSubs, checkSub); - if (0 !== badSubs.length) { - badSubs.sort(); - let errorMessage = `\ -The following submodules with unstaged will be deleted by this reset: - -`; - for (let name of badSubs) { - errorMessage += ` ${colors.red(name)}\n`; - } - errorMessage += ` -Please stage these changes or close the submodules before proceeding. -`; - throw new UserError(errorMessage); + yield DoWorkQueue.doInParallel(Object.keys(changes), tidySub); } }); @@ -131,13 +162,14 @@ exports.reset = co.wrap(function *(repo, commit, type) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); assert.isString(type); + assert.property(TYPE, type); const head = yield repo.getHeadCommit(); const headTree = yield head.getTree(); const commitTree = yield commit.getTree(); const diff = yield NodeGit.Diff.treeToTree(repo, - commitTree, headTree, + commitTree, null); const changedSubs = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff, true); @@ -147,47 +179,39 @@ exports.reset = co.wrap(function *(repo, commit, type) { const opener = new Open.Opener(repo, null); const fetcher = yield opener.fetcher(); + const openSubsSet = yield opener.getOpenSubs(); + + yield GitUtil.updateHead(repo, commit, `reset`); + const index = yield repo.index(); + + // With a soft reset we don't need to do anything to the meta-repo. We're + // not going to touch the index or the `.gitmodules` file. + + if (TYPE.SOFT !== type) { + yield exports.resetMetaRepo(repo, index, commit, changedSubs); + } const resetType = getType(type); // Make a list of submodules to reset, including all that have been changed // between HEAD and 'commit', and all that are open. - const openSubsSet = yield opener.getOpenSubs(); - const pathsToResetSet = new Set(openSubsSet); - Object.keys(changedSubs).forEach(path => pathsToResetSet.add(path)); - const pathsToReset = Array.from(pathsToResetSet); - const shas = yield SubmoduleUtil.getSubmoduleShasForCommit(repo, - pathsToReset, - commit); - - // If doing a HARD reset, work around the problem where libgit2 may throw - // away unstaged changes in open submodules. See - // https://github.com/twosigma/git-meta/issues/509 - - if (TYPE.HARD === type) { - yield exports.validateClean(opener, shas); - } - yield SubmoduleUtil.cacheSubmodules(repo, () => { - return NodeGit.Reset.reset(repo, commit, resetType); - }); - - const index = yield repo.index(); const resetSubmodule = co.wrap(function *(name) { const change = changedSubs[name]; + + // Nothing to do if the change was an addition or deletion. + if (undefined !== change && (null === change.oldSha || null === change.newSha)) { - // If the submodule has been added or removed since 'commit', - // there's nothing to do. return; // RETURN - } - const sha = shas[name]; + } - // When doing a hard reset, we don't need to open closed submodules - // because we would be throwing away the changes anyway. + // When doing a hard or merge reset, we don't need to open closed + // submodules because we would be throwing away the changes anyway. - if (TYPE.HARD === type && !openSubsSet.has(name)) { + if ((TYPE.HARD === type || TYPE.MERGE === type) && + !openSubsSet.has(name)) { return; // RETURN } @@ -195,9 +219,15 @@ exports.reset = co.wrap(function *(repo, commit, type) { // resetting in case we don't have it. const subRepo = yield opener.getSubrepo(name); - yield fetcher.fetchSha(subRepo, name, sha); - const subCommit = yield subRepo.getCommit(sha); + // If the submodule wasn't changed, we'll just reset it to HEAD + let subCommit; + if (undefined === change) { + subCommit = yield subRepo.getHeadCommit(); + } else { + yield fetcher.fetchSha(subRepo, name, change.newSha); + subCommit = yield subRepo.getCommit(change.newSha); + } yield NodeGit.Reset.reset(subRepo, subCommit, resetType); // Set the index to have the commit to which we just set the submodule; @@ -206,7 +236,10 @@ exports.reset = co.wrap(function *(repo, commit, type) { yield index.addByPath(name); }); - yield DoWorkQueue.doInParallel(pathsToReset, resetSubmodule); + const openSubs = Array.from(openSubsSet); + const changedSubNames = Object.keys(changedSubs); + const subsToTry = Array.from(new Set(changedSubNames.concat(openSubs))); + yield DoWorkQueue.doInParallel(subsToTry, resetSubmodule); // Write the index in case we've had to stage submodule changes. yield index.write(); diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index bbb08e49e..1b8f9ed6f 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -891,12 +891,12 @@ describe("abort", function () { fails: true, }, "some changes in meta": { - input: "x=S:C2-1;Bmaster=2;QC 1: 2: 0 2", + input: "x=S:C2-1 s=S/a:1;Bmaster=2;QC 1: 2: 0 2", expected: "x=S", }, "some conflicted changes in meta": { input: ` -x=S:C2-1;Bmaster=2;QC 1: 2: 0 2;I *README.md=b*c*d;W README.md=8`, +x=S:C2-1 s=S/a:1;Bmaster=2;QC 1: 2: 0 2;I *s=~*~*s=S:1`, expected: "x=S", }, diff --git a/node/test/util/pull.js b/node/test/util/pull.js index e30548fff..0c619e2b6 100644 --- a/node/test/util/pull.js +++ b/node/test/util/pull.js @@ -66,7 +66,7 @@ describe("pull", function () { fails: true, }, "changes": { - initial: "a=B:C2-1;Bfoo=2|x=Ca", + initial: "a=B:C2-1 s=Sa:1;Bfoo=2|x=Ca", remote: "origin", source: "foo", expected: "x=E:Bmaster=2 origin/master", diff --git a/node/test/util/rebase_util.js b/node/test/util/rebase_util.js index c395670b5..e4c5ed7a2 100644 --- a/node/test/util/rebase_util.js +++ b/node/test/util/rebase_util.js @@ -190,7 +190,7 @@ describe("rebase", function () { onto: "1", }, "ffwd": { - initial: "x=S:C2-1;Bfoo=2", + initial: "x=S:C2-1 s=S/a:1;Bfoo=2", onto: "2", expected: "x=E:Bmaster=2", }, @@ -296,7 +296,7 @@ x=E:C3M-4 s=Sa:cs;Bmaster=3M;Os Ccs-bs c=c!Cbs-d b=b`, }, "rebase with ffwd changes in sub and meta": { initial: ` -a=B:Bmaster=3;C2-1 s=Sb:q;C3-2 s=Sb:r,rar=wow| +a=B:Bmaster=3;C2-1 s=Sb:q;C3-2 s=Sb:r| b=B:Cq-1;Cr-q;Bmaster=r| x=Ca:Bmaster=2;Os`, onto: "3", @@ -496,7 +496,7 @@ x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;H=4; QR 3: 4: 0 3; Os I foo=bar!Edetached HEAD,b,c!H=c!Bb=b!Bc=c`, errorMessage: `\ -Conflict in ${colors.red("s")} +Submodule ${colors.red("s")} is conflicted. `, expected: ` x=E:Os Cas-c foo=bar!H=as!Edetached HEAD,b,c!Bb=b!Bc=c!I *c=~*c*d! diff --git a/node/test/util/reset.js b/node/test/util/reset.js index 39c310b38..df30dd03b 100644 --- a/node/test/util/reset.js +++ b/node/test/util/reset.js @@ -30,61 +30,119 @@ */ "use strict"; -const assert = require("chai").assert; -const co = require("co"); -const path = require("path"); +const assert = require("chai").assert; +const co = require("co"); +const fs = require("fs-promise"); +const NodeGit = require("nodegit"); +const path = require("path"); -const Open = require("../../lib/util/open"); -const Reset = require("../../lib/util/reset"); -const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); -const UserError = require("../../lib/util/user_error"); +const Reset = require("../../lib/util/reset"); +const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const SparseCheckoutUtil = require("../../lib/util/sparse_checkout_util"); +const SubmoduleChange = require("../../lib/util/submodule_change"); +const SubmoduleConfigUtil = require("../../lib/util/submodule_config_util"); +const SubmoduleUtil = require("../../lib/util/submodule_util"); describe("reset", function () { - describe("validateClean", function () { + describe("resetMetaRepo", function () { const cases = { - "trivial": { - state: "x=S", - shas: {}, - fails: false, + "noop": { + input: "x=S", + to: "1", }, - "open and clean": { - state: "a=B|x=U:Os", - shas: {}, - fails: false, + "another": { + input: "x=S:C2-1;Bfoo=2", + to: "2", + expected: "x=E:I 2=2;W 2", }, - "open, dirty, but existent": { - state: "a=B|x=U:Os W README.md=8888", - shas: { s: "3" }, - fails: false, + "a sub": { + input: "a=B:Ca-1;Ba=a|x=U:C3 s=Sa:a;Bfoo=3", + to: "3", + expected: "x=E:I s=Sa:a,README.md;W README.md=hello world", }, - "open, dirty, and gone": { - state: "a=B|x=U:Os W README.md=8888", - shas: { t: "3" }, - fails: true, + "a new sub": { + input: "a=B|x=S:C2-1 s=Sa:1;Bfoo=2", + to: "2", + expected: "x=E:I s=Sa:1", + }, + "removed a sub": { + input: "a=B|x=U:C3-2 s;Bfoo=3", + to: "3", + expected: "x=E:I s", }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; + const updateHead = co.wrap(function *(repos, maps) { + const repo = repos.x; + const rev = maps.reverseCommitMap; + const index = yield repo.index(); + const commit = yield repo.getCommit(rev[c.to]); + const head = yield repo.getHeadCommit(); + const headTree = yield head.getTree(); + const commitTree = yield commit.getTree(); + const diff = yield NodeGit.Diff.treeToTree(repo, + headTree, + commitTree, + null); + const changes = + yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff, true); + + yield Reset.resetMetaRepo(repo, index, commit, changes); + yield index.write(); + const fromWorkdir = + yield SubmoduleConfigUtil.getSubmodulesFromWorkdir(repo); + const fromIndex = + yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + assert.deepEqual(fromIndex, fromWorkdir); + }); it(caseName, co.wrap(function *() { - const w = yield RepoASTTestUtil.createMultiRepos(c.state); - const repo = w.repos.x; - const opener = new Open.Opener(repo, null); - let exception; - try { - yield Reset.validateClean(opener, c.shas); - } catch (e) { - exception = e; - } - if (undefined === exception) { - assert.equal(false, c.fails); - } else { - if (!(exception instanceof UserError) || !c.fails) { - throw exception; - } - } + yield RepoASTTestUtil.testMultiRepoManipulator(c.input, + c.expected, + updateHead); + })); }); - }); + it("submodule directory is cleaned up", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo( + "U:C3-2 s;Bfoo=3"); + const repo = written.repo; + const rev = written.oldCommitMap; + const index = yield repo.index(); + const commit = yield repo.getCommit(rev["3"]); + const changes = { + s: new SubmoduleChange(rev["1"], null), + }; + yield Reset.resetMetaRepo(repo, index, commit, changes); + let exists = true; + try { + yield fs.stat(path.join(repo.workdir(), "s")); + } catch (e) { + exists = false; + } + assert.equal(false, exists); + })); + it("no directory creatd in sparse mode", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo( + "S:C2-1 s=S/a:1;Bfoo=2"); + const repo = written.repo; + yield SparseCheckoutUtil.setSparseMode(repo); + const rev = written.oldCommitMap; + const index = yield repo.index(); + const commit = yield repo.getCommit(rev["2"]); + const changes = { + s: new SubmoduleChange(null, rev["1"]), + }; + yield Reset.resetMetaRepo(repo, index, commit, changes); + let exists = true; + try { + yield fs.stat(path.join(repo.workdir(), "s")); + } catch (e) { + exists = false; + } + assert.equal(false, exists); + })); + }); describe("reset", function () { // We are deferring the actual reset logic to NodeGit, so we are not @@ -109,26 +167,8 @@ describe("reset", function () { to: "1", type: TYPE.HARD, }, - "meta soft": { - initial: "x=S:C2-1 README.md=aaa;Bfoo=2", - to: "2", - type: TYPE.SOFT, - expected: "x=E:Bmaster=2;I README.md=hello world", - }, - "meta mixed": { - initial: "x=S:C2-1 README.md=aaa;Bfoo=2", - to: "2", - type: TYPE.MIXED, - expected: "x=E:Bmaster=2;W README.md=hello world", - }, - "meta hard": { - initial: "x=S:C2-1 README.md=aaa;Bfoo=2", - to: "2", - type: TYPE.HARD, - expected: "x=E:Bmaster=2", - }, "unchanged sub-repo not open": { - initial: "a=B|x=U:C4-2 x=y;Bfoo=4", + initial: "a=B|x=U:C4-2 t=Sa:1;Bfoo=4", to: "4", type: TYPE.HARD, expected: "x=E:Bmaster=4" @@ -171,12 +211,6 @@ a=B:Ca-1;Bmaster=a|x=U:C3-2 s=Sa:a;Bmaster=3;Bf=3;Os`, type: TYPE.SOFT, expected: "x=E:Os H=1!I a=a;Bmaster=2", }, - "hard, submodule with changes should refuse": { - initial: `a=B|x=U:Os W README.md=888`, - to: "1", - type: TYPE.HARD, - fails: true, - }, "merge should do as HARD but not refuse": { initial: `a=B|x=U:Os W README.md=888`, to: "1", @@ -189,6 +223,12 @@ a=B:Ca-1;Bmaster=a|x=U:C3-2 s=Sa:a;Bmaster=3;Bf=3;Os`, expected: "x=E:Bmaster=1;I s=Sa:1", type: TYPE.SOFT, }, + "submodule with unchanged head but changes": { + initial: "a=B|x=U:Os W README.md=888", + to: "2", + type: TYPE.HARD, + expected: "x=E:Os", + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From 1858e1fa94d8986bef5a628cab2aafd40c90e4d9 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 17 Apr 2018 09:53:50 -0400 Subject: [PATCH 102/402] Throw error when rebase ends in conflict from pull --- node/lib/util/pull.js | 5 ++++- node/test/util/pull.js | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/node/lib/util/pull.js b/node/lib/util/pull.js index 9d07c6e84..5b3d4993f 100644 --- a/node/lib/util/pull.js +++ b/node/lib/util/pull.js @@ -81,7 +81,10 @@ ${colors.red(source)} in the remote ${colors.yellow(remoteName)}.`); const remoteCommitId = remoteBranch.target(); const remoteCommit = yield NodeGit.Commit.lookup(metaRepo, remoteCommitId); - yield RebaseUtil.rebase(metaRepo, remoteCommit, status); + const result = yield RebaseUtil.rebase(metaRepo, remoteCommit, status); + if (null !== result.errorMessage) { + throw new UserError(result.errorMessage); + } }); diff --git a/node/test/util/pull.js b/node/test/util/pull.js index 0c619e2b6..0e818232d 100644 --- a/node/test/util/pull.js +++ b/node/test/util/pull.js @@ -77,6 +77,21 @@ describe("pull", function () { source: "foo", expected: "x=E:Rorigin=a foo=1", }, + "conflict": { + initial: ` +a=B:Ca-1;Cb-1 a=8;Ba=a;Bb=b|y=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4|x=Cy`, + remote: "origin", + source: "foo", + expected: ` +x=E:H=4;QR 3:refs/heads/master 4: 0 3;Os I *a=~*8*a!Edetached HEAD,a,b! W a=\ +<<<<<<< HEAD +8 +======= +a +>>>>>>> message +;`, + fails: true, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From 92df21aa90c76528159f823a7c741899e677359f Mon Sep 17 00:00:00 2001 From: Zokir Tiliaev Date: Tue, 17 Apr 2018 20:15:32 -0400 Subject: [PATCH 103/402] Allow tracking of nested remote branch names. --- node/lib/util/checkout.js | 2 +- node/test/util/checkout.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 14b2abd0e..02a4ea2c4 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -471,7 +471,7 @@ Cannot setup tracking information; starting point is not a branch.`); // otherwise leve the remote name 'null'; if (committishBranch.isRemote()) { - const parts = committishBranch.shorthand().split("/"); + const parts = committishBranch.shorthand().split(/\/(.+)/); tracking = { remoteName: parts[0], branchName: parts[1], diff --git a/node/test/util/checkout.js b/node/test/util/checkout.js index 414227de5..987ab29b0 100644 --- a/node/test/util/checkout.js +++ b/node/test/util/checkout.js @@ -405,6 +405,21 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, }, expectedSwitchBranch: "foo", }, + "new branch(nested name) with remote tracking": { + state: "x=S:Rhar=/a managed/hey=1", + committish: "har/managed/hey", + newBranch: "foo", + track: true, + expectedSha: "1", + expectedNewBranch: { + name: "foo", + tracking: { + remoteName: "har", + branchName: "managed/hey", + }, + }, + expectedSwitchBranch: "foo", + }, "tracking on new branch but commit not a branch": { state: "x=S", committish: "1", From 3515d1cb3d7514f4817817e2f83af073938e25dd Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 18 Apr 2018 11:56:16 -0400 Subject: [PATCH 104/402] Fix bug leaving dangling `tree_entry` objects. Causing a crash on write. This is caused by a bug in nodegit, as described in the comment for this change. Github issue for Nodegit bug: https://github.com/nodegit/nodegit/issues/1333 --- node/lib/util/tree_util.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/node/lib/util/tree_util.js b/node/lib/util/tree_util.js index 916417d7f..ca43cae67 100644 --- a/node/lib/util/tree_util.js +++ b/node/lib/util/tree_util.js @@ -145,6 +145,19 @@ exports.writeTree = co.wrap(function *(repo, baseTree, changes) { const directory = exports.buildDirectoryTree(changes); + // TODO: This is a workaround for a bug in nodegit. The contract for + // libgit2's `treebuilder` is to not free the `tree_entry` objects that its + // methods return until your done with the `treebuilder` object that + // returned them -- then you must free them else leak. Nodegit, however, + // appears to be freeing them as each `tree_entry` is GC'd; this seems to + // be the normal way the bindings work and I imagine it's a mechanical + // thing. The workaround is to stick all the `tree_entry` objects we see + // into an array whose lifetime is scoped to that of this method. + // + // Nodegit issue on github: https://github.com/nodegit/nodegit/issues/1333 + + const treeEntries = []; + // This method does the real work, but assumes an already aggregated // directory structure. @@ -159,7 +172,9 @@ exports.writeTree = co.wrap(function *(repo, baseTree, changes) { builder.remove(filename); } else if (entry instanceof Change) { - yield builder.insert(filename, entry.id, entry.mode); + const inserted = + yield builder.insert(filename, entry.id, entry.mode); + treeEntries.push(inserted); } else { let subtree; @@ -174,6 +189,7 @@ exports.writeTree = co.wrap(function *(repo, baseTree, changes) { } if (null !== treeEntry) { assert(treeEntry.isTree(), `${filename} should be a tree`); + treeEntries.push(treeEntry); const treeId = treeEntry.id(); const curTree = yield repo.getTree(treeId); subtree = yield writeSubtree(curTree, entry); @@ -185,9 +201,11 @@ exports.writeTree = co.wrap(function *(repo, baseTree, changes) { builder.remove(filename); } else { - yield builder.insert(filename, - subtree.id(), - NodeGit.TreeEntry.FILEMODE.TREE); + const inserted = yield builder.insert( + filename, + subtree.id(), + NodeGit.TreeEntry.FILEMODE.TREE); + treeEntries.push(inserted); } } } From d3621861f727bd40a9327c2effe84f99e803c3fa Mon Sep 17 00:00:00 2001 From: Shihui Xiang Date: Wed, 18 Apr 2018 13:18:47 -0400 Subject: [PATCH 105/402] #63: git meta diff command --- node/lib/cmd/forward.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node/lib/cmd/forward.js b/node/lib/cmd/forward.js index d6c92ba72..0f6b44a40 100644 --- a/node/lib/cmd/forward.js +++ b/node/lib/cmd/forward.js @@ -83,6 +83,7 @@ exports.execute = co.wrap(function *(name, args) { "-C", GitUtil.getRootGitDirectory(), name, + name === "diff" ? "--submodule=diff" : "" ].concat(args); try { yield ChildProcess.spawn("git", gitArgs, { From b8587ad6c9e72e2f8f054177f5d2811bc6270d66 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 20 Apr 2018 09:43:37 -0400 Subject: [PATCH 106/402] Test framework support for sparse checkout mode. We're not trying to support general sparse checkout, just our specific mode, and we don't really care about non-submodule changes. Overview: 1. Added `sparse` to RepoAST and RepoASTUtil 2. Support for reading and writing repos with in sparse mode. 3. Shorthand support -- '%' prefixed to a repos base type indicates sparse mode. Addresses: https://github.com/twosigma/git-meta/projects/5#card-9115833 --- node/lib/util/read_repo_ast_util.js | 11 ++++- node/lib/util/repo_ast.js | 16 +++++++ node/lib/util/repo_ast_util.js | 10 +++++ node/lib/util/shorthand_parser_util.js | 36 +++++++++++++--- node/lib/util/write_repo_ast_util.js | 57 +++++++++++++++++++++---- node/test/util/read_repo_ast_util.js | 18 ++++++++ node/test/util/repo_ast.js | 25 +++++++++++ node/test/util/repo_ast_util.js | 9 +++- node/test/util/shorthand_parser_util.js | 8 ++++ node/test/util/write_repo_ast_util.js | 30 +++++++++++++ 10 files changed, 203 insertions(+), 17 deletions(-) diff --git a/node/lib/util/read_repo_ast_util.js b/node/lib/util/read_repo_ast_util.js index 62a12a1c4..c5117d3a0 100644 --- a/node/lib/util/read_repo_ast_util.js +++ b/node/lib/util/read_repo_ast_util.js @@ -44,6 +44,7 @@ const path = require("path"); const RepoAST = require("./repo_ast"); const RebaseFileUtil = require("./rebase_file_util"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); const SequencerStateUtil = require("./sequencer_state_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -425,10 +426,17 @@ exports.readRAST = co.wrap(function *(repo) { headCommitId = headCommit.id().tostrS(); } + const sparse = yield SparseCheckoutUtil.inSparseMode(repo); + if (!bare) { const current = yield loadIndexAndWorkdir(repo, headCommit); index = current.index; - workdir = current.workdir; + + // Ignore the workdir if it's sparse. + + if (!sparse) { + workdir = current.workdir; + } } // Now we can actually build the remote objects. @@ -564,5 +572,6 @@ exports.readRAST = co.wrap(function *(repo) { rebase: rebase, sequencerState: sequencer, bare: bare, + sparse: sparse, }); }); diff --git a/node/lib/util/repo_ast.js b/node/lib/util/repo_ast.js index 0e74a42c1..119ba33cb 100644 --- a/node/lib/util/repo_ast.js +++ b/node/lib/util/repo_ast.js @@ -393,6 +393,7 @@ class AST { * @param {Object} [args.openSubmodules] * @param {Rebase} [args.rebase] * @param {SequencerState} [args.sequencerState] + * @param {Boolean} [args.sparse] */ constructor(args) { if (undefined === args) { @@ -655,6 +656,13 @@ in commit ${id}.`); } } } + + this.d_sparse = false; + if ("sparse" in args) { + this.d_sparse = args.sparse; + assert.isBoolean(this.d_sparse); + } + Object.freeze(this); } @@ -753,6 +761,13 @@ in commit ${id}.`); return this.d_sequencerState; } + /** + * @property {Boolean} true if repo is sparse and false otherwise + */ + get sparse() { + return this.d_sparse; + } + /** * Accumulate the specified `changes` into the specified `dest` map. A * non-null value in `changes` overrides any existing value in `dest`; a @@ -815,6 +830,7 @@ in commit ${id}.`); sequencerState: ("sequencerState" in args) ? args.sequencerState: this.d_sequencerState, bare: ("bare" in args) ? args.bare : this.d_bare, + sparse: ("sparse" in args) ? args.sparse : this.d_sparse, }); } diff --git a/node/lib/util/repo_ast_util.js b/node/lib/util/repo_ast_util.js index e82869d7a..15fc1707f 100644 --- a/node/lib/util/repo_ast_util.js +++ b/node/lib/util/repo_ast_util.js @@ -495,6 +495,16 @@ Expected sequencer to be ${actual.sequencerState} but got \ ${expected.sequencerState}`); } + // Check sparse + + if (actual.sparse !== expected.sparse) { + if (expected.sparse) { + result.push(`Expected repository to be sparse.`); + } + else { + result.push(`Expected repository not to be sparse.`); + } + } return result; } diff --git a/node/lib/util/shorthand_parser_util.js b/node/lib/util/shorthand_parser_util.js index 8777298e8..389f2480c 100644 --- a/node/lib/util/shorthand_parser_util.js +++ b/node/lib/util/shorthand_parser_util.js @@ -56,7 +56,7 @@ const SequencerState = RepoAST.SequencerState; * * The shorthand syntax for describing a repository: * - * shorthand = [':'(';\s*')*] + * shorthand = ['%'] [':'(';\s*')*] * base repo type = 'S' | 'B' | ('C') | 'A' | 'N' * override = | | | | * | | | | @@ -155,9 +155,13 @@ const SequencerState = RepoAST.SequencerState; * the first character in the data, e.g. `^ab$` would match a file with a line * ending in `ab`. * + * If the repo type begins with a percent ('%') sign, it is configured to be + * sparse. + * * Examples: * * S -- same as RepoType.S + * %S -- A sparse repo of type 'S' * A33 -- Like S but the single commit is named 33 and * contains a file named 33 with the contents 33. * S:H= -- removes head, bare repo @@ -387,6 +391,7 @@ function prepareASTArguments(baseAST, rawRepo) { rebase: baseAST.rebase, sequencerState: baseAST.sequencerState, bare: baseAST.bare, + sparse: baseAST.sparse, }; // Process HEAD. @@ -502,6 +507,10 @@ function prepareASTArguments(baseAST, rawRepo) { resultArgs.sequencerState = rawRepo.sequencerState; } + if ("sparse" in rawRepo) { + resultArgs.sparse = rawRepo.sparse; + } + return resultArgs; } @@ -1088,17 +1097,31 @@ exports.parseRepoShorthandRaw = function (shorthand) { // Manually process the base repository type; there is only one and it must // be at the beginning. - let typeEnd = findChar(shorthand, ":", 0, shorthand.length) || + let sparse = false; + let typeBegin = 0; + + if ("%" === shorthand[0]) { + typeBegin = 1; + sparse = true; + } + + const typeEnd = findChar(shorthand, ":", typeBegin, shorthand.length) || shorthand.length; - let typeStr = shorthand[0]; - let typeData = - (typeEnd > 1) ? shorthand.substr(1, typeEnd - 1) : undefined; + const typeStr = shorthand[typeBegin]; + const dataBegin = typeBegin + 1; + const typeData = (typeEnd - typeBegin > 1) ? + shorthand.substr(dataBegin, typeEnd - dataBegin) : + undefined; - let result = { + const result = { type: typeStr, }; + if (sparse) { + result.sparse = sparse; + } + // If there is data after the base type description, recurse. let overridesBegin = typeEnd; @@ -1116,7 +1139,6 @@ exports.parseRepoShorthandRaw = function (shorthand) { if (undefined !== typeData) { result.typeData = typeData; } - return result; }; diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index f6da92b9a..89c882d14 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -51,6 +51,7 @@ const RebaseFileUtil = require("./rebase_file_util"); const RepoAST = require("./repo_ast"); const RepoASTUtil = require("./repo_ast_util"); const SequencerStateUtil = require("./sequencer_state_util"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); const TestUtil = require("./test_util"); const TreeUtil = require("./tree_util"); @@ -332,7 +333,6 @@ const writeAllCommits = co.wrap(function *(repo, commits, treeCache) { * @param {Object} treeCache map of tree entries */ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { - const makeConflictEntry = co.wrap(function *(data) { assert.instanceOf(repo, NodeGit.Repository); if (null === data) { @@ -408,14 +408,47 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { } } else if (null !== ast.currentBranchName) { - const currentBranch = - yield repo.getBranch("refs/heads/" + ast.currentBranchName); - yield repo.checkoutBranch(currentBranch); + // If we use NodeGit to checkout, it will not respect the + // sparse-checkout settings. + + const checkoutStr = `\ +git -C '${repo.workdir()}' checkout ${ast.currentBranchName} +`; + try { + yield exec(checkoutStr); + } catch (e) { + // This can fail if there is no .gitmodules file to checkout and + // it's sparse. Git will complain that it cannot do the checkout + // because the worktree is empty. + } } else if (null !== ast.head) { - const headCommit = yield repo.getCommit(newHeadSha); - repo.setHeadDetached(newHeadSha); - yield NodeGit.Reset.reset(repo, headCommit, NodeGit.Reset.TYPE.HARD); + // If we use NodeGit to do the reset, it will not respect the sparsery + // of the repository, so we use plain Git. + + repo.detachHead(); + const resetStr = `\ +git -C '${repo.workdir()}' reset --hard ${newHeadSha} +`; + try { + yield exec(resetStr); + } catch (e) { + // This can fail if there is no .gitmodules file to checkout and + // it's sparse. Git will complain that it cannot do the checkout + // because the worktree is empty. + } + repo.detachHead(); + + // Git makes a master branch when we don't have one (even though there + // is nothing in the documentation for `reset` that says it will do + // so). So, we remove it. + + if (!("master" in ast.branches)) { + // Here, Git has created a master branch we didn't ask for; remove + // it. + + NodeGit.Reference.remove(repo, "refs/heads/master"); + } } const notes = ast.notes; @@ -506,9 +539,10 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { // not. I didn't see anything about `clean` and `Checkout.index` // didn't seem to work.. + const filesStr = ast.sparse ? "-- .gitmodules" : "-a"; const checkoutIndexStr = `\ git -C '${repo.workdir()}' clean -f -d -git -C '${repo.workdir()}' checkout-index -a -f +git -C '${repo.workdir()}' checkout-index -f ${filesStr} `; yield exec(checkoutIndexStr); @@ -624,6 +658,10 @@ exports.writeRAST = co.wrap(function *(ast, path) { const repo = yield NodeGit.Repository.init(path, ast.bare ? 1 : 0); + if (ast.sparse) { + yield SparseCheckoutUtil.setSparseMode(repo); + } + yield configRepo(repo); const treeCache = {}; @@ -780,6 +818,9 @@ git -C '${repo.path()}' -c gc.reflogExpire=0 -c gc.reflogExpireUnreachable=0 \ repoPath, { bare: ast.bare ? 1 : 0 }); + if (ast.sparse) { + yield SparseCheckoutUtil.setSparseMode(repo); + } yield configRepo(repo); yield writeRepo(repo, ast, repoPath); resultRepos[repoName] = repo; diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index a793fab91..18c0b283a 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -43,6 +43,7 @@ const ReadRepoASTUtil = require("../../lib/util/read_repo_ast_util"); const RepoASTUtil = require("../../lib/util/repo_ast_util"); const SequencerState = require("../../lib/util/sequencer_state"); const SequencerStateUtil = require("../../lib/util/sequencer_state_util"); +const SparseCheckoutUtil = require("../../lib/util/sparse_checkout_util"); const SubmoduleConfigUtil = require("../../lib/util/submodule_config_util"); const TestUtil = require("../../lib/util/test_util"); @@ -1833,5 +1834,22 @@ describe("readRAST", function () { RepoASTUtil.assertEqualASTs(actual, expected); })); }); + it("sparse", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + yield SparseCheckoutUtil.setSparseMode(repo); + const result = yield ReadRepoASTUtil.readRAST(repo); + assert.equal(result.sparse, true); + })); + it("sparse ignores worktree", co.wrap(function *() { + // Unfortunately, NodeGit will view files missing from the worktree as + // modifications. We need to verify that we deal with that. + + const repo = yield TestUtil.createSimpleRepository(); + yield SparseCheckoutUtil.setSparseMode(repo); + yield fs.unlink(path.join(repo.workdir(), "README.md")); + const result = yield ReadRepoASTUtil.readRAST(repo); + assert.equal(result.sparse, true); + assert.deepEqual(result.workdir, {}); + })); }); diff --git a/node/test/util/repo_ast.js b/node/test/util/repo_ast.js index 25316c089..bf79a8099 100644 --- a/node/test/util/repo_ast.js +++ b/node/test/util/repo_ast.js @@ -275,6 +275,8 @@ const REBASE = SequencerState.TYPE.REBASE; erebase: ("rebase" in expected) ? expected.rebase : null, esequencerState: ("sequencerState" in expected) ? expected.sequencerState : null, + esparse : ("sparse" in expected) ? + expected.sparse : false, fails : fails, }; } @@ -290,6 +292,7 @@ const REBASE = SequencerState.TYPE.REBASE; rebase: null, sequencerState: null, bare: false, + sparse: false, }, undefined, false), @@ -660,6 +663,7 @@ const REBASE = SequencerState.TYPE.REBASE; currentCommit: 1, }), }, undefined, true), + "with sparse": m({ sparse: true }, { sparse: true} , false), }; Object.keys(cases).forEach(caseName => { it(caseName, function () { @@ -684,6 +688,7 @@ const REBASE = SequencerState.TYPE.REBASE; assert.deepEqual(obj.rebase, c.erebase); assert.deepEqual(obj.sequencerState, c.esequencerState); assert.equal(obj.bare, c.ebare); + assert.equal(obj.sparse, c.esparse); if (c.input) { assert.notEqual(obj.commits, c.input.commits); @@ -810,6 +815,7 @@ const REBASE = SequencerState.TYPE.REBASE; currentCommit: 0, }), bare: false, + sparse: false, }); const newArgs = { commits: { "2": new RepoAST.Commit()}, @@ -829,6 +835,7 @@ const REBASE = SequencerState.TYPE.REBASE; currentCommit: 0, }), bare: false, + sparse: false, }; const cases = { "trivial": { @@ -856,6 +863,23 @@ const REBASE = SequencerState.TYPE.REBASE; bare: true, }), }, + "sparse": { + i: { + sparse: true, + index: {}, + workdir: {}, + rebase: null, + sequencerState: null, + }, + e: new RepoAST({ + commits: { "1": new RepoAST.Commit()}, + branches: { "master": new RepoAST.Branch("1", null) }, + refs: { "a/b": "1"}, + head: "1", + currentBranchName: "master", + sparse: true, + }), + }, }; Object.keys(cases).forEach(caseName => { it(caseName, function () { @@ -873,6 +897,7 @@ const REBASE = SequencerState.TYPE.REBASE; assert.deepEqual(obj.rebase, c.e.rebase); assert.deepEqual(obj.sequencerState, c.e.sequencerState); assert.equal(obj.bare, c.e.bare); + assert.equal(obj.sparse, c.e.sparse); }); }); }); diff --git a/node/test/util/repo_ast_util.js b/node/test/util/repo_ast_util.js index 34594894d..ae5c2f6fe 100644 --- a/node/test/util/repo_ast_util.js +++ b/node/test/util/repo_ast_util.js @@ -178,6 +178,7 @@ describe("RepoAstUtil", function () { currentCommit: 0, }), bare: false, + sparse: false, }), expected: new AST({ commits: { "1": aCommit}, @@ -198,6 +199,7 @@ describe("RepoAstUtil", function () { currentCommit: 0, }), bare: false, + sparse: false, }), }, "bad bare": { @@ -643,7 +645,12 @@ describe("RepoAstUtil", function () { }), fails: true, }, - }; + "bad sparse": { + actual: new AST({ sparse: true }), + expected: new AST({ sparse: false }), + fails: true, + }, + }; Object.keys(cases).forEach((caseName) => { const c = cases[caseName]; it(caseName, function () { diff --git a/node/test/util/shorthand_parser_util.js b/node/test/util/shorthand_parser_util.js index a8ede324f..658bca090 100644 --- a/node/test/util/shorthand_parser_util.js +++ b/node/test/util/shorthand_parser_util.js @@ -158,6 +158,7 @@ describe("ShorthandParserUtil", function () { } const cases = { "just type": { i: "S", e: m({ type: "S"})}, + "sparse": { i: "%S", e: m({ type: "S", sparse: true}) }, "just another type": { i: "B", e: m({ type: "B"})}, "branch": { i: "S:Bm=2", e: m({ branches: { m: new RepoAST.Branch("2", null), }, @@ -739,6 +740,7 @@ describe("ShorthandParserUtil", function () { assert.deepEqual(r.openSubmodules, e.openSubmodules); assert.deepEqual(r.rebase, e.rebase); assert.deepEqual(r.sequencerState, e.sequencerState); + assert.equal(r.sparse, e.sparse); }); }); }); @@ -757,6 +759,12 @@ describe("ShorthandParserUtil", function () { i: "S", e: S }, + "sparse": { + i: "%S", + e: S.copy({ + sparse: true, + }), + }, "null": { i: "N", e: new RepoAST(), diff --git a/node/test/util/write_repo_ast_util.js b/node/test/util/write_repo_ast_util.js index 559b8d006..f74885692 100644 --- a/node/test/util/write_repo_ast_util.js +++ b/node/test/util/write_repo_ast_util.js @@ -32,7 +32,9 @@ const assert = require("chai").assert; const co = require("co"); +const fs = require("fs-promise"); const NodeGit = require("nodegit"); +const path = require("path"); const ReadRepoASTUtil = require("../../lib/util/read_repo_ast_util"); const RepoAST = require("../../lib/util/repo_ast"); @@ -290,6 +292,7 @@ ${sha} not in written list ${JSON.stringify(written)}`); const cases = { "simple": "S", + "sparse": "%S:H=1", "new head": "S:C2-1;H=2", "simple with branch": "S:Bfoo=1", "simple with ref": "S:Bfoo=1;Fa/b=1", @@ -369,6 +372,7 @@ S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q;H=3", describe("writeMultiRAST", function () { const cases = { "simple": "a=S", + "simple sparse": "a=%S", "bare": "a=B", "multiple": "a=B|b=Ca:C2-1;Bmaster=2", "external commit": "a=S:Bfoo=2|b=S:C2-1;Bmaster=2", @@ -454,5 +458,31 @@ S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q;H=3", } assert(false, "commit still exists"); })); + it("sparse subdir rm'd", co.wrap(function *() { + const root = yield TestUtil.makeTempDir(); + const input = "a=B|x=%U"; + const asts = ShorthandParserUtil.parseMultiRepoShorthand(input); + yield WriteRepoASTUtil.writeMultiRAST(asts, root); + let exists = true; + try { + yield fs.stat(path.join(root, "x", "s")); + } catch (e) { + exists = false; + } + assert.equal(exists, false); + })); + it("sparse subdir rm'd, detached head", co.wrap(function *() { + const root = yield TestUtil.makeTempDir(); + const input = "a=B|x=%U:H=2"; + const asts = ShorthandParserUtil.parseMultiRepoShorthand(input); + yield WriteRepoASTUtil.writeMultiRAST(asts, root); + let exists = true; + try { + yield fs.stat(path.join(root, "x", "s")); + } catch (e) { + exists = false; + } + assert.equal(exists, false); + })); }); }); From f7d2d6233c8cb63afcf7792145fc5ba1a0d46084 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 20 Apr 2018 16:26:10 -0400 Subject: [PATCH 107/402] Don't deinit changing to commit where sub is gone This is generally unexpected and confusing behavior for users (and Git doesn't do it). --- node/lib/util/cherry_pick_util.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 362b919e8..94f489696 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -75,10 +75,10 @@ function ensureCherryInProgress(seq) { * Change the specified `submodules` in the specified index. If a name maps to * a `Submodule`, update it in the specified `index` in the specified `repo` * and if that submodule is open, reset its HEAD, index, and worktree to - * reflect that commit. Otherwise, if it maps to `null`, remove it and deinit - * it if it's open. Obtain submodule repositories from the specified `opener`, - * but do not open any closed repositories. The behavior is undefined if any - * referenced submodule is open and has index or workdir modifications. + * reflect that commit. Otherwise, if it maps to `null`, remove it. Obtain + * submodule repositories from the specified `opener`, but do not open any + * closed repositories. The behavior is undefined if any referenced submodule + * is open and has index or workdir modifications. * * @param {NodeGit.Repository} repo * @param {Open.Opener} opener @@ -107,7 +107,6 @@ exports.changeSubmodules = co.wrap(function *(repo, for (let name in submodules) { const sub = submodules[name]; if (null === sub) { - yield SubmoduleConfigUtil.deinit(repo, name); changes[name] = null; delete urls[name]; yield rmrf(name); From cad6980276b574882b7215efb91b385fc50aeffc Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 20 Apr 2018 16:36:30 -0400 Subject: [PATCH 108/402] Clear empty parent directories after closing sub --- node/lib/util/submodule_config_util.js | 19 ++++++++++++++++++- node/test/util/submodule_config_util.js | 18 +++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index 51f4ade66..3aa2b4649 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -165,12 +165,29 @@ exports.deinit = co.wrap(function *(repo, submoduleName) { const rootDir = repo.workdir(); const submodulePath = path.join(rootDir, submoduleName); - const files = yield fs.readdir(submodulePath); const sparse = yield SparseCheckoutUtil.inSparseMode(repo); if (sparse) { + // Clear out submodule's contents. + yield doRimRaf(submodulePath); + + // Clear parent directories until they're all gone or we find one + // that's non-empty. + + let next = path.dirname(submoduleName); + try { + while ("." !== next) { + yield fs.rmdir(path.join(rootDir, next)); + next = path.dirname(next); + } + } catch (e) { + if ("ENOTEMPTY" !== e.code) { + throw e; + } + } } else { + const files = yield fs.readdir(submodulePath); yield files.map(co.wrap(function *(filename) { yield doRimRaf(path.join(submodulePath, filename)); })); diff --git a/node/test/util/submodule_config_util.js b/node/test/util/submodule_config_util.js index 57927a696..50550f3bf 100644 --- a/node/test/util/submodule_config_util.js +++ b/node/test/util/submodule_config_util.js @@ -136,6 +136,7 @@ describe("SubmoduleConfigUtil", function () { baseSubPath, "x/y", 1); + yield NodeGit.Submodule.addSetup(repo, baseSubPath, "x/z", 1); const subRepo = yield sub.open(); const origin = yield subRepo.getRemote("origin"); yield origin.connect(NodeGit.Enums.DIRECTION.FETCH, @@ -152,11 +153,22 @@ describe("SubmoduleConfigUtil", function () { yield SparseCheckoutUtil.setSparseMode(repo); yield SubmoduleConfigUtil.deinit(repo, "x/y"); - // Verify that directory is gone - const subPath = path.join(repo.workdir(), "x", "y"); + // Verify that directory for sub is gone + let failed = false; try { - yield fs.readdir(subPath); + yield fs.readdir(path.join(repo.workdir(), "x", "y")); + } catch (e) { + failed = true; + } + assert(failed); + + // verify we clean the root when all is gone + + failed = false; + yield SubmoduleConfigUtil.deinit(repo, "x/z"); + try { + yield fs.readdir(path.join(repo.workdir(), "x")); } catch (e) { failed = true; } From a640c99e9e37274c29e3f2cf3fc4ea8d0b3e2bb8 Mon Sep 17 00:00:00 2001 From: Zokir Tiliaev Date: Sun, 22 Apr 2018 13:01:32 -0400 Subject: [PATCH 109/402] [issue#548] Print output even if hook returns non-zero status, give hint if hook is non-executable --- node/lib/util/hook.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/node/lib/util/hook.js b/node/lib/util/hook.js index 43c36132e..038c05860 100644 --- a/node/lib/util/hook.js +++ b/node/lib/util/hook.js @@ -57,8 +57,12 @@ exports.execHook = co.wrap(function *(name, args=[]) { console.log(result.stdout); return result.stdout; } catch (e) { - // ignore the exception - // Fixme: Need to be sync with git. If the hook is not executable, - // should give user hints. + if (e.code === "EACCES") { + console.log("EACCES: Cannot execute: " + absPath); + return e.stderr; + } else if (e.stdout) { + console.log(e.stdout); + return e.stdout; + } } }); From 86f3f51dbec768fffe320a7b9cb0f5c4b32bf842 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 19 Apr 2018 11:44:32 -0400 Subject: [PATCH 110/402] Stop using `NodeGit.Checkout` in the meta-repo. Addresses: https://github.com/twosigma/git-meta/projects/5#card-8956107 Also, stopped using `Merge.base` in reset, which was unnecessary and slow. --- node/lib/util/checkout.js | 143 ++++++++++++++-------------------- node/lib/util/reset.js | 12 +-- node/test/util/checkout.js | 42 ++++++---- node/test/util/merge_util.js | 14 ++-- node/test/util/rebase_util.js | 14 +++- 5 files changed, 109 insertions(+), 116 deletions(-) diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 02a4ea2c4..3efc36b98 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -38,9 +38,10 @@ const co = require("co"); const colors = require("colors"); const NodeGit = require("nodegit"); -const DoWorkQueue = require("../util/do_work_queue"); +const DoWorkQueue = require("../util/do_work_queue"); const GitUtil = require("./git_util"); const SubmoduleFetcher = require("./submodule_fetcher"); +const Reset = require("./reset"); const RepoStatus = require("./repo_status"); const StatusUtil = require("./status_util"); const SubmoduleUtil = require("./submodule_util"); @@ -82,75 +83,46 @@ exports.findTrackingBranch = co.wrap(function *(repo, name) { * out -- the ones that are both open and also exist on `commit`. * * @param {NodeGit.Repository} repo - * @param {NodeGit.Commit} commit - * @return {Object} map from name to { repo, commit } + * @param {Object} changes map from name to `SubmoduleChange` + * @return {Object} map from name to { repo, [commit] } */ -const loadSubmodulesToCheckout = co.wrap(function *(repo, commit) { - let open = yield SubmoduleUtil.listOpenSubmodules(repo); - const openSet = new Set(open); - - const names = yield SubmoduleUtil.getSubmoduleNamesForCommit(repo, commit); - - // Condense `open` to be open submodules that are also valid submodules on - // `commit`. - - open = names.filter(name => openSet.has(name)); - - const shas = yield SubmoduleUtil.getSubmoduleShasForCommit(repo, - open, - commit); +const loadSubmodulesToCheckout = co.wrap(function *(repo, changes) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isObject(changes); + const openSet = new Set(yield SubmoduleUtil.listOpenSubmodules(repo)); + const toLoad = Object.keys(changes).filter(name => openSet.has(name)); const result = {}; - const subFetcher = new SubmoduleFetcher(repo, commit); + const head = yield repo.getHeadCommit(); + const subFetcher = new SubmoduleFetcher(repo, head); const doSub = co.wrap(function *(name) { const subRepo = yield SubmoduleUtil.getRepo(repo, name); - const sha = shas[name]; - yield subFetcher.fetchSha(subRepo, name, sha); - const commit = yield subRepo.getCommit(sha); - result[name] = { repo: subRepo, commit: commit }; + const sha = changes[name].newSha; + if (null !== sha) { + yield subFetcher.fetchSha(subRepo, name, sha); + const commit = yield subRepo.getCommit(sha); + result[name] = { repo: subRepo, commit: commit }; + } }); - yield DoWorkQueue.doInParallel(open, doSub); + yield DoWorkQueue.doInParallel(toLoad, doSub); return result; }); /** * Return a list of errors that would be encountered if a non-force attempt was - * made to check out the specified `submodules` on the specified `commit` in - * the specified `metaRepo`. + * made to check out the specified `submodules` in the specified `metaRepo`. + * * TODO: consider exposing this and testing it separately * - * @param {NodeGit.Repository} repository - * @param {NodeGit.Commit} commit - * @param {Object} submodules map from name to { repo, commit } - * @return {String []} list of errors + * @param {NodeGit.Repository} + * @param {Object} submodules map from name to {repo, commit} + * @return {String []} list of errors */ -const dryRun = co.wrap(function *(metaRepo, commit, submodules) { - let errors = []; - - /** - * If it is possible to check out the specified `commit` in the specified - * `repo`, return `null`; otherwise, return an error message. - */ - const runOne = co.wrap(function *(repo, commit) { - try { - yield NodeGit.Checkout.tree(repo, commit, { - checkoutStrategy: NodeGit.Checkout.STRATEGY.NONE, - }); - return null; // RETURN - } - catch(e) { - return e.message; // RETURN - } - }); - - // Check meta +const dryRun = co.wrap(function *(metaRepo, submodules) { + assert.instanceOf(metaRepo, NodeGit.Repository); + assert.isObject(submodules); - const metaError = yield SubmoduleUtil.cacheSubmodules( - metaRepo, - () => runOne(metaRepo, commit)); - if (null !== metaError) { - errors.push(`Unable to check out meta-repo: ${metaError}.`); - } + let errors = []; // Check for new commits in submodules @@ -191,17 +163,21 @@ Submodule ${colors.yellow(name)} has a new commit.`); // Try the submodules; store the opened repos and loaded commits for // use in the actual checkout later. - yield Object.keys(submodules).map(co.wrap(function *(name) { + const dryRunSub = co.wrap(function *(name) { const sub = submodules[name]; const repo = sub.repo; - const subCommit = sub.commit; - const error = yield runOne(repo, subCommit); - if (null !== error) { + const commit = sub.commit; + try { + yield NodeGit.Checkout.tree(repo, commit, { + checkoutStrategy: NodeGit.Checkout.STRATEGY.NONE, + }); + } + catch(e) { errors.push(`\ -Unable to checkout submodule ${colors.yellow(name)}: ${error}.`); - } - })); - +Unable to checkout submodule ${colors.yellow(name)}: ${e.message}.`); + } + }); + yield DoWorkQueue.doInParallel(Object.keys(submodules), dryRunSub); return errors; }); @@ -209,25 +185,30 @@ Unable to checkout submodule ${colors.yellow(name)}: ${error}.`); * Checkout the specified `commit` in the specified `metaRepo`, and update all * open submodules to be on the indicated commit, fetching it if necessary. * Throw a `UserError` if one of the submodules or the meta-repo cannot be - * checked out. + * checked out. On successful checkout, leave HEAD detached. * * @async * @param {NodeGit.Repository} repo * @param {NodeGit.Commit} commit * @param {Boolean} force */ -exports.checkoutCommit = co.wrap(function *(metaRepo, commit, force) { - assert.instanceOf(metaRepo, NodeGit.Repository); +exports.checkoutCommit = co.wrap(function *(repo, commit, force) { + assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); assert.isBoolean(force); - const subs = yield loadSubmodulesToCheckout(metaRepo, commit); + const head = yield repo.getHeadCommit(); + const changes = yield SubmoduleUtil.getSubmoduleChanges(repo, + commit, + head, + false); + const subs = yield loadSubmodulesToCheckout(repo, changes); // If we're not forcing the commit, attempt a dry run and fail if it // doesn't pass. if (!force) { - const errors = yield dryRun(metaRepo, commit, subs); + const errors = yield dryRun(repo, subs); // Throw an error if any dry-runs failed. @@ -236,10 +217,14 @@ exports.checkoutCommit = co.wrap(function *(metaRepo, commit, force) { } } - /** - * Checkout and set as head the specified `commit` in the specified `repo`. - */ - const doCheckout = co.wrap(function *(repo, commit) { + const index = yield repo.index(); + yield Reset.resetMetaRepo(repo, index, commit, changes); + repo.setHeadDetached(commit); + + const doCheckout = co.wrap(function *(name) { + const sub = subs[name]; + const commit = sub.commit; + const repo = sub.repo; const strategy = force ? NodeGit.Checkout.STRATEGY.FORCE : NodeGit.Checkout.STRATEGY.SAFE; @@ -248,18 +233,8 @@ exports.checkoutCommit = co.wrap(function *(metaRepo, commit, force) { }); repo.setHeadDetached(commit); }); - - // Now do the actual checkouts. - - yield SubmoduleUtil.cacheSubmodules(metaRepo, - () => doCheckout(metaRepo, commit)); - - yield Object.keys(subs).map(co.wrap(function *(name) { - const sub = subs[name]; - const repo = sub.repo; - const subCommit = sub.commit; - yield doCheckout(repo, subCommit); - })); + yield DoWorkQueue.doInParallel(Object.keys(subs), doCheckout); + yield index.write(); }); /** diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index 2b4dad00c..6cd477d26 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -165,14 +165,10 @@ exports.reset = co.wrap(function *(repo, commit, type) { assert.property(TYPE, type); const head = yield repo.getHeadCommit(); - const headTree = yield head.getTree(); - const commitTree = yield commit.getTree(); - const diff = yield NodeGit.Diff.treeToTree(repo, - headTree, - commitTree, - null); - const changedSubs = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff, - true); + const changedSubs = yield SubmoduleUtil.getSubmoduleChanges(repo, + commit, + head, + false); // Prep the opener to open submodules on HEAD; otherwise, our resets will // be noops. diff --git a/node/test/util/checkout.js b/node/test/util/checkout.js index 987ab29b0..acd13d3bc 100644 --- a/node/test/util/checkout.js +++ b/node/test/util/checkout.js @@ -91,30 +91,46 @@ describe("Checkout", function () { // We will operate on the repository `x`. const cases = { + "meta change": { + input: "x=S:C2-1 README.md=8;Bfoo=2", + committish: "foo", + fails: true, + }, + "conflict": { + input: `a=B:Ca-1;Ba=a|x=U:C3-2 s=Sa:a;Bfoo=3;Os I a=9`, + committish: "foo", + fails: true, + }, + "removal when changes to open sub being removed": { + input: ` +a=B|x=U:Os I a=b! W README.md=8;C3-2 s;Bfoo=3`, + committish: "foo", + expected: "x=U:C3-2 s;H=3;Bfoo=3", + }, "simple checkout with untracked file": { input: "x=S:Bfoo=1;W car=bmw", expected: "x=E:H=1", committish: "foo", }, "simple checkout with unrelated change": { - input: "x=S:C2-1;Bfoo=2;W README.md=meh", + input: `a=B|x=U:C3-2 t=Sa:1;Bfoo=3;Os I a=8`, committish: "foo", - expected: "x=E:H=2", + expected: "x=E:H=3", }, - "checkout new commit": { - input: "x=S:C2-1;Bfoo=2", + "simple checkout to branch missing sub": { + input: `a=B|x=U:C3-1 t=Sa:1;Bfoo=3;Os I a=8`, committish: "foo", - expected: "x=E:H=2", + expected: "x=U:C3-1 t=Sa:1;Bfoo=3;H=3", }, - "checkout with conflict": { - input: "x=S:C2-1;Bfoo=2;W 2=meh", + "checkout new commit": { + input: "a=B|x=S:C2-1 s=Sa:1;Bfoo=2", committish: "foo", - fails: true, + expected: "x=E:H=2", }, "checkout with conflict, but forced": { - input: "x=S:C2-1;Bfoo=2;W 2=meh", + input: `a=B:Ca-1;Ba=a|x=U:C3-2 s=Sa:a;Bfoo=3;Os I a=9`, committish: "foo", - expected: "x=E:H=2;W 2=~", + expected: "x=E:H=3;Os", force: true, }, "sub closed": { @@ -133,7 +149,7 @@ describe("Checkout", function () { expected: "x=E:H=2", }, "open sub but no change to sub": { - input: "a=S|x=U:C4-2;Os;Bfoo=4", + input: "a=S|x=U:C4-2 q=Sa:1;Os;Bfoo=4", committish: "foo", expected: "x=E:H=4", }, @@ -491,7 +507,7 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, switchBranch: null, }, "just a commit": { - input: "x=S:C2-1;Bfoo=2", + input: "a=B|x=S:C2-1 s=Sa:1;Bfoo=2", committish: "foo", newBranch: null, switchBranch: null, @@ -508,7 +524,7 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, expected: "x=E:Bfoo=1;*=foo", }, "commit and a branch": { - input: "x=S:C2-1;Bfoo=2", + input: "a=B|x=S:C2-1 s=Sa:1;Bfoo=2", committish: "foo", newBranch: { name: "bar", diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index baf1d2b22..5cb34c2f8 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -70,19 +70,19 @@ describe("MergeUtil", function () { const MODE = MergeUtil.MODE; const cases = { "simple": { - initial: "x=S:C2-1;Bfoo=2", + initial: "a=B|x=S:C2-1 q=Sa:1;Bfoo=2", commit: "2", mode: MODE.NORMAL, expected: "x=E:Bmaster=2", }, "simple, FF_ONLY": { - initial: "x=S:C2-1;Bfoo=2", + initial: "a=B|x=S:C2-1 t/y=Sa:1;Bfoo=2", commit: "2", mode: MODE.FF_ONLY, expected: "x=E:Bmaster=2", }, "simple detached": { - initial: "x=S:C2-1;Bfoo=2;*=", + initial: "a=B|x=S:C2-1 u=Sa:1;Bfoo=2;*=", commit: "2", mode: MODE.NORMAL, expected: "x=E:H=2", @@ -116,17 +116,17 @@ x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, fails: true, }, "force commit": { - initial: "x=S:C2-1;Bfoo=2", + initial: "a=B|x=S:C2-1 z=Sa:1;Bfoo=2", commit: "2", mode: MODE.FORCE_COMMIT, - expected: "x=E:Chahaha\n#x-1,2 2=2;Bmaster=x", + expected: "x=E:Chahaha\n#x-1,2 z=Sa:1;Bmaster=x", message: "hahaha", }, "force commit, detached": { - initial: "x=S:C2-1;Bfoo=2;*=", + initial: "a=B|x=S:C2-1 y=Sa:1;Bfoo=2;*=", commit: "2", mode: MODE.FORCE_COMMIT, - expected: "x=E:Chahaha\n#x-1,2 2=2;H=x", + expected: "x=E:Chahaha\n#x-1,2 y=Sa:1;H=x", message: "hahaha", }, "ff merge adding submodule": { diff --git a/node/test/util/rebase_util.js b/node/test/util/rebase_util.js index e4c5ed7a2..568f07f81 100644 --- a/node/test/util/rebase_util.js +++ b/node/test/util/rebase_util.js @@ -117,7 +117,7 @@ a=B:Cf-1;Bf=f|x=U:C3-2 s=Sa:f;C4-2 t=Sa:f;H=4;Bfoo=3`, }, "two commits": { state: ` -a=B|x=S:C2-1;Bmaster=2;Cf-1 s=Sa:1;Cg-1 t=Sa:1;Bf=f;Bg=g`, +a=B|x=S:C2-1 q=Sa:1;Bmaster=2;Cf-1 s=Sa:1;Cg-1 t=Sa:1;Bf=f;Bg=g`, seq: new SequencerState({ type: REBASE, originalHead: new CommitAndRef("2", "refs/heads/master"), @@ -202,7 +202,7 @@ a=B:Cf-1;Cg-1;Bf=f;Bg=g|x=U:C3-2 s=Sa:f;C4-2 s=Sa:g;Bother=4;Bfoo=3;Bmaster=3`, }, "two commits": { initial: ` -a=B|x=S:C2-1;Bmaster=g;Cf-1 s=Sa:1;Cg-f t=Sa:1;Bonto=2;Bg=g`, +a=B|x=S:C2-1 q=Sa:1;Bmaster=g;Cf-1 s=Sa:1;Cg-f t=Sa:1;Bonto=2;Bg=g`, onto: "2", expected: "x=E:CgM-fM t=Sa:1;CfM-2 s=Sa:1;Bmaster=gM", }, @@ -275,7 +275,7 @@ x=U:C3-2 u=Sa:a,s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Bother=3;Os`, "ffwd sub 2X": { initial: ` a=B:Ca-1;Cb-a;Cc-b;Bmaster=b;Bfoo=c| -x=U:Cr-2;C3-2 s=Sa:b;C4-3 s=Sa:c;Bmaster=4;Bother=r;Os;Bfoo=4`, +x=U:Cr-2 r=Sa:1;C3-2 s=Sa:b;C4-3 s=Sa:c;Bmaster=4;Bother=r;Os;Bfoo=4`, onto: "r", expected: "x=E:C3M-r s=Sa:b;C4M-3M s=Sa:c;Bmaster=4M;Os H=c", }, @@ -324,7 +324,7 @@ x=E:C3M-4 q=Sa:k;Bmaster=3M`, initial: ` a=B| b=B:Cj-1;Ck-1;Bmaster=j;Bfoo=k| -x=S:C2-1 s=Sa:1,t=Sb:1;C3-2;C4-2 t=Sb:k;Bmaster=4;Bfoo=3;Bold=4`, +x=S:C2-1 s=Sa:1,t=Sb:1;C3-2 z=Sa:1;C4-2 t=Sb:k;Bmaster=4;Bfoo=3;Bold=4`, onto: "3", expected: ` x=E:C4M-3 t=Sb:k;Bmaster=4M`, @@ -371,6 +371,12 @@ t Submodule ${colors.red("s")} is conflicted. `, }, + "does not close open submodules when rewinding": { + initial: ` +a=B|x=S:C2-1 s=Sa:1;Bmaster=2;Os;C3-1 t=Sa:1;Bfoo=3;Bold=2`, + onto: "3", + expected: `x=E:C2M-3 s=Sa:1;Bmaster=2M` + }, // TODO: I could not get libgit2 to remember or restore submodule branches when // used in the three-way mode; it stores it in "onto_name", not in "head-name". // "maintain submodule branch": { From 64fe21f4cecc9f9d0c95f259627c299406d83de7 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 20 Apr 2018 16:17:36 -0400 Subject: [PATCH 111/402] Allow `open` to open submodules in sparse mode Addresses https://github.com/twosigma/git-meta/projects/5#card-8955574 --- node/lib/cmd/add.js | 4 +- node/lib/cmd/submodule.js | 22 ++++---- node/lib/util/add.js | 16 +++++- node/lib/util/git_util.js | 10 +--- node/lib/util/reset.js | 2 +- node/lib/util/status_util.js | 11 ++-- node/lib/util/submodule_util.js | 89 ++++++++++++-------------------- node/test/util/add.js | 5 ++ node/test/util/git_util.js | 17 ++++-- node/test/util/open.js | 6 +++ node/test/util/submodule_util.js | 76 ++++++++++++++------------- 11 files changed, 129 insertions(+), 129 deletions(-) diff --git a/node/lib/cmd/add.js b/node/lib/cmd/add.js index c450d6408..4c1002b3d 100644 --- a/node/lib/cmd/add.js +++ b/node/lib/cmd/add.js @@ -99,8 +99,8 @@ exports.executeableSubcommand = co.wrap(function *(args) { const workdir = repo.workdir(); const cwd = process.cwd(); - const paths = yield args.paths.map(filename => { - return GitUtil.resolveRelativePath(workdir, cwd, filename); + const paths = args.paths.map(filename => { + return GitUtil.resolveRelativePath(workdir, cwd, filename); }); yield Add.stagePaths(repo, paths, args.meta, args.update); }); diff --git a/node/lib/cmd/submodule.js b/node/lib/cmd/submodule.js index ed8e14c50..831f9db27 100644 --- a/node/lib/cmd/submodule.js +++ b/node/lib/cmd/submodule.js @@ -144,7 +144,7 @@ const doStatusCommand = co.wrap(function *(paths, verbose) { paths: paths, showMetaChanges: false, }); - paths = yield paths.map(filename => { + paths = paths.map(filename => { return GitUtil.resolveRelativePath(workdir, cwd, filename); }); const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, head); @@ -156,10 +156,9 @@ const doStatusCommand = co.wrap(function *(paths, verbose) { const open = new Set(openList); let pathsToUse = allSubs; if (0 !== paths.length) { - pathsToUse = Object.keys(yield SubmoduleUtil.resolvePaths(relCwd, - paths, - allSubs, - openList)); + pathsToUse = Object.keys(SubmoduleUtil.resolvePaths(paths, + allSubs, + openList)); } const pathsSet = new Set(pathsToUse); const subShas = {}; @@ -205,13 +204,12 @@ const doFindCommand = co.wrap(function *(path, metaCommittish, subCommittish) { // Here, we find which submodule `path` refers too. It migt be invalid by // referring to no submodule, or by referring to more than one. - const relPath = yield GitUtil.resolveRelativePath(workdir, - process.cwd(), - path); - const resolvedPaths = yield SubmoduleUtil.resolvePaths(workdir, - [relPath], - subNames, - openSubNames); + const relPath = GitUtil.resolveRelativePath(workdir, + process.cwd(), + path); + const resolvedPaths = SubmoduleUtil.resolvePaths([relPath], + subNames, + openSubNames); const paths = Object.keys(resolvedPaths); if (0 === paths.length) { throw new UserError(`No submodule found in ${colors.red(path)}.`); diff --git a/node/lib/util/add.js b/node/lib/util/add.js index d12a5b099..7d3004d88 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -32,19 +32,23 @@ const assert = require("chai").assert; const co = require("co"); +const colors = require("colors"); +const fs = require("fs-promise"); const NodeGit = require("nodegit"); +const path = require("path"); const RepoStatus = require("./repo_status"); const StatusUtil = require("./status_util"); const SubmoduleUtil = require("./submodule_util"); +const UserError = require("./user_error"); /** * Stage modified content at the specified `paths` in the specified `repo`. If * a path in `paths` refers to a file, stage it; if it refers to a directory, * stage all modified content rooted at that path, including that in open * submodules. Note that a path of "" is taken to indicate the entire - * repository. The behavior is undefined unless every path in `paths` is a - * valid relative path in `repo.workdir()`. + * repository. Throw a `UserError` if any path in `paths` doesn't exist or + * can't be read. * * @async * @param {NodeGit.Repository} repo @@ -56,6 +60,14 @@ exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update) { assert.isBoolean(stageMetaChanges); assert.isBoolean(update); + yield paths.map(co.wrap(function* (name) { + try { + yield fs.stat(path.join(repo.workdir(), name)); + } catch (e) { + throw new UserError(`Invalid path: ${colors.red(name)}`); + } + })); + const repoStatus = yield StatusUtil.getRepoStatus(repo, { showMetaChanges: stageMetaChanges, paths: paths, diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 8e4fe73ed..652e6b484 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -757,24 +757,18 @@ exports.parseRefspec = function(str) { * @param {String} dir * @return {String} */ -exports.resolveRelativePath = co.wrap(function *(workdir, cwd, filename) { +exports.resolveRelativePath = function (workdir, cwd, filename) { assert.isString(workdir); assert.isString(cwd); assert.isString(filename); const absPath = path.resolve(cwd, filename); - try { - yield fs.stat(absPath); - } - catch (e) { - throw new UserError(`${colors.red(filename)} does not exist.`); - } const relPath = path.relative(workdir, absPath); if ("" !== relPath && "." === relPath[0]) { throw new UserError(`${colors.red(filename)} is outside the workdir.`); } return relPath; -}); +}; /* * Return the editor command to use for the specified `repo`. diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index 6cd477d26..71fa0fcab 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -313,7 +313,7 @@ exports.resetPaths = co.wrap(function *(repo, cwd, commit, paths) { throw new UserError("Cannot reset files to a commit that is not HEAD"); } - const resolvedPaths = yield paths.map(filename => { + const resolvedPaths = paths.map(filename => { return GitUtil.resolveRelativePath(repo.workdir(), cwd, filename); }); yield SubmoduleUtil.cacheSubmodules(repo, () => { diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 0309affac..a9d322a7b 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -414,10 +414,10 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { } if (undefined !== options.cwd) { assert.isString(options.cwd); - options.paths = yield options.paths.map(filename => { + options.paths = options.paths.map(filename => { return GitUtil.resolveRelativePath(repo.workdir(), options.cwd, - filename); + filename); }); } const headCommit = yield repo.getHeadCommit(); @@ -504,10 +504,9 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { openArray.concat(Object.keys(changes)))); if (filtering) { - filterPaths = yield SubmoduleUtil.resolvePaths(repo.workdir(), - options.paths, - subsToList, - openArray); + filterPaths = SubmoduleUtil.resolvePaths(options.paths, + subsToList, + openArray); subsToList = Object.keys(filterPaths); } diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 8c583f449..bac9708a5 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -465,17 +465,14 @@ exports.getSubmodulesForCommit = co.wrap(function *(repo, commit, names) { /** * Return the list of submodules, listed in the specified `indexSubNames`, that * are a descendant of the specified `dir`, including (potentially) `dir` - * itself (unless `dir` is suffixed with '/'), in the specified `repo`. The - * behavior is undefined unless `dir` is empty or refers to a valid path within - * `repo`. Note that if `"" === dir`, the result will be all submodules. + * itself (unless `dir` is suffixed with '/'). * * @param {NodeGit.Repository} repo * @param {String} dir * @param {String []} indexSubNames * @return {String[]} */ -exports.getSubmodulesInPath = co.wrap(function *(workdir, dir, indexSubNames) { - assert.isString(workdir); +exports.getSubmodulesInPath = function (dir, indexSubNames) { assert.isString(dir); assert.isArray(indexSubNames); if ("" !== dir) { @@ -486,27 +483,16 @@ exports.getSubmodulesInPath = co.wrap(function *(workdir, dir, indexSubNames) { if ("" === dir) { return indexSubNames; } - const subs = new Set(indexSubNames); const result = []; - const listForFilename = co.wrap(function *(filepath) { - if (subs.has(filepath)) { - result.push(filepath); + for (const subPath of indexSubNames) { + if (subPath === dir) { + return [dir]; // RETURN + } else if (subPath.startsWith(dir)) { + result.push(subPath); } - else { - const absPath = path.join(workdir, filepath); - const stat = yield fs.stat(absPath); - if (stat.isDirectory()) { - const subdirs = yield fs.readdir(absPath); - yield subdirs.map(filename => { - return listForFilename(path.join(filepath, filename)); - }); - - } - } - }); - yield listForFilename(dir); + } return result; -}); +}; /** * Return the list of submodules found in the specified `paths` in the @@ -515,62 +501,53 @@ exports.getSubmodulesInPath = co.wrap(function *(workdir, dir, indexSubNames) { * `cwd`. Throw a `UserError` if an invalid path is encountered, and log * warnings for valid paths containing no submodules. * - * @async * @param {String} workdir * @param {String} cwd * @param {String[]} submoduleNames * @param {String[]} paths * @return {String[]} */ -exports.resolveSubmoduleNames = co.wrap(function *(workdir, - cwd, - submoduleNames, - paths) { +exports.resolveSubmoduleNames = function (workdir, + cwd, + submoduleNames, + paths) { assert.isString(workdir); assert.isString(cwd); assert.isArray(submoduleNames); assert.isArray(paths); - const subLists = yield paths.map(co.wrap(function *(filename) { + const subLists = paths.map(filename => { // Compute the relative path for `filename` from the root of the repo, // and check for invalid values. - const relPath = yield GitUtil.resolveRelativePath(workdir, - cwd, - filename); - const result = yield exports.getSubmodulesInPath(workdir, - relPath, - submoduleNames); + const relPath = GitUtil.resolveRelativePath(workdir, + cwd, + filename); + const result = exports.getSubmodulesInPath(relPath, submoduleNames); if (0 === result.length) { console.warn(`\ No submodules found from ${colors.yellow(filename)}.`); } return result; - })); + }); return subLists.reduce((a, b) => a.concat(b), []); -}); +}; /** * Return a map from submodule name to an array of paths (relative to the root - * of each submodule) identified by the specified `paths` relative to the root - * of the specified `workdir`, indicating one of the submodule names in the - * specified `indexSubNames`. Check each path to see if it points into one of - * the specified `openSubmodules`, and add the relative offset to the paths for - * that submodule if it does. If any path in `paths` contains a submodule - * entirely (as opposed to a sub-path within it), it will be mappped to an - * empty array (regardless of whether or not any sub-path in that submodule is - * identified). + * of each submodule) identified by the specified `paths`, indicating one of + * the submodule names in the specified `indexSubNames`. Check each path to + * see if it points into one of the specified `openSubmodules`, and add the + * relative offset to the paths for that submodule if it does. If any path in + * `paths` contains a submodule entirely (as opposed to a sub-path within it), + * it will be mappped to an empty array (regardless of whether or not any + * sub-path in that submodule is identified). * - * @param {String} workdir * @param {String []} paths * @param {String []} indexSubNames * @param {String []} openSubmodules * @return {Object} map from submodule name to array of paths */ -exports.resolvePaths = co.wrap(function *(workdir, - paths, - indexSubNames, - openSubmodules) { - assert.isString(workdir); +exports.resolvePaths = function (paths, indexSubNames, openSubmodules) { assert.isArray(paths); assert.isArray(indexSubNames); assert.isArray(openSubmodules); @@ -580,12 +557,10 @@ exports.resolvePaths = co.wrap(function *(workdir, // First, populate 'result' with all the subs that are completely // contained. - yield paths.map(co.wrap(function *(path) { - const subs = yield exports.getSubmodulesInPath(workdir, - path, - indexSubNames); + paths.map(path => { + const subs = exports.getSubmodulesInPath(path, indexSubNames); subs.forEach(subName => result[subName] = []); - })); + }); // Now check to see which paths refer to a path inside a submodule. // Checking each file against the name of each open submodule has @@ -619,7 +594,7 @@ exports.resolvePaths = co.wrap(function *(workdir, } return result; -}); +}; /** * Create references having the specified `refs` names in each of the specified diff --git a/node/test/util/add.js b/node/test/util/add.js index d0ae5ff74..f3b906a8c 100644 --- a/node/test/util/add.js +++ b/node/test/util/add.js @@ -42,6 +42,11 @@ describe("add", function () { initial: "x=S", paths: [], }, + "bad": { + initial: "x=S", + paths: ["foo"], + fails: true, + }, "nothing to add": { initial: "x=S", paths: [""], diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index ee824546b..eb48c6450 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -910,12 +910,19 @@ describe("GitUtil", function () { filename: "/", fails: true, }, - "invalid": { + "not there": { paths: ["a"], workdir: "a", cwd: "a", filename: "b", - fails: true, + expected: "b", + }, + "not there, relative": { + paths: ["a/c"], + workdir: "a", + cwd: "a/c", + filename: "../b", + expected: "b", }, "inside": { paths: ["a/b/c"], @@ -951,9 +958,9 @@ describe("GitUtil", function () { const cwd = path.join(tempDir, c.cwd); let result; try { - result = yield GitUtil.resolveRelativePath(workdir, - cwd, - c.filename); + result = GitUtil.resolveRelativePath(workdir, + cwd, + c.filename); } catch (e) { if (!c.fails) { diff --git a/node/test/util/open.js b/node/test/util/open.js index 3ee32770d..132fe9baf 100644 --- a/node/test/util/open.js +++ b/node/test/util/open.js @@ -51,6 +51,12 @@ describe("openOnCommit", function () { commitSha: "1", expected: "x=E:Os", }, + "sparse": { + initial: "a=B|x=%U", + subName: "s", + commitSha: "1", + expected: "x=E:Os", + }, "not head": { initial: "a=B:C3-1;Bmaster=3|x=U", subName: "s", diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index c69a93dfb..cbc5bc9f2 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -698,54 +698,64 @@ describe("SubmoduleUtil", function () { describe("getSubmodulesInPath", function () { const cases = { "trivial": { - state: "x=S", dir: "", indexSubNames: [], expected: [], }, "got a sub": { - state: "a=B|x=S:C2-1 q/r=Sa:1;Bmaster=2", dir: "", indexSubNames: ["q/r"], expected: ["q/r"], }, "got a sub, by path": { - state: "a=B|x=S:C2-1 q/r=Sa:1;Bmaster=2", dir: "q", indexSubNames: ["q/r"], expected: ["q/r"], }, "got a sub, by exact path": { - state: "a=B|x=S:C2-1 q/r=Sa:1;Bmaster=2", dir: "q/r", indexSubNames: ["q/r"], expected: ["q/r"], }, "missed": { - state: "a=B|x=S:C2-1 q/r=Sa:1;Bmaster=2", dir: "README.md", indexSubNames: ["q/r"], expected: [], }, "missed, with a dot": { - state: "a=B|x=S:C2-1 q/r=Sa:1;Bmaster=2;W .foo=bar", dir: ".foo", indexSubNames: ["q/r"], expected: [], }, + "direct hit, simple": { + dir: "q", + indexSubNames: ["q"], + expected: ["q"], + }, + "missed, with a slash": { + dir: "q/", + indexSubNames: ["q"], + expected: [], + }, + "from tree": { + dir: "q", + indexSubNames: ["q/r", "q/s", "q/t/v", "z"], + expected: ["q/r", "q/s", "q/t/v"], + }, + "from tree, with slash": { + dir: "q/", + indexSubNames: ["q/r", "q/s", "q/t/v", "z"], + expected: ["q/r", "q/s", "q/t/v"], + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; - it(caseName, co.wrap(function *() { - const written = - yield RepoASTTestUtil.createMultiRepos(c.state); - const x = written.repos.x; - const result = yield SubmoduleUtil.getSubmodulesInPath( - x.workdir(), + it(caseName, function() { + const result = SubmoduleUtil.getSubmodulesInPath( c.dir, c.indexSubNames); assert.deepEqual(result.sort(), c.expected.sort()); - })); + }); }); }); describe("resolveSubmoduleNames", function () { @@ -755,11 +765,10 @@ describe("SubmoduleUtil", function () { state: "x=S", expected: [], }, - "bad path": { + "missing path": { state: "x=S", - paths: ["a-bad-path"], + paths: ["a-missing-path"], expected: [], - fails: true, }, "no subs in path": { state: "x=S:C2-1 foo/bar=baz;Bmaster=2", @@ -820,43 +829,43 @@ describe("SubmoduleUtil", function () { const cases = { "trivial": { - state: "x=S", + subNames: [], paths: [], open: [], expected: {}, }, "got by path": { - state: "a=B|x=U", + subNames: ["s"], paths: ["s"], open: [], expected: { "s": [] }, }, "inside path": { - state: "a=B|x=U:Os", + subNames: ["s"], paths: ["s/README.md"], open: ["s"], expected: { "s": ["README.md"]}, }, "inside but not listed": { - state: "a=B|x=U:Os", + subNames: ["s"], paths: ["s/README.md"], open: [], expected: {}, }, "two inside": { - state: "a=B|x=U:Os I x/y=a,a/b=b", + subNames: ["s", "x/y", "a/b"], paths: ["s/x", "s/a"], open: ["s"], expected: { s: ["x","a"]}, }, "inside path, trumped by full path": { - state: "a=B|x=U:Os", + subNames: ["s"], paths: ["s/README.md", "s"], open: ["s"], expected: { "s": []}, }, "two contained": { - state: "a=B|x=S:I a/b=Sa:1,a/c=Sa:1", + subNames: ["a/b", "a/c"], paths: ["a"], open: [], expected: { @@ -865,7 +874,7 @@ describe("SubmoduleUtil", function () { }, }, "two specified": { - state: "a=B|x=S:I a/b=Sa:1,a/c=Sa:1", + subNames: ["a/b", "a/c"], paths: ["a/b", "a/c"], open: [], expected: { @@ -874,13 +883,13 @@ describe("SubmoduleUtil", function () { }, }, "path not in sub": { - state: "a=B|x=U:Os", + subNames: ["s"], paths: ["README.md"], open: ["s"], expected: {}, }, "filename starts with subname but not in it": { - state: "a=B|x=U:I sam=3;Os", + subNames: ["s"], paths: ["sam"], open: ["s"], expected: {}, @@ -888,17 +897,12 @@ describe("SubmoduleUtil", function () { }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; - it(caseName, co.wrap(function *() { - const w = yield RepoASTTestUtil.createMultiRepos(c.state); - const repo = w.repos.x; - const workdir = repo.workdir(); - const subs = yield SubmoduleUtil.getSubmoduleNames(repo); - const result = yield SubmoduleUtil.resolvePaths(workdir, - c.paths, - subs, - c.open); + it(caseName, function () { + const result = SubmoduleUtil.resolvePaths(c.paths, + c.subNames, + c.open); assert.deepEqual(result, c.expected); - })); + }); }); }); describe("addRefs", function () { From 4ad4cb3d8071611b144d895ec1c6df6b13c9de42 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sat, 21 Apr 2018 20:19:43 -0400 Subject: [PATCH 112/402] Remove uses of `cacheSubmodules` and places where we performed operations on the meta-repo --- node/lib/cmd/status.js | 9 ---- node/lib/util/reset.js | 75 ++++++++++++-------------------- node/lib/util/status_util.js | 12 +++-- node/lib/util/submodule_util.js | 24 ---------- node/test/util/reset.js | 19 -------- node/test/util/submodule_util.js | 26 ----------- 6 files changed, 34 insertions(+), 131 deletions(-) diff --git a/node/lib/cmd/status.js b/node/lib/cmd/status.js index b0cdd56aa..1afec51fe 100644 --- a/node/lib/cmd/status.js +++ b/node/lib/cmd/status.js @@ -56,14 +56,6 @@ sub-repo. Also show diagnostic information if the repository is in consistent state, e.g., when a sub-repo is on a different branch than the meta-repo.`; exports.configureParser = function (parser) { - parser.addArgument(["--meta"], { - required: false, - action: "storeConst", - constant: true, - help: ` -Include changes to the meta-repo; disabled by default to improve performance.`, - defaultValue: false, - }); parser.addArgument(["path"], { type: "string", help: "paths to inspect for changes", @@ -91,7 +83,6 @@ exports.executeableSubcommand = co.wrap(function *(args) { const workdir = repo.workdir(); const cwd = process.cwd(); const repoStatus = yield StatusUtil.getRepoStatus(repo, { - showMetaChanges: args.meta, cwd: cwd, paths: args.path, }); diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index 71fa0fcab..8edd65b23 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -241,50 +241,6 @@ exports.reset = co.wrap(function *(repo, commit, type) { yield index.write(); }); -/** - * Helper method for `resolvePaths` to simplify use of `cacheSubmodules`. - */ -const resetPathsHelper = co.wrap(function *(repo, commit, resolvedPaths) { - // Get a `Status` object reflecting only the values in `paths`. - - const status = yield StatusUtil.getRepoStatus(repo, { - showMetaChanges: true, - paths: resolvedPaths, - }); - - // Reset the meta-repo. - - const metaStaged = Object.keys(status.staged); - if (0 !== metaStaged.length) { - yield NodeGit.Reset.default(repo, commit, metaStaged); - } - - const subs = status.submodules; - const fetcher = new SubmoduleFetcher(repo, commit); - const subNames = Object.keys(subs); - const shas = - yield SubmoduleUtil.getSubmoduleShasForCommit(repo, subNames, commit); - - yield subNames.map(co.wrap(function *(subName) { - const sub = subs[subName]; - const workdir = sub.workdir; - const sha = shas[subName]; - - // If the submodule isn't open (no workdir) or didn't exist on `commit` - // (i.e., it had no sha there), skip it. - - if (null !== workdir && undefined !== sha) { - const subRepo = yield SubmoduleUtil.getRepo(repo, subName); - yield fetcher.fetchSha(subRepo, subName, sha); - const subCommit = yield subRepo.getCommit(sha); - const staged = Object.keys(workdir.status.staged); - if (0 !== staged.length) { - yield NodeGit.Reset.default(subRepo, subCommit, staged); - } - } - })); -}); - /** * Reset the state of the index of the specified `repo` for the specified * `paths` to their state in the specified `commit`; or throw a `UserError` if @@ -316,7 +272,34 @@ exports.resetPaths = co.wrap(function *(repo, cwd, commit, paths) { const resolvedPaths = paths.map(filename => { return GitUtil.resolveRelativePath(repo.workdir(), cwd, filename); }); - yield SubmoduleUtil.cacheSubmodules(repo, () => { - return resetPathsHelper(repo, commit, resolvedPaths); + + const status = yield StatusUtil.getRepoStatus(repo, { + paths: resolvedPaths, }); + + const subs = status.submodules; + const fetcher = new SubmoduleFetcher(repo, commit); + const subNames = Object.keys(subs); + const shas = + yield SubmoduleUtil.getSubmoduleShasForCommit(repo, subNames, commit); + + yield subNames.map(co.wrap(function *(subName) { + const sub = subs[subName]; + const workdir = sub.workdir; + const sha = shas[subName]; + + // If the submodule isn't open (no workdir) or didn't exist on `commit` + // (i.e., it had no sha there), skip it. + + if (null !== workdir && undefined !== sha) { + const subRepo = yield SubmoduleUtil.getRepo(repo, subName); + yield fetcher.fetchSha(subRepo, subName, sha); + const subCommit = yield subRepo.getCommit(sha); + const staged = Object.keys(workdir.status.staged); + if (0 !== staged.length) { + yield NodeGit.Reset.default(subRepo, subCommit, staged); + } + } + })); + }); diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index a9d322a7b..f3595965d 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -453,13 +453,11 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { const treeId = head.treeId(); tree = yield NodeGit.Tree.lookup(repo, treeId); } - const status = yield SubmoduleUtil.cacheSubmodules(repo, () => { - return DiffUtil.getRepoStatus(repo, - tree, - options.paths, - options.ignoreIndex, - options.showAllUntracked); - }); + const status = yield DiffUtil.getRepoStatus(repo, + tree, + options.paths, + options.ignoreIndex, + options.showAllUntracked); args.staged = status.staged; args.workdir = status.workdir; } diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index bac9708a5..a507ff170 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -657,27 +657,3 @@ exports.addRefs = co.wrap(function *(repo, refs, submodules) { })); })); }); - -/** - * Cache the submodules before invoking the specified `operation` and uncache - * them after the operation is completed, or before allowing an exception to - * propagte. Return the result of `operation`. - * - * @param {NodeGit.Repository} repo - * @param {(repo ) => Promise} operation - */ -exports.cacheSubmodules = co.wrap(function *(repo, operation) { - assert.instanceOf(repo, NodeGit.Repository); - assert.isFunction(operation); - repo.submoduleCacheAll(); - let result; - try { - result = yield operation(repo); - } - catch (e) { - repo.submoduleCacheClear(); - throw e; - } - repo.submoduleCacheClear(); - return result; -}); diff --git a/node/test/util/reset.js b/node/test/util/reset.js index df30dd03b..9dfe4f997 100644 --- a/node/test/util/reset.js +++ b/node/test/util/reset.js @@ -258,31 +258,12 @@ a=B:Ca-1;Bmaster=a|x=U:C3-2 s=Sa:a;Bmaster=3;Bf=3;Os`, commit: "1", paths: [ "README.md" ], }, - "reset one": { - initial: "x=S:I README.md=3", - commit: "1", - paths: [ "README.md" ], - expected: "x=S:W README.md=3", - }, - "reset multiple": { - initial: "x=S:C2-1;Bmaster=2;I README.md=3,2=3", - commit: "2", - paths: [ "README.md", "2" ], - expected: "x=S:C2-1;Bmaster=2;W README.md=3,2=3", - }, "reset from another commit": { initial: "x=S:C2-1 README.md=8;Bfoo=2;I README.md=3", commit: "2", paths: [ "README.md" ], fails: true, }, - "in subdir": { - initial: "x=S:C2-1 s/x=foo;Bmaster=2;I s/x=8", - commit: "2", - paths: [ "x" ], - cwd: "s", - expected: "x=E:I s/x=~;W s/x=8", - }, "in submodule": { initial: "a=B|x=U:Os I README.md=88", commit: "2", diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index cbc5bc9f2..69c71f1c5 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -949,30 +949,4 @@ describe("SubmoduleUtil", function () { })); }); }); - describe("cacheSubmodules", function () { - it("breathing", co.wrap(function *() { - const repo = yield TestUtil.createSimpleRepository(); - function op(r) { - assert.equal(repo, r); - return Promise.resolve(3); - } - const result = yield SubmoduleUtil.cacheSubmodules(repo, op); - assert.equal(result, 3); - })); - it("exception", co.wrap(function *() { - class MyException {} - function op() { - throw new MyException(); - } - const repo = yield TestUtil.createSimpleRepository(); - try { - yield SubmoduleUtil.cacheSubmodules(repo, op); - } - catch (e) { - assert.instanceOf(e, MyException); - return; - } - assert(false, "should have thrown"); - })); - }); }); From 6a3beadff559f3d380ede81bc4ea60f759b19a5e Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 4 Apr 2018 16:40:21 -0400 Subject: [PATCH 113/402] spelling --- node/lib/util/shorthand_parser_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/shorthand_parser_util.js b/node/lib/util/shorthand_parser_util.js index 389f2480c..4d274cfef 100644 --- a/node/lib/util/shorthand_parser_util.js +++ b/node/lib/util/shorthand_parser_util.js @@ -181,7 +181,7 @@ const SequencerState = RepoAST.SequencerState; * C/foo/bar:Bfoo=1 -- a clone of /foo/bar overriding branch foo to 1 * S:I foo=bar,x=y -- staged changes to 'foo' and 'x' * S:I foo=bar,x=y;W foo,q=z -- as above but `foo` is deleted in the - * workding directory and `q` has been added + * working directory and `q` has been added * with content set to `z`. * S:I foo=S/a:1;Ofoo -- A submodule added to the index at `foo` * -- that is open but no local changes From ba5c55882629410dfb98dce652e6cef07a83cc4e Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 6 Apr 2018 12:50:24 -0400 Subject: [PATCH 114/402] upgrade nodegit --- node/lib/util/commit.js | 2 +- node/lib/util/git_util.js | 3 ++- node/lib/util/stash_util.js | 2 +- node/lib/util/submodule_rebase_util.js | 2 +- node/lib/util/tree_util.js | 34 ++++++++++---------------- node/lib/util/write_repo_ast_util.js | 4 +-- node/package.json | 2 +- node/test/util/tree_util.js | 6 ++--- 8 files changed, 24 insertions(+), 31 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 2739672e3..cbdff6e2f 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -572,7 +572,7 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message) { changes[filename] = null; } else { - const blobId = TreeUtil.hashFile(repo, filename); + const blobId = yield TreeUtil.hashFile(repo, filename); changes[filename] = new Change(blobId, FILEMODE.BLOB); yield index.addByPath(filename); diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 652e6b484..cf5ebde27 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -531,7 +531,8 @@ exports.fetchSha = co.wrap(function *(repo, url, sha) { const execString = `git -C '${repo.path()}' fetch -q '${url}' ${sha}`; try { - return yield ChildProcess.exec(execString); + yield ChildProcess.exec(execString); + return yield repo.getCommit(sha); } catch (e) { throw new UserError(e.message); diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 0e25d1a69..c0b977c11 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -233,7 +233,7 @@ exports.createReflogIfNeeded = co.wrap(function *(repo, if (0 === log.entrycount()) { const id = NodeGit.Oid.fromString(sha); log.append(id, repo.defaultSignature(), message); - log.write(); + yield log.write(); } }); diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index 85f74a7d1..7f071f16e 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -173,7 +173,7 @@ exports.processRebase = co.wrap(function *(repo, rebase, op) { result.conflictedCommit = op.id().tostrS(); return result; // RETURN } - const newCommit = rebase.commit(null, signature, null); + const newCommit = yield rebase.commit(null, signature, null); const originalCommit = op.id().tostrS(); result.commits[newCommit.tostrS()] = originalCommit; op = yield exports.callNext(rebase); diff --git a/node/lib/util/tree_util.js b/node/lib/util/tree_util.js index ca43cae67..993fb823c 100644 --- a/node/lib/util/tree_util.js +++ b/node/lib/util/tree_util.js @@ -173,7 +173,7 @@ exports.writeTree = co.wrap(function *(repo, baseTree, changes) { } else if (entry instanceof Change) { const inserted = - yield builder.insert(filename, entry.id, entry.mode); + builder.insert(filename, entry.id, entry.mode); treeEntries.push(inserted); } else { @@ -201,10 +201,10 @@ exports.writeTree = co.wrap(function *(repo, baseTree, changes) { builder.remove(filename); } else { - const inserted = yield builder.insert( - filename, - subtree.id(), - NodeGit.TreeEntry.FILEMODE.TREE); + const inserted = builder.insert( + filename, + subtree.id(), + NodeGit.TreeEntry.FILEMODE.TREE); treeEntries.push(inserted); } } @@ -222,20 +222,13 @@ exports.writeTree = co.wrap(function *(repo, baseTree, changes) { * @param {String} filename * @return {NodeGit.Oid} */ -exports.hashFile = function (repo, filename) { +exports.hashFile = co.wrap(function* (repo, filename) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(filename); - // 'createFromDisk' is unfinished; instead of returning an id, it takes an - // buffer and writes into it, unlike the rest of its brethern on `Blob`. - // TODO: patch nodegit with corrected API. - - const placeholder = - NodeGit.Oid.fromString("0000000000000000000000000000000000000000"); const filepath = path.join(repo.workdir(), filename); - NodeGit.Blob.createFromDisk(placeholder, repo, filepath); - return placeholder; -}; + return yield NodeGit.Blob.createFromDisk(repo, filepath); +}); /** * Return a map from path to `Change` for the working directory of the @@ -273,14 +266,13 @@ exports.listWorkdirChanges = co.wrap(function *(repo, status, includeUnstaged) { switch (workdir[subpath]) { case FILESTATUS.ADDED: if (includeUnstaged) { - result[subpath] = new Change( - exports.hashFile(repo, subpath), - filemode); + const sha = yield exports.hashFile(repo, subpath); + result[subpath] = new Change(sha, filemode); } break; case FILESTATUS.MODIFIED: - result[subpath] = new Change(exports.hashFile(repo, subpath), - filemode); + const sha = yield exports.hashFile(repo, subpath); + result[subpath] = new Change(sha,filemode); break; case FILESTATUS.REMOVED: result[subpath] = null; @@ -322,7 +314,7 @@ exports.listWorkdirChanges = co.wrap(function *(repo, status, includeUnstaged) { if (touchedModules) { const modulesName = SubmoduleConfigUtil.modulesFileName; - const id = exports.hashFile(repo, modulesName); + const id = yield exports.hashFile(repo, modulesName); result[modulesName] = new Change(id, FILEMODE.BLOB); } diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index 89c882d14..64e001081 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -401,7 +401,7 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { if (ast.bare) { if (null !== ast.currentBranchName) { - repo.setHead("refs/heads/" + ast.currentBranchName); + yield repo.setHead("refs/heads/" + ast.currentBranchName); } else { repo.setHeadDetached(newHeadSha); @@ -841,7 +841,7 @@ git -C '${repo.path()}' -c gc.reflogExpire=0 -c gc.reflogExpireUnreachable=0 \ if (null === index) { index = - yield RepoAST.renderIndex(ast.commits, ast.head, ast.index); + RepoAST.renderIndex(ast.commits, ast.head, ast.index); } const sub = index[subName]; const openSubAST = ast.openSubmodules[subName]; diff --git a/node/package.json b/node/package.json index 08759cbf6..1c3550545 100644 --- a/node/package.json +++ b/node/package.json @@ -33,7 +33,7 @@ "co": "", "colors": "", "fs-promise": "", - "nodegit": "^0.19.0", + "nodegit": "^0.21.1", "rimraf": "", "split": "" }, diff --git a/node/test/util/tree_util.js b/node/test/util/tree_util.js index 388096748..f94c7841c 100644 --- a/node/test/util/tree_util.js +++ b/node/test/util/tree_util.js @@ -244,7 +244,7 @@ describe("TreeUtil", function () { const filename = "foo"; const filepath = path.join(repo.workdir(), filename); yield fs.writeFile(filepath, content); - const result = TreeUtil.hashFile(repo, filename); + const result = yield TreeUtil.hashFile(repo, filename); const db = yield repo.odb(); const BLOB = 3; const expected = yield db.write(content, content.length, BLOB); @@ -354,7 +354,7 @@ describe("TreeUtil", function () { // It doesn't matter what's in the file, just that the function // includes its contents. yield fs.writeFile(modPath, "foo"); - const modId = TreeUtil.hashFile( + const modId = yield TreeUtil.hashFile( repo, SubmoduleConfigUtil.modulesFileName); const status = new RepoStatus({ @@ -383,7 +383,7 @@ describe("TreeUtil", function () { // It doesn't matter what's in the file, just that the function // includes its contents. yield fs.writeFile(modPath, "foo"); - const modId = TreeUtil.hashFile( + const modId = yield TreeUtil.hashFile( repo, SubmoduleConfigUtil.modulesFileName); From 6e158ad3e361047ef3647e9b40c99015c8a9ddce Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 9 Apr 2018 20:51:52 -0400 Subject: [PATCH 115/402] we only need one hashObject --- node/lib/util/git_util.js | 18 +++++++++++++++++ node/lib/util/write_repo_ast_util.js | 29 ++++------------------------ node/test/util/conflict_util.js | 13 ++++--------- node/test/util/read_repo_ast_util.js | 11 +++-------- 4 files changed, 29 insertions(+), 42 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index cf5ebde27..69d367453 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -982,3 +982,21 @@ exports.updateHead = co.wrap(function *(repo, commit, reason) { yield headRef.setTarget(commit, reason); } }); + +/* + * Write the specified `data` to the specified `repo` and return its hash + * value. + * + * @async + * @private + * @param {NodeGit.Repository} repo + * @param {String} data + * @return {String} + */ +exports.hashObject = co.wrap(function *(repo, data) { + const BLOB = 3; + const db = yield repo.odb(); + const res = yield db.write(data, data.length, BLOB); + return res; +}); + diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index 64e001081..ed61fd007 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -47,6 +47,7 @@ const path = require("path"); const ConflictUtil = require("./conflict_util"); const DoWorkQueue = require("./do_work_queue"); +const GitUtil = require("./git_util"); const RebaseFileUtil = require("./rebase_file_util"); const RepoAST = require("./repo_ast"); const RepoASTUtil = require("./repo_ast_util"); @@ -60,22 +61,6 @@ const FILEMODE = NodeGit.TreeEntry.FILEMODE; // Begin module-local methods -/** - * Write the specified `data` to the specified `repo` and return its hash - * value. - * - * @async - * @private - * @param {NodeGit.Repository} repo - * @param {String} data - * @return {String} - */ -const hashObject = co.wrap(function *(db, data) { - const BLOB = 3; - const res = yield db.write(data, data.length, BLOB); - return res; -}); - /** * Configure the specified `repo` to have settings needed by git-meta tests. * @@ -107,7 +92,6 @@ const configRepo = co.wrap(function *(repo) { * @return {NodeGit.Tree} return.tree */ const writeTree = co.wrap(function *(repo, - db, shaMap, changes, parent) { @@ -143,7 +127,7 @@ const writeTree = co.wrap(function *(repo, else if ("string" === typeof entry) { // A string is just plain data. - const id = (yield hashObject(db, entry)).tostrS(); + const id = (yield GitUtil.hashObject(repo, entry)).tostrS(); pathToChange[filename] = new TreeUtil.Change(id, FILEMODE.BLOB); } else if (entry instanceof RepoAST.Submodule) { @@ -181,7 +165,7 @@ const writeTree = co.wrap(function *(repo, \turl = ${url} `; } - const dataId = (yield hashObject(db, data)).tostrS(); + const dataId = (yield GitUtil.hashObject(repo, data)).tostrS(); pathToChange[SubmoduleConfigUtil.modulesFileName] = new TreeUtil.Change(dataId, FILEMODE.BLOB); } @@ -223,7 +207,6 @@ exports.writeCommits = co.wrap(function *(oldCommitMap, assert.isObject(commits); assert.isArray(shas); - const db = yield repo.odb(); let newCommitMap = {}; // from new to old sha const sig = repo.defaultSignature(); @@ -252,7 +235,6 @@ exports.writeCommits = co.wrap(function *(oldCommitMap, // by the commit at `sha` and has caches for subtrees and submodules. const trees = yield writeTree(repo, - db, oldCommitMap, commit.changes, parentTrees); @@ -346,8 +328,7 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { const sha = commitMap[data.sha]; return new ConflictUtil.ConflictEntry(FILEMODE.COMMIT, sha); } - const db = yield repo.odb(); - const id = yield hashObject(db, data); + const id = yield GitUtil.hashObject(repo, data); const BLOB = FILEMODE.BLOB; return new ConflictUtil.ConflictEntry(BLOB, id.tostrS()); }); @@ -512,9 +493,7 @@ git -C '${repo.workdir()}' reset --hard ${newHeadSha} if (null !== indexHead) { indexParent = treeCache[indexHead]; } - const db = yield repo.odb(); const trees = yield writeTree(repo, - db, commitMap, ast.index, indexParent); diff --git a/node/test/util/conflict_util.js b/node/test/util/conflict_util.js index a595aea82..504e58f93 100644 --- a/node/test/util/conflict_util.js +++ b/node/test/util/conflict_util.js @@ -37,14 +37,9 @@ const NodeGit = require("nodegit"); const path = require("path"); const ConflictUtil = require("../../lib/util/conflict_util"); +const GitUtil = require("../../lib/util/git_util"); const TestUtil = require("../../lib/util/test_util"); -const hashObject = co.wrap(function *(repo, data) { - const db = yield repo.odb(); - const BLOB = 3; - return yield db.write(data, data.length, BLOB); -}); - describe("ConflictUtil", function () { const ConflictEntry = ConflictUtil.ConflictEntry; const Conflict = ConflictUtil.Conflict; @@ -76,7 +71,7 @@ describe("addConflict", function () { it("existing", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); const makeEntry = co.wrap(function *(data) { - const id = yield hashObject(repo, data); + const id = yield GitUtil.hashObject(repo, data); return new ConflictEntry(FILEMODE.BLOB, id.tostrS()); }); const ancestor = yield makeEntry("xxx"); @@ -98,7 +93,7 @@ describe("addConflict", function () { it("multiple values", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); const makeEntry = co.wrap(function *(data) { - const id = yield hashObject(repo, data); + const id = yield GitUtil.hashObject(repo, data); return new ConflictEntry(FILEMODE.BLOB, id.tostrS()); }); const ancestor = yield makeEntry("xxx"); @@ -118,7 +113,7 @@ describe("addConflict", function () { it("with null", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); const makeEntry = co.wrap(function *(data) { - const id = yield hashObject(repo, data); + const id = yield GitUtil.hashObject(repo, data); return new ConflictEntry(FILEMODE.BLOB, id.tostrS()); }); const ancestor = yield makeEntry("xxx"); diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index 18c0b283a..2c3d910e7 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -37,6 +37,7 @@ const NodeGit = require("nodegit"); const path = require("path"); const ConflictUtil = require("../../lib/util/conflict_util"); +const GitUtil = require("../../lib/util/git_util"); const Rebase = require("../../lib/util/rebase"); const RepoAST = require("../../lib/util/repo_ast"); const ReadRepoASTUtil = require("../../lib/util/read_repo_ast_util"); @@ -76,12 +77,6 @@ const astFromSimpleRepo = co.wrap(function *(repo) { }); }); -const hashObject = co.wrap(function *(repo, data) { - const db = yield repo.odb(); - const BLOB = 3; - return yield db.write(data, data.length, BLOB); -}); - /** * Create a repository with a branch and two commits and a `RepoAST` object * representing its expected state. @@ -1701,7 +1696,7 @@ describe("readRAST", function () { it("three versions", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); const makeEntry = co.wrap(function *(data) { - const id = yield hashObject(repo, data); + const id = yield GitUtil.hashObject(repo, data); return new ConflictEntry(FILEMODE.BLOB, id.tostrS()); }); const ancestor = yield makeEntry("xxx"); @@ -1729,7 +1724,7 @@ describe("readRAST", function () { it("with a deletion", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); const makeEntry = co.wrap(function *(data) { - const id = yield hashObject(repo, data); + const id = yield GitUtil.hashObject(repo, data); return new ConflictEntry(FILEMODE.BLOB, id.tostrS()); }); const ancestor = yield makeEntry("xxx"); From ba79e65385c2702ad77b62c3458904600a0cff0c Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 10 Apr 2018 00:12:39 -0400 Subject: [PATCH 116/402] fix flaky test --- node/test/util/stash_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index c7044244a..50c4d7dac 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -48,7 +48,7 @@ const writeLog = co.wrap(function *(repo, reverseMap, logs) { const sha = reverseMap[logSha]; log.append(NodeGit.Oid.fromString(sha), sig, `log of ${logSha}`); } - log.write(); + yield log.write(); }); /** From b81209f2ea4dbff0dd4bb4e34c796f2bf2ed07a1 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 13 Apr 2018 15:50:56 -0400 Subject: [PATCH 117/402] do not report closed submodules deleted from the working tree --- node/lib/util/read_repo_ast_util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/lib/util/read_repo_ast_util.js b/node/lib/util/read_repo_ast_util.js index c5117d3a0..a45e91de3 100644 --- a/node/lib/util/read_repo_ast_util.js +++ b/node/lib/util/read_repo_ast_util.js @@ -490,8 +490,8 @@ exports.readRAST = co.wrap(function *(repo) { for (let i = 0; i < subNames.length; ++i) { const subName = subNames[i]; const status = yield NodeGit.Submodule.status(repo, subName, 0); - const IN_WD = NodeGit.Submodule.STATUS.IN_WD; - if (status & IN_WD) { + if (status & NodeGit.Submodule.STATUS.IN_WD && + !(status & NodeGit.Submodule.STATUS.WD_UNINITIALIZED)) { const sub = yield NodeGit.Submodule.lookup(repo, subName); const subRepo = yield sub.open(); const subAST = yield exports.readRAST(subRepo); From a73ff97b0818a13485a86efc79d778890d26bc1f Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 13 Apr 2018 15:52:58 -0400 Subject: [PATCH 118/402] writeUrls learns cached argument --- node/lib/util/submodule_config_util.js | 40 ++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index 3aa2b4649..d503c3bb6 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -53,6 +53,7 @@ const path = require("path"); const rimraf = require("rimraf"); const url = require("url"); +const GitUtil = require("./git_util"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const UserError = require("./user_error"); @@ -616,11 +617,44 @@ exports.writeConfigText = function (urls) { * @param {NodeGit.Repository} repo * @param {NodeGit.Index} index * @param {Object} urls submodule name to url + * @param {Boolean} cached do not write to the working tree */ -exports.writeUrls = co.wrap(function *(repo, index, urls) { +exports.writeUrls = co.wrap(function *(repo, index, urls, cached) { + if (undefined === cached) { + cached = false; + } + else { + assert.isBoolean(cached); + } + const modulesPath = path.join(repo.workdir(), exports.modulesFileName); const newConf = exports.writeConfigText(urls); - yield fs.writeFile(modulesPath, newConf); - yield index.addByPath(exports.modulesFileName); + if (newConf.length === 0) { + if (!cached) { + try { + yield fs.unlink(modulesPath); + } catch (e) { + //maybe it didn't exist prior to this + } + } + try { + yield index.removeByPath(exports.modulesFileName); + } catch (e) { + // ditto + } + } + else { + const oid = yield GitUtil.hashObject(repo, newConf); + const sha = oid.toString(); + if (!cached) { + yield fs.writeFile(modulesPath, newConf); + } + const entry = new NodeGit.IndexEntry(); + entry.path = exports.modulesFileName; + entry.mode = NodeGit.TreeEntry.FILEMODE.BLOB; + entry.id = NodeGit.Oid.fromString(sha); + entry.flags = entry.flagsExtended = 0; + yield index.add(entry); + } }); From 8a8ec00e458b4b949b9efb6cc00b4d59fadd7481 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 18 Apr 2018 17:18:12 -0400 Subject: [PATCH 119/402] textutils pluralize and strcmp --- node/lib/util/text_util.js | 32 ++++++++++++++++++ node/test/util/text_util.js | 65 +++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/node/lib/util/text_util.js b/node/lib/util/text_util.js index 3a6646204..2828a4ab5 100644 --- a/node/lib/util/text_util.js +++ b/node/lib/util/text_util.js @@ -57,3 +57,35 @@ exports.indent = function(str, count) { } return " ".repeat(count) + str; }; + +/** + * Convert a list of strings to a newline-delimited, indented string. + * + * @param {Array} The strings + * @param {Integer} count (default 4) + * @return {String} + */ +exports.listToIndentedString = function(strings, count) { + return strings.map(s => exports.indent(s, count)).join("\n"); +}; + +/** + * Pluralize a noun (if necessary). This is kind of a hack: + * it doesn't handle 'children', 'wolves', 'gees', or 'oxen'. + * + * @param {String} The noun + * @param {Integer} the count -- if it's not 1, the noun will be pluralized. + * @return {String} + */ +exports.pluralize = function(noun, count) { + if (count === 1) { + return noun; + } + if (noun.match("(?:s|sh|ch|z|x)$")) { + return noun + "es"; + } + if (noun.endsWith("y")) { + return noun.substring(0, noun.length - 1) + "ies"; + } + return noun + "s"; +}; diff --git a/node/test/util/text_util.js b/node/test/util/text_util.js index 97b9af1f7..f9b50d9e7 100644 --- a/node/test/util/text_util.js +++ b/node/test/util/text_util.js @@ -46,4 +46,69 @@ describe("TextUtil", function () { assert.equal(" three", TextUtil.indent("three", 3)); }); }); + + describe("strcmp", function () { + it("breathing test", function() { + const cases = [ + { + a : "fleem", + b : "morx", + expect : -1 + }, + { + a : "morx", + b : "fleem", + expect : 1 + }, + { + a : "foo", + b : "foo", + expect : 0 + } + ]; + for (const c of cases) { + assert.equal(TextUtil.strcmp(c.a, c.b), c.expect); + } + }); + }); + + describe("pluralize", function () { + it("breathing test", function() { + const cases = [ + { + noun : "fleem", + count : 1, + expect : "fleem" + }, + { + noun : "fleem", + count : 0, + expect : "fleems" + }, + { + noun : "fleem", + count : 2, + expect : "fleems" + }, + { + noun : "bass", + count : 1, + expect : "bass" + }, + { + noun : "bass", + count : 2, + expect : "basses" + }, + { + noun : "harpy", + count : 2, + expect : "harpies" + } + ]; + for (const c of cases) { + assert.equal(TextUtil.pluralize(c.noun, c.count), c.expect); + } + }); + }); }); From 6ae534620685cd6c99c18330bc062946116b3682 Mon Sep 17 00:00:00 2001 From: Shihui Xiang Date: Mon, 23 Apr 2018 15:53:15 -0400 Subject: [PATCH 120/402] #556: git meta commit-shadow should allow a timestamp to be specified --- node/lib/cmd/commit-shadow.js | 12 ++++++++++-- node/lib/util/stash_util.js | 13 ++++++++++++- node/test/util/stash_util.js | 9 +++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/node/lib/cmd/commit-shadow.js b/node/lib/cmd/commit-shadow.js index b5d8c3f8f..7d4a6152d 100644 --- a/node/lib/cmd/commit-shadow.js +++ b/node/lib/cmd/commit-shadow.js @@ -75,10 +75,17 @@ exports.configureParser = function (parser) { required: true, help: "commit message for shadow commits", }); + parser.addArgument(["-e", "--epoch-timestamp"], { + required: false, + action: "storeConst", + constant: true, + defaultValue: false, + help: "include timestamp in the shadow commit", + }); }; /** - * Exeucte the `commit-shadow` command according to the specified `args`. + * Execute the `commit-shadow` command according to the specified `args`. * * @async * @param {Object} args @@ -91,8 +98,9 @@ exports.executeableSubcommand = co.wrap(function *(args) { const repo = yield GitUtil.getCurrentRepo(); const result = yield StashUtil.makeShadowCommit(repo, args.message, + args.epoch_timestamp, false, - args.include_untracked ); + args.include_untracked); if (null === result) { const head = yield repo.getHeadCommit(); console.log(head.id().tostrS()); diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 0e25d1a69..d3155768b 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -476,17 +476,20 @@ exports.list = co.wrap(function *(repo) { * @param {NodeGit.Repository} repo * @param {RepoStatus} status * @param {String} message + * @param {Bool} useEpochTimestamp * @param {Bool} includeUntracked * @return {String} */ const makeShadowCommitForRepo = co.wrap(function *(repo, status, message, + useEpochTimestamp, includeUntracked) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(status, RepoStatus); assert.isString(message); assert.isBoolean(includeUntracked); + assert.isBoolean(useEpochTimestamp); const changes = yield TreeUtil.listWorkdirChanges(repo, status, @@ -502,7 +505,10 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, const indexTree = yield repo.getTree(treeOid); const newTree = yield TreeUtil.writeTree(repo, indexTree, changes); - const sig = repo.defaultSignature(); + let sig = repo.defaultSignature(); + if (useEpochTimestamp) { + sig = NodeGit.Signature.create(sig.name(), sig.email(), 0, 0); + } const id = yield NodeGit.Commit.create(repo, null, sig, @@ -528,6 +534,7 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, * * @param {NodeGit.Repository} repo * @param {String} message + * @param {Bool} useEpochTimestamp * @param {Bool} includeMeta * @param {Bool} includeUntracked * @return {Object|null} @@ -536,12 +543,14 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, */ exports.makeShadowCommit = co.wrap(function *(repo, message, + useEpochTimestamp, includeMeta, includeUntracked) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(message); assert.isBoolean(includeMeta); assert.isBoolean(includeUntracked); + assert.isBoolean(useEpochTimestamp); if (!message.endsWith("\n")) { message += "\n"; @@ -572,6 +581,7 @@ exports.makeShadowCommit = co.wrap(function *(repo, const subSha = yield makeShadowCommitForRepo(subRepo, wd.status, message, + useEpochTimestamp, includeUntracked); const newSubStat = new Submodule({ commit: subStatus.commit, @@ -595,6 +605,7 @@ exports.makeShadowCommit = co.wrap(function *(repo, const metaCommit = yield makeShadowCommitForRepo(repo, newStatus, message, + useEpochTimestamp, includeUntracked); return { metaCommit: metaCommit, diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index c7044244a..f9723e3b0 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -720,6 +720,7 @@ meta-stash@{1}: log of 1 expected: "x=E:Cfoo\n#m-1 foo/bar=2;Bm=m", message: "foo", includeMeta: true, + useEpochTimestamp: true, }, "deleted file": { state: "x=S:W README.md", @@ -785,15 +786,23 @@ x=E:Cm-1 s=Sa:s;Bm=m;Os Cs foo=bar!Bs=s!W foo=bar` const meta = c.includeMeta; const includeUntracked = undefined === c.includeUntracked || c.includeUntracked; + const useEpochTimestamp = + (undefined === c.useEpochTimestamp) ? + false : c.useEpochTimestamp; const result = yield StashUtil.makeShadowCommit( repo, message, + useEpochTimestamp, meta, includeUntracked); + const commitMap = {}; if (null !== result) { const metaSha = result.metaCommit; const commit = yield repo.getCommit(metaSha); + if (useEpochTimestamp) { + assert.equal(commit.time(), 0); + } commitMap[metaSha] = "m"; yield NodeGit.Branch.create(repo, "m", commit, 1); for (let path in result.subCommits) { From f52804a21c50d5bd889987cece188f5feaff2efc Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 23 Apr 2018 13:28:34 -0400 Subject: [PATCH 121/402] implement rm --- node/lib/cmd/rm.js | 110 ++++++++++ node/lib/git-meta.js | 2 + node/lib/util/rm.js | 473 +++++++++++++++++++++++++++++++++++++++++++ node/package.json | 2 + node/test/util/rm.js | 229 +++++++++++++++++++++ 5 files changed, 816 insertions(+) create mode 100644 node/lib/cmd/rm.js create mode 100644 node/lib/util/rm.js create mode 100644 node/test/util/rm.js diff --git a/node/lib/cmd/rm.js b/node/lib/cmd/rm.js new file mode 100644 index 000000000..b2bd38ad7 --- /dev/null +++ b/node/lib/cmd/rm.js @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const co = require("co"); + +/** + * This module contains methods for implementing the `rm` command. + */ + +/** + * help text for the `rm` command + * @property {String} + */ +exports.helpText = `Remove files or submodules from the mono-repo.`; + +/** + * description of the `rm` command + * @property {String} + */ +exports.description =` +This command updates the (logical) mono-repo index using the current content +found in the working tree, to prepare the content staged for the next commit. +If the path specified is a submodule, this command will stage the removal of +the submodule entirely (including removing it from .gitmodules). Otherwise, +its removal will be staged in the index. +`; + +exports.configureParser = function (parser) { + parser.addArgument(["-f", "--force"], { + required: false, + action: "storeConst", + constant: true, + help: "Force removal of a commit with stated changes.", + defaultValue:false + }); + + parser.addArgument(["-r", "--recursive"], { + required: false, + action: "storeConst", + constant: true, + help: "Remove a directory recursively", + defaultValue:false + }); + + parser.addArgument(["--cached"], { + required: false, + action: "storeConst", + constant: true, + help: ` + Remove the file from the index, but not from disk. In the case of a + submodule, edits to .gitmodules will staged (but the on-disk version + will not be affected)`, + defaultValue: false, + }); + parser.addArgument(["paths"], { + nargs: "+", + type: "string", + help: "the paths to rm", + }); +}; + +/** + * Execute the `rm` command according to the specified `args`. + * + * @async + * @param {Object} args + * @param {String[]} args.paths + */ +exports.executeableSubcommand = co.wrap(function *(args) { + const Rm = require("../util/rm"); + const GitUtil = require("../util/git_util"); + + const repo = yield GitUtil.getCurrentRepo(); + const workdir = repo.workdir(); + const cwd = process.cwd(); + + const paths = yield args.paths.map(filename => { + return GitUtil.resolveRelativePath(workdir, cwd, filename); + }); + yield Rm.rmPaths(repo, paths, args.recursive, args.cached, args.force); +}); diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js index e79fec7bb..0ab2f2ec0 100755 --- a/node/lib/git-meta.js +++ b/node/lib/git-meta.js @@ -53,6 +53,7 @@ const pull = require("./cmd/pull"); const push = require("./cmd/push"); const rebase = require("./cmd/rebase"); const reset = require("./cmd/reset"); +const rm = require("./cmd/rm"); const root = require("./cmd/root"); const commitShadow = require("./cmd/commit-shadow"); const stash = require("./cmd/stash"); @@ -135,6 +136,7 @@ const commands = { "push": push, "rebase": rebase, "reset": reset, + "rm": rm, "root": root, "commit-shadow": commitShadow, "stash": stash, diff --git a/node/lib/util/rm.js b/node/lib/util/rm.js new file mode 100644 index 000000000..f49d507f9 --- /dev/null +++ b/node/lib/util/rm.js @@ -0,0 +1,473 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const binarySearch = require("binary-search"); +const co = require("co"); +const fs = require("fs-promise"); +const groupBy = require("group-by"); +const path = require("path"); +const NodeGit = require("nodegit"); + +const CloseUtil = require("./close_util"); +const SubmoduleUtil = require("./submodule_util"); +const SubmoduleConfigUtil = require("./submodule_config_util"); +const TextUtil = require("./text_util"); +const UserError = require("./user_error"); + +const FILEMODE = NodeGit.TreeEntry.FILEMODE; +const STATUS = NodeGit.Status.STATUS; + +function needToRequestRecursive(path) { + throw new UserError( + `not removing '${path}' recursively without -r`); +} + +function pluralizeHas(n) { + return n === 1 ? "has" : "have"; +} + +const errorsByCause = { + unstaged: { + problem: "local modifications", + solution: "use --cached to keep the file, or -f to force removal" + }, + staged: { + problem: "changes staged in the index", + solution: "use -f to force removal" + }, + stagedAndUnstaged: { + problem: "staged content different from both the file and the HEAD", + solution: "use -f to force removal" + } +}; + +function problemMessage(badFiles, cause, entityType) { + const error = errorsByCause[cause]; + assert.isDefined(error); + return `the following ${TextUtil.pluralize(entityType, badFiles.length)} \ +${pluralizeHas(badFiles.length)} ${error.problem}: +${TextUtil.listToIndentedString(badFiles)} +(${error.solution})`; +} + +/** + * @class Problem + * describes a problem which prevents removal of an object + */ +class Problem { + constructor(path, cause, entityType) { + if (undefined === entityType) { + entityType = "file"; + } + + this.d_path = path; + this.d_cause = cause; + this.d_entityType = entityType; + Object.freeze(this); + } + get path() { + return this.d_path; + } + get cause() { + return this.d_cause; + } + get entityType() { + return this.d_entityType; + } +} + +const checkIndexIsHead = co.wrap(function*(headTree, indexId, entryPath) { + if (headTree === null) { + return false; + } + else { + let inRepo; + try { + inRepo = yield headTree.entryByPath(entryPath); + } catch (e) { + return false; + } + if (!indexId.equal(inRepo.id())) { + return false; + } + } + return true; +}); + +/** + * Return null if check index == HEAD || index == workdir. Else, + * return a Problem + */ +const checkIndexIsHeadOrWorkdir = co.wrap(function*(repo, headTree, entry, + entryPath, displayPath) { + const inIndex = entry.id; + + const indexIsHead = yield checkIndexIsHead(headTree, inIndex, entryPath); + if (indexIsHead) { + return null; + } + const filePath = path.join(repo.workdir(), entryPath); + try { + yield fs.access(filePath); + } catch (e) { + assert.equal("ENOENT", e.code); + //a file that doesn't exist is considered OK for rm --cached, too + return null; + } + + if (entry.mode === FILEMODE.COMMIT) { + // this is a submodule so we don't check the working tree + return new Problem(displayPath, "staged", "submodule"); + } + + const index = yield repo.index(); + const diff = yield NodeGit.Diff.indexToWorkdir(repo, index, + {"pathspec" : [entryPath]}); + if (diff.numDeltas() === 0) { + return null; + } + else { + return new Problem(displayPath, "unstaged", "file"); + } +}); + +function setDefault(options, arg, def) { + if (undefined === options[arg]) { + options[arg] = def; + } + else { + assert.isBoolean(options[arg]); + } +} + +/** + * Check that a file is clean enough to delete, according to Git's + * rules. An already-deleted file is always clean. If --cached + * is supplied, index may match either HEAD or the worktree; otherwise, + * only the worktree is permitted. For unclean files, return a Problem + * describing how the file is unclean. + */ +const checkCleanliness = co.wrap(function *(repo, headTree, index, pathname, + options) { + if (options.force) { + return null; + } + + let displayPath = pathname; + if (options.prefix !== undefined) { + displayPath = path.join(options.prefix, pathname); + } + const entry = index.getByPath(pathname); + + if (options.cached) { + return yield checkIndexIsHeadOrWorkdir(repo, headTree, entry, + pathname, displayPath); + } + + const status = yield NodeGit.Status.file(repo, pathname); + + if (status === 0 || status & STATUS.WT_DELETED !== 0) { + //git considers these OK regardless + return null; + } + if ((status & STATUS.WT_MODIFIED) !== 0) { + if ((status & STATUS.INDEX_MODIFIED) !== 0) { + return new Problem(displayPath, "stagedAndUnstaged"); + } + else { + return new Problem(displayPath, "unstaged"); + } + } + else { + if ((status & STATUS.INDEX_MODIFIED) !== 0) { + return new Problem(displayPath, "staged"); + } + } + return null; +}); + +const deleteEmptyParents = co.wrap(function*(fullpath) { + const parent = path.dirname(fullpath); + try { + yield fs.rmdir(parent); + } catch (e) { + //We only want to remove empty parent dirs, but it's + //cheaper just to try to remove them and ignore errors + return; + } + yield deleteEmptyParents(parent); +}); + +function checkAllPathsResolved(paths, resolved) { + const allResolvedPaths = []; + for (const rootLevel of Object.keys(resolved)) { + const subPaths = resolved[rootLevel]; + if (subPaths.length === 0) { + allResolvedPaths.push(rootLevel); + } else { + for (const sub of subPaths) { + allResolvedPaths.push(path.join(rootLevel, sub)); + } + } + } + allResolvedPaths.sort(); + for (const spec of paths) { + let stripped = spec; + if (spec.endsWith("/")) { + stripped = spec.substring(0, spec.length - 1); + } + const idx = binarySearch(allResolvedPaths, stripped, TextUtil.strcmp); + if (idx < 0) { + //spec is a/b, next item is a/b/c, OK + const insertionPoint = -idx - 1; + const nextResolved = allResolvedPaths[insertionPoint]; + if (insertionPoint >= allResolvedPaths.length || + !nextResolved.startsWith(stripped + "/")) { + throw new UserError(`pathspec '${spec}' did not match any \ + files`); + } + } + } +} + +/** + * Remove the specified `paths` in the specified `repo`. If a path in + * `paths` refers to a file, remove it. If it refers to a directory, + * and recursive is true, remove it recursively. If it's false, throw + * a UserError. If a path to be removed does not exist in the index, + * throw a UserError. If a path to be removed is not clean, and force + * is false, throw a UserError. + * + * If --cached is supplied, the paths will be removed from the index + * but not from disk. + * + * If there are any dirty paths, the meta or submodule indexes may be + * dirty upon return from this function. Unfortunately due to a + * nodegit bug, we can't reload the index: + * https://github.com/nodegit/nodegit/issues/1478 + * + * @async + * @param {NodeGit.Repository} repo + * @param {String []} paths + * @param {Object} [options] + * @param {Boolean} [options.recursive] + * @param {Boolean} [options.cached] (remove from index not disk) + * @param {Boolean} [options.force] + * @param {String} [options.prefix] Path prefix for file lookup + */ +exports.rmPaths = co.wrap(function *(repo, paths, options) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isArray(paths); + if (undefined === options) { + options = {}; + } + else { + assert.isObject(options); + } + setDefault(options, "recursive", false); + setDefault(options, "cached", false); + setDefault(options, "force", false); + if (undefined === options.prefix) { + options.prefix = ""; + } + else { + assert.isString(options.prefix); + } + + for (const p of paths) { + if (p === "") { + throw new UserError("warning: empty strings as pathspecs are " + + "invalid. Please use . instead if you " + + "meant to match all paths."); + } + } + + const index = yield repo.index(); + const headCommit = yield repo.getHeadCommit(); + + const indexUrls = + yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + const headUrls = + yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, headCommit); + + // In theory, it would be be possible for a sub to be in the + // workdir (that is, in an unstaged change to .gitmodules) as + // well, but since git rm generally doesn't deal with workdir + // changes, we ignore that possibility. + + // We might, at least for easy testing, want to handle changes to + // root-level files, so we add those files to the root level items + + const rootLevelItemSet = new Set(); + for (const submodules of [indexUrls, headUrls]) { + for (const sub of Object.keys(submodules)) { + rootLevelItemSet.add(sub); + } + } + + for (const entry of index.entries()) { + rootLevelItemSet.add(entry.path); + } + + const rootLevelItems = []; + rootLevelItems.push(...rootLevelItemSet); + const openArray = yield SubmoduleUtil.listOpenSubmodules(repo); + const openSubmoduleSet = new Set(openArray); + + const resolved = yield SubmoduleUtil.resolvePaths(paths, + rootLevelItems, + openArray); + + checkAllPathsResolved(paths, resolved); + + let headTree = null; + if (headCommit !== null) { + const treeId = headCommit.treeId(); + headTree = yield repo.getTree(treeId); + } + + // Check that everything is clean enough to remove + const pathSet = new Set(paths); + const toRemove = []; + const problems = []; + const removedSubmodules = []; + const toRecurse = []; + for (const rootLevel of Object.keys(resolved)) { + const items = resolved[rootLevel]; + if (items.length === 0) { + if (!(options.recursive || pathSet.has(rootLevel))) { + return needToRequestRecursive([rootLevel]); + } + toRemove.push(rootLevel); + const problem = yield checkCleanliness(repo, headTree, index, + rootLevel, options); + if (problem !== null) { + problems.push(problem); + } + const entry = index.getByPath(rootLevel); + if (entry === undefined || entry.mode === FILEMODE.COMMIT) { + removedSubmodules.push(rootLevel); + } + } + else { + // recurse into submodule + const subRepo = yield SubmoduleUtil.getRepo(repo, rootLevel); + const subOptions = {}; + Object.assign(subOptions, options); + subOptions.prefix = rootLevel; + // We set dryRun = true here because we're just checking + // for clean. If there are no problems, we'll later + // go through toRecurse and do a full run. + subOptions.dryRun = true; + toRecurse.push({repo : subRepo, items : items, + options : subOptions}); + yield exports.rmPaths(subRepo, items, subOptions); + } + } + + //report any problems + if (problems.length !== 0) { + let msg = ""; + const byType = groupBy(problems, "entityType"); + + for (const type in byType) { + const byCause = groupBy(byType[type], "cause"); + for (const cause in byCause) { + msg += problemMessage(byCause[cause].map(x => x.path), + cause, type); + } + } + throw new UserError(msg); + } + + if (options.dryRun) { + return; + } + + // Now do the full run on submodules + for (const r of toRecurse) { + r.options.dryRun = false; + yield exports.rmPaths(r.repo, r.items, r.options); + } + + // This "if" is necessary due to + // https://github.com/nodegit/nodegit/issues/1487 + if (toRemove.length !== 0) { + yield index.removeAll(toRemove); + yield index.write(); + } + + // close to-be-deleted submodules + const submodulesToClose = []; + for (const submodule of removedSubmodules) { + if (openSubmoduleSet.has(submodule)) { + submodulesToClose.push(submodule); + } + } + + yield CloseUtil.close(repo, repo.workdir(), submodulesToClose, + options.force); + + + // Clean up the workdir + const root = repo.workdir(); + if (!options.cached) { + for (const file of toRemove) { + const fullpath = path.join(root, file); + let stat = null; + try { + stat = yield fs.stat(fullpath); + } catch (e) { + //it is possible that e doesn't exist on disk + assert.equal("ENOENT", e.code); + } + if (stat !== null) { + if (stat.isDirectory()) { + yield fs.rmdir(fullpath); + } else { + yield fs.unlink(fullpath); + } + } + yield deleteEmptyParents(fullpath); + } + } + + // And write any gitmodules files changes + const modules = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, + index); + for (const file of removedSubmodules) { + delete modules[file]; + } + + yield SubmoduleConfigUtil.writeUrls(repo, index, modules, options.cached); +}); diff --git a/node/package.json b/node/package.json index 1c3550545..504805b46 100644 --- a/node/package.json +++ b/node/package.json @@ -28,11 +28,13 @@ "homepage": "https://github.com/twosigma/git-meta#readme", "dependencies": { "argparse": "", + "binary-search": "", "chai": "", "child-process-promise": "", "co": "", "colors": "", "fs-promise": "", + "group-by": "", "nodegit": "^0.21.1", "rimraf": "", "split": "" diff --git a/node/test/util/rm.js b/node/test/util/rm.js new file mode 100644 index 000000000..8ba30970b --- /dev/null +++ b/node/test/util/rm.js @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const co = require("co"); + +const Rm = require("../../lib/util/rm"); +const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); + +describe("rm", function () { + describe("rmPaths", function () { + const cases = { + // Cases in meta repo + "everything from empty repo": { + initial: "x=S:I README.md", + paths: [""], + fails: true, + }, + "a clean file by name": { + initial: "x=S:C2-1 x/y/z=foo;W x/q/z=bar;Bmaster=2", + paths: ["x/y/z"], + expect: "x=E:I x/y/z", + }, + "a clean file by name --cached": { + initial: "x=S:C2-1 x/y/z=foo;W x/q/z=bar;Bmaster=2", + paths: ["x/y/z"], + cached: true, + expect: "x=E:I x/y/z;W x/y/z=foo", + }, + "a clean file by removing its containing dir, no --recursive": { + initial: "x=S:C2-1 x/y/z=foo;W x/q/z=bar;Bmaster=2", + paths: ["x/y"], + fails: true, + }, + "a clean file by removing its containing dir": { + initial: "x=S:C2-1 x/y/z=foo;W x/q/z=bar;Bmaster=2", + paths: ["x/y"], + recursive: true, + expect: "x=E:I x/y/z" + }, + "two clean files by removing their containing dir": { + initial: "x=S:C2-1 x/y/a=foo,x/y/z=foo;W x/q/z=bar;Bmaster=2", + paths: ["x/y"], + recursive: true, + expect: "x=E:I x/y/a,x/y/z" + }, + "two clean by removing their grandparent dir": { + initial: "x=S:C2-1 x/y/a=foo,x/y/z=foo;W x/q/z=bar;Bmaster=2", + paths: ["x"], + recursive: true, + expect: "x=E:I x/y/a,x/y/z" + }, + "a non-existent thing": { + initial: "x=S:C2-1 x/y/a=foo,x/y/z=foo;W x/q/z=bar;Bmaster=2", + paths: ["z"], + fails: true, + }, + "a file that only exists in the index": { + initial: "x=S:I x/y/a=foo;W x/y/a", + paths: ["x/y/a"], + expect: "x=S", + }, + "a file that only exists in the index, --cached": { + initial: "x=S:I x/y/a=foo;W x/y/a", + paths: ["x/y/a"], + cached: true, + expect: "x=S", + }, + "a file that only exists in the index, -f": { + initial: "x=S:I x/y/a=foo;W x/y/a=", + paths: ["x/y/a"], + force: true, + expect: "x=S" + }, + "a submodule": { + initial: `x=S:C2-1 x/y/a=foo,x/y/z=foo;C3-1 d=S/baz.git:2; + Bmaster=3;Bsub=2`, + paths: ["d"], + expect: "x=E:I d" + }, + "an open submodule": { + initial: `sub=S:C8-1 x/y/a=foo,x/y/z=foo;Bmaster=8| + x=S:C2-1 d=Ssub:8;Bmaster=2;Od`, + paths: ["d"], + expect: "x=S:C2-1 d=Ssub:8;Bmaster=2;I d" + }, + "a submodule that only exists in the index": { + // because this only exists in the index, need -f to remove it + initial: `sub=S:C8-1 x/y/a=foo;Bmaster=8| + x=S:I d=Ssub:8;Bmaster=1;Od`, + paths: ["d"], + cached: true, + force: true, + expect: "x=S:Bmaster=1" + }, + "a submodule that only exists in the wt and .gitmodules": { + initial: `sub=B|x=S:I d=Ssub:;Od`, + paths: ["d"], + expect: `x=S`, + }, + // cases inside submodules + "a non-existent thing from a submodule": { + initial: `sub=S:C4-1;Bsub=4|x=S:C2-1 x/y/a=foo,x/y/z=foo; + C3-2 d=Ssub:4; + Bmaster=3;Od`, + paths: ["d/f"], + fails: true, + }, + "a file from a closed submodule": { + initial: `x=S:C2-1 x/y/a=foo,x/y/z=foo;C3-1 d=S/baz.git:2; + Bmaster=3;Bsub=2`, + paths: ["d/x/y/z"], + fails: true, + }, + "a file from an open submodule": { + initial: `sub=S:C2-1 x/y/a=foo,x/y/z=foo;Bmaster=2| + x=S:C3-1 d=Ssub:2;Bmaster=3;Od`, + paths: ["d/x/y/z"], + expect: "x=E:Od I x/y/z", + }, + "a nonexistent dir from an open submodule": { + initial: `sub=S:C2-1 x/y/a=foo,x/y/z=foo;Bmaster=2| + x=S:C3-1 d=Ssub:2;Bmaster=3;Od`, + paths: ["d/q"], + fails: true, + }, + "a file an open submodule via dir, no --recursive": { + initial: `sub=S:C2-1 x/y/a=foo,x/y/z=foo;Bmaster=2| + x=S:C3-1 d=Ssub:2;Bmaster=3;Od`, + paths: ["d/x/y"], + fails: true, + }, + "a file from an open submodule via dir": { + initial: `sub=S:C2-1 x/y/a=foo,x/y/z=foo;Bmaster=2| + x=S:C3-1 d=Ssub:2,d2=Ssub:2;Bmaster=3;Od;Od2`, + paths: ["d/x/y"], + recursive: true, + expect: "x=E:Od I x/y/a,x/y/z;Od2", + }, + "a modified file from an open submodule via dir": { + initial: `sub=S:C2-1 x/y/a=foo,x/y/z=foo;Bmaster=2| + x=S:C3-1 d=Ssub:2;Bmaster=3;Od W x/y/z=mod`, + paths: ["d/x/y"], + recursive: true, + fails: true, + }, + "a cached file from an open submodule via dir": { + initial: `sub=S:C2-1 x/y/a=foo,x/y/z=orig;Bmaster=2| + x=S:C3-1 d=Ssub:2; + Bmaster=3;Od I x/y/z=mod!W x/y/z=orig`, + paths: ["d/x/y"], + recursive: true, + fails: true, + }, + "a cached file from an open submodule via dir --cached": { + initial: `sub=S:C2-1 x/y/a=foo,x/y/z=orig;Bmaster=2| + x=S:C3-1 d=Ssub:2; + Bmaster=3;Od I x/y/z=mod!W x/y/z=orig`, + paths: ["d/x/y"], + recursive: true, + cached: true, + fails: true, + }, + "a cached file from an open submodule, --cached, index = head": + { + initial: `sub=S:C2-1 x/y/a=foo,x/y/z=orig;Bmaster=2| + x=S:C3-1 d=Ssub:2; + Bmaster=3;Od I x/y/z=orig!W x/y/z=mod`, + paths: ["d/x/y/z"], + recursive: true, + cached: true, + expect: "x=E:Od I x/y/z!W x/y/z=mod", + }, + "a cached file from an open submodule, --cached, index = wt": { + initial: `sub=S:C2-1 x/y/a=foo,x/y/z=orig;Bmaster=2| + x=S:C3-1 d=Ssub:2; + Bmaster=3;Od I x/y/z=mod!W x/y/z=mod`, + paths: ["d/x/y/z"], + recursive: true, + cached: true, + expect: "x=E:Od I x/y/z!W x/y/z=mod", + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const rmPaths = co.wrap(function *(repos) { + const repo = repos.x; + yield Rm.rmPaths(repo, c.paths, { + recursive: c.recursive, + cached: c.cached, + force: c.force}); + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + c.expect, + rmPaths, + c.fails); + })); + }); + }); +}); From 63a2453baa2332f84c56ade1627cf18d76ae1f01 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 25 Apr 2018 09:07:50 -0400 Subject: [PATCH 122/402] Allow checkout to work in empty/semi-empty state Fixed a couple of problems here: * Allowed `SubmoduleFetcher` to accept a null commit in its constructor to make it possible to work with a headless repo * Fixed `GitUtil.getCurrentBranchName` to work in a semi-empty state where libgit2 throws an error if you ask for the current branch. Addresses https://github.com/twosigma/git-meta/issues/558 --- node/lib/util/git_util.js | 12 +++++++++++- node/lib/util/submodule_fetcher.js | 18 +++++++++++++----- node/test/util/checkout.js | 5 +++++ node/test/util/git_util.js | 3 +++ node/test/util/submodule_fetcher.js | 12 ++++++++++-- 5 files changed, 42 insertions(+), 8 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 69d367453..663f087d9 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -391,7 +391,17 @@ exports.getCurrentBranchName = co.wrap(function *(repo) { assert.instanceOf(repo, NodeGit.Repository); if (!repo.isEmpty() && 1 !== repo.headDetached()) { - const branch = yield repo.getCurrentBranch(); + let branch; + try { + branch = yield repo.getCurrentBranch(); + } catch (e) { + // TODO: raise an issue with libgit2. If your repository is in a + // newly initialized state, but not fully empty (e.g., it has + // configured remotes), this method throws an exception: + // "reference 'refs/heads/master' not found". Either it should + // return null or `isEmpty` should return true. + return null; + } return branch.shorthand(); } return null; diff --git a/node/lib/util/submodule_fetcher.js b/node/lib/util/submodule_fetcher.js index 7268c9a8c..ed6823785 100644 --- a/node/lib/util/submodule_fetcher.js +++ b/node/lib/util/submodule_fetcher.js @@ -53,17 +53,25 @@ class SubmoduleFetcher { * if provided, or the URL of the remote named "origin" in `repo` * otherwise. If `repo` has no remote named "origin", and `fetchSha` is * called for a submodule that has a relativre URL, throw a `UserError`. + * If `null === commit`, no URLS are available. * - * @param {NodeGit.Repository} repo - * @param {NodeGit.Commit} commit + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit|null} commit */ constructor(repo, commit) { assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(commit, NodeGit.Commit); + if (null !== commit) { + assert.instanceOf(commit, NodeGit.Commit); + } this.d_repo = repo; this.d_commit = commit; - this.d_urls = null; + + if (null === commit) { + this.d_urls = {}; + } else { + this.d_urls = null; + } // d_metaOrigin may have three types of values: // 1. undefined -- we haven't tried to access it yet @@ -81,7 +89,7 @@ class SubmoduleFetcher { } /** - * @param {NodeGit.Commit} commit commit associated with this fetcher + * @param {NodeGit.Commit|null} commit commit associated with this fetcher */ get commit() { return this.d_commit; diff --git a/node/test/util/checkout.js b/node/test/util/checkout.js index acd13d3bc..574157643 100644 --- a/node/test/util/checkout.js +++ b/node/test/util/checkout.js @@ -96,6 +96,11 @@ describe("Checkout", function () { committish: "foo", fails: true, }, + "from empty": { + input: "a=B:C2 s=Sb:1;Bfoo=2|b=B|x=N:Rtrunk=a foo=2", + committish: "trunk/foo", + expected: "x=E:H=2", + }, "conflict": { input: `a=B:Ca-1;Ba=a|x=U:C3-2 s=Sa:a;Bfoo=3;Os I a=9`, committish: "foo", diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index eb48c6450..df77a6437 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -516,6 +516,9 @@ describe("GitUtil", function () { "detached head": { input: "S:*=", expected: null }, "not master": { input: "S:Bmaster=;Bfoo=1;*=foo", expected: "foo"}, "empty": { input: new RepoAST(), expected: null }, + "no current branch but not empty": { + input: "N:C2;Rtrunk=/a foo=2", + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; diff --git a/node/test/util/submodule_fetcher.js b/node/test/util/submodule_fetcher.js index c23d73103..7ba87e579 100644 --- a/node/test/util/submodule_fetcher.js +++ b/node/test/util/submodule_fetcher.js @@ -49,6 +49,11 @@ describe("SubmoduleFetcher", function () { metaSha: "2", expected: null, }, + "null commit": { + initial: "a=B:C4-1;Bfoo=4|b=B|x=U:Os Rorigin=c", + metaSha: null, + expected: null, + }, "pulled from origin": { initial: "a=B:C4-1;Bfoo=4|b=B|x=Ca", metaSha: "1", @@ -62,8 +67,11 @@ describe("SubmoduleFetcher", function () { const written = yield RepoASTTestUtil.createMultiRepos(c.initial); const repo = written.repos.x; - const newSha = written.reverseCommitMap[c.metaSha]; - const commit = yield repo.getCommit(newSha); + let commit = null; + if (null !== c.metaSha) { + const newSha = written.reverseCommitMap[c.metaSha]; + commit = yield repo.getCommit(newSha); + } let metaUrl = c.metaOriginUrl; if (null !== metaUrl) { metaUrl = written.reverseUrlMap[metaUrl]; From c76f977fbe3304349d8fa169724375b230e80fc1 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sat, 28 Apr 2018 14:37:35 -0400 Subject: [PATCH 123/402] Address path mode changes in `TreeUtil` Specifically, those where a "tree" (a.k.a. directory) changes into something else and vice-versa. Addresses: https://github.com/twosigma/git-meta/issues/562 --- node/lib/util/tree_util.js | 26 +++++++++---- node/test/util/tree_util.js | 74 +++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/node/lib/util/tree_util.js b/node/lib/util/tree_util.js index 993fb823c..e6af3d2b6 100644 --- a/node/lib/util/tree_util.js +++ b/node/lib/util/tree_util.js @@ -62,20 +62,33 @@ exports.buildDirectoryTree = function (flatTree) { for (let i = 0; i + 1 < paths.length; ++i) { const nextPath = paths[i]; - if (nextPath in tree) { + let nextTree = tree[nextPath]; + + // If we have a null entry for something that we need to be a tree, + // that means we've changed something that was an object into a + // parent directory. Otherwise, we need to build a new object for + // this directory. + + if (undefined !== nextTree && null !== nextTree) { tree = tree[nextPath]; - assert.isObject(tree, `for path ${path}`); } else { - const nextTree = {}; + nextTree = {}; tree[nextPath] = nextTree; tree = nextTree; } } const leafPath = paths[paths.length - 1]; - assert.notProperty(tree, leafPath, `duplicate entry for ${path}`); + const leafData = tree[leafPath]; const data = flatTree[subpath]; - tree[leafPath] = data; + + // Similar to above, if we see something changed to null where we + // alreaduy have data, we can ignore it. This just means that + // something we are removing is turning into a tree. + + if (undefined === leafData || null !== data) { + tree[leafPath] = data; + } } return result; @@ -187,8 +200,7 @@ exports.writeTree = co.wrap(function *(repo, baseTree, changes) { // 'filename' didn't exist in 'parentTree' } } - if (null !== treeEntry) { - assert(treeEntry.isTree(), `${filename} should be a tree`); + if (null !== treeEntry && treeEntry.isTree()) { treeEntries.push(treeEntry); const treeId = treeEntry.id(); const curTree = yield repo.getTree(treeId); diff --git a/node/test/util/tree_util.js b/node/test/util/tree_util.js index f94c7841c..abbf421fd 100644 --- a/node/test/util/tree_util.js +++ b/node/test/util/tree_util.js @@ -85,6 +85,28 @@ describe("TreeUtil", function () { }, }, }, + "deleted tree changed to parent": { + input: { + "a": null, + "a/b": "1", + }, + expected: { + "a": { + "b": "1", + }, + }, + }, + "deleted tree changed to parent, order reversed": { + input: { + "a/b": "1", + "a": null, + }, + expected: { + "a": { + "b": "1", + }, + }, + }, }; Object.keys(cases).forEach((caseName) => { const c = cases[caseName]; @@ -236,6 +258,58 @@ describe("TreeUtil", function () { const entry = yield secondTree.entryByPath("foo"); assert.equal(entry.id().tostrS(), newId.tostrS()); })); + it("from blob to tree with blob", co.wrap(function *() { + const repo = yield makeRepo(); + const id = yield hashObject(repo, "xxxxxxxh"); + const firstTree = yield TreeUtil.writeTree(repo, null, { + "foo": new Change(id, FILEMODE.BLOB), + }); + const secondTree = yield TreeUtil.writeTree(repo, firstTree, { + "foo": null, + "foo/bar": new Change(id, FILEMODE.BLOB), + }); + const entry = yield secondTree.entryByPath("foo/bar"); + assert.equal(entry.id().tostrS(), id.tostrS()); + })); + it("from blob to tree with blob, reversed", co.wrap(function *() { + const repo = yield makeRepo(); + const id = yield hashObject(repo, "xxxxxxxh"); + const firstTree = yield TreeUtil.writeTree(repo, null, { + "foo": new Change(id, FILEMODE.BLOB), + }); + const secondTree = yield TreeUtil.writeTree(repo, firstTree, { + "foo/bar": new Change(id, FILEMODE.BLOB), + "foo": null, + }); + const entry = yield secondTree.entryByPath("foo/bar"); + assert.equal(entry.id().tostrS(), id.tostrS()); + })); + it("from tree with blob to blob", co.wrap(function *() { + const repo = yield makeRepo(); + const id = yield hashObject(repo, "xxxxxxxh"); + const firstTree = yield TreeUtil.writeTree(repo, null, { + "foo/bar": new Change(id, FILEMODE.BLOB), + }); + const secondTree = yield TreeUtil.writeTree(repo, firstTree, { + "foo": new Change(id, FILEMODE.BLOB), + "foo/bar": null, + }); + const entry = yield secondTree.entryByPath("foo"); + assert.equal(entry.id().tostrS(), id.tostrS()); + })); + it("from tree with blob to blob, reversed", co.wrap(function *() { + const repo = yield makeRepo(); + const id = yield hashObject(repo, "xxxxxxxh"); + const firstTree = yield TreeUtil.writeTree(repo, null, { + "foo/bar": new Change(id, FILEMODE.BLOB), + }); + const secondTree = yield TreeUtil.writeTree(repo, firstTree, { + "foo/bar": null, + "foo": new Change(id, FILEMODE.BLOB), + }); + const entry = yield secondTree.entryByPath("foo"); + assert.equal(entry.id().tostrS(), id.tostrS()); + })); }); describe("hashFile", function () { it("breathing", co.wrap(function *() { From 2e954b4e3e99ec705f857f924c3d474dc802c0b7 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 29 Apr 2018 18:46:23 -0400 Subject: [PATCH 124/402] Fixed missing `yield` in stash_util Wasn't needed before Nodegit upgrade --- node/lib/util/stash_util.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 554b4992b..2ebcf44a1 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -383,11 +383,13 @@ const getStashSha = co.wrap(function *(repo, index) { exports.removeStash = co.wrap(function *(repo, index) { assert.instanceOf(repo, NodeGit.Repository); assert.isNumber(index); + + const log = yield NodeGit.Reflog.read(repo, metaStashRef); const stashSha = yield getStashSha(repo, index); const count = log.entrycount(); log.drop(index, 1 /* rewrite previous entry */); - log.write(); + yield log.write(); // We dropped the first element. We need to update `refs/meta-stash` @@ -403,7 +405,7 @@ exports.removeStash = co.wrap(function *(repo, index) { // remove the old one. log.drop(1, 1 /* rewrite previous entry */); - log.write(); + yield log.write(); } else { NodeGit.Reference.remove(repo, metaStashRef); From ca3b059683f7583d382ed1fbfc59d0f1e66c833a Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 30 Apr 2018 15:13:16 -0400 Subject: [PATCH 125/402] git meta status --short --- node/lib/cmd/status.js | 24 +++++++++--- node/lib/util/print_status_util.js | 61 +++++++++++++++++++++++++++++ node/test/util/print_status_util.js | 41 +++++++++++++++++++ 3 files changed, 120 insertions(+), 6 deletions(-) diff --git a/node/lib/cmd/status.js b/node/lib/cmd/status.js index 1afec51fe..43e8ac265 100644 --- a/node/lib/cmd/status.js +++ b/node/lib/cmd/status.js @@ -56,6 +56,13 @@ sub-repo. Also show diagnostic information if the repository is in consistent state, e.g., when a sub-repo is on a different branch than the meta-repo.`; exports.configureParser = function (parser) { + parser.addArgument(["-s", "--short"], { + required: false, + action: "storeConst", + constant: true, + help: "Give the output in a short format", + dest: "shortFormat" //"short" is a reserved word in js + }); parser.addArgument(["path"], { type: "string", help: "paths to inspect for changes", @@ -64,13 +71,12 @@ exports.configureParser = function (parser) { }; /** - * Execute the pull command according to the specified `args`. + * Execute the status command according to the specified `args`. * * @async - * @param {Object} args - * @param {Boolean} args.any - * @param {String} repository - * @param {String} [source] + * @param {Object} args + * @param {Boolean} args.shortFormat + * @param {[String]} args.path */ exports.executeableSubcommand = co.wrap(function *(args) { const path = require("path"); @@ -92,6 +98,12 @@ exports.executeableSubcommand = co.wrap(function *(args) { const relCwd = path.relative(workdir, cwd); - const text = PrintStatusUtil.printRepoStatus(repoStatus, relCwd); + let text; + if (args.shortFormat) { + text = PrintStatusUtil.printRepoStatusShort(repoStatus, relCwd); + } else { + text = PrintStatusUtil.printRepoStatus(repoStatus, relCwd); + } + process.stdout.write(text); }); diff --git a/node/lib/util/print_status_util.js b/node/lib/util/print_status_util.js index 78e2ca64c..eb9ce66ec 100644 --- a/node/lib/util/print_status_util.js +++ b/node/lib/util/print_status_util.js @@ -456,6 +456,67 @@ Untracked files: return result; }; +/** + * Return a short description of the specified `status`, displaying + * paths relative to the specified `cwd`. Note that a value of "" for + * `cwd` indicates the root of the repository. + * + * @param {RepoStatus} status + * @param {String} cwd + */ +exports.printRepoStatusShort = function (status, cwd) { + assert.instanceOf(status, RepoStatus); + assert.isString(cwd); + + let result = ""; + + const indexChangesByPath = {}; + const workdirChangesByPath = {}; + const allFiles = new Set(); + + const fileStatuses = exports.accumulateStatus(status); + + const statusFlagByMode = "MAD"; + + for (const p of fileStatuses.staged) { + indexChangesByPath[p.path] = colors.green(statusFlagByMode[p.status]); + allFiles.add(p.path); + } + + for (const p of fileStatuses.workdir) { + workdirChangesByPath[p.path] = colors.red(statusFlagByMode[p.status]); + allFiles.add(p.path); + } + + for (const u of fileStatuses.untracked) { + indexChangesByPath[u] = colors.red("?"); + workdirChangesByPath[u] = colors.red("?"); + allFiles.add(u); + } + + const allFilesArray = []; + allFilesArray.push(...allFiles); + exports.sortDescriptorsByPath(allFilesArray); + for (const f of allFilesArray) { + let index = " "; + if (f in indexChangesByPath) { + index = indexChangesByPath[f]; + } + let workdir = " "; + if (f in workdirChangesByPath) { + workdir = workdirChangesByPath[f]; + } + result += index + workdir + " " + f + "\n"; + } + + if (result.length === 0) { + result = "\n"; + } + + return result; +}; + + /** * Print a string describing the status of the specified `subsToPrint`. Use * the specified `openSubs` set to determine which submodules are open. Unless diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js index 0fafc2f8e..c4b65fff1 100644 --- a/node/test/util/print_status_util.js +++ b/node/test/util/print_status_util.js @@ -722,6 +722,7 @@ A merge is in progress. On branch ${colors.green("master")}. nothing to commit, working tree clean `, + shortExact: '\n', }, "sequencer": { input: new RepoStatus({ @@ -742,12 +743,14 @@ A rebase is in progress. (use "git meta rebase --abort" to check out the original branch) nothing to commit, working tree clean `, + shortExact: '\n', }, "detached": { input: new RepoStatus({ headCommit: "ffffaaaaffffaaaa", }), regex: /detached/, + shortExact: '\n', }, "dirty meta": { input: new RepoStatus({ @@ -757,6 +760,8 @@ nothing to commit, working tree clean }, }), regex: /.*qrst/, + shortExact: `${colors.green('A')} qrst +`, }, "dirty sub": { input: new RepoStatus({ @@ -773,6 +778,31 @@ nothing to commit, working tree clean }, }), regex: /qrst\/x\/y\/z/, + shortExact: `${colors.green('A')} qrst +${colors.green('M')} qrst/x/y/z +`, + }, + "dirty-and-staged sub": { + input: new RepoStatus({ + currentBranchName: "master", + submodules: { + qrst: new Submodule({ + index: new Index(null, "a", null), + workdir: new Workdir(new RepoStatus({ + staged: { + "x/y/z": FILESTATUS.MODIFIED, + }, + workdir: { + "x/y/z": FILESTATUS.MODIFIED, + }, + }), null), + }), + }, + }), + regex: /qrst\/x\/y\/z/, + shortExact: `${colors.green('A')} qrst +${colors.green('M')}${colors.red('M')} qrst/x/y/z +`, }, "cwd": { input: new RepoStatus({ @@ -783,6 +813,8 @@ nothing to commit, working tree clean }), cwd: "u/v", regex: /\.\.\/\.\.\/qrst/, + shortExact: `${colors.green('A')} qrst +`, }, "untracked": { input: new RepoStatus({ @@ -796,6 +828,8 @@ Untracked files: \t${colors.red("foo")} +`, + shortExact: `${colors.red('?') + colors.red('?')} foo `, }, "change in sub workdir": { @@ -815,6 +849,8 @@ Changes to be committed: \t${colors.green("modified: zap")} (submodule, new commits) +`, + shortExact: `${colors.green('M')} zap `, }, }; @@ -834,6 +870,11 @@ Changes to be committed: else { assert.match(result, c.regex); } + + + const shortResult = PrintStatusUtil.printRepoStatusShort( + c.input, cwd); + assert.equal(c.shortExact, shortResult); }); }); }); From cead0afbbe3e57f13b3b776efdb051e5c7b85491 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 30 Apr 2018 18:28:43 -0400 Subject: [PATCH 126/402] fix rm arg parsing --- node/lib/cmd/rm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/rm.js b/node/lib/cmd/rm.js index b2bd38ad7..e074c3600 100644 --- a/node/lib/cmd/rm.js +++ b/node/lib/cmd/rm.js @@ -106,5 +106,5 @@ exports.executeableSubcommand = co.wrap(function *(args) { const paths = yield args.paths.map(filename => { return GitUtil.resolveRelativePath(workdir, cwd, filename); }); - yield Rm.rmPaths(repo, paths, args.recursive, args.cached, args.force); + yield Rm.rmPaths(repo, paths, args); }); From 944437c63d4855cb88ec9edad8546199fa2e7f44 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 1 May 2018 11:01:28 -0400 Subject: [PATCH 127/402] Test framework to put workdir in good sparse state Addresses: https://github.com/twosigma/git-meta/issues/569 --- node/lib/util/write_repo_ast_util.js | 39 +++++++--------------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index ed61fd007..5fb7fd88f 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -388,48 +388,25 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { repo.setHeadDetached(newHeadSha); } } - else if (null !== ast.currentBranchName) { + else if (null !== ast.currentBranchName || null !== ast.head) { // If we use NodeGit to checkout, it will not respect the // sparse-checkout settings. - const checkoutStr = `\ -git -C '${repo.workdir()}' checkout ${ast.currentBranchName} -`; - try { - yield exec(checkoutStr); - } catch (e) { - // This can fail if there is no .gitmodules file to checkout and - // it's sparse. Git will complain that it cannot do the checkout - // because the worktree is empty. + if (null === ast.currentBranchName) { + repo.detachHead(); } - } - else if (null !== ast.head) { - // If we use NodeGit to do the reset, it will not respect the sparsery - // of the repository, so we use plain Git. - repo.detachHead(); - const resetStr = `\ -git -C '${repo.workdir()}' reset --hard ${newHeadSha} + const toCheckout = ast.currentBranchName || newHeadSha; + const checkoutStr = `\ +git -C '${repo.workdir()}' checkout ${toCheckout} `; try { - yield exec(resetStr); + yield exec(checkoutStr); } catch (e) { // This can fail if there is no .gitmodules file to checkout and // it's sparse. Git will complain that it cannot do the checkout // because the worktree is empty. } - repo.detachHead(); - - // Git makes a master branch when we don't have one (even though there - // is nothing in the documentation for `reset` that says it will do - // so). So, we remove it. - - if (!("master" in ast.branches)) { - // Here, Git has created a master branch we didn't ask for; remove - // it. - - NodeGit.Reference.remove(repo, "refs/heads/master"); - } } const notes = ast.notes; @@ -512,6 +489,7 @@ git -C '${repo.workdir()}' reset --hard ${newHeadSha} yield ConflictUtil.addConflict(index, filename, conflict); } } + yield index.write(); // TODO: Firgure out if this can be done with NodeGit; extend if @@ -520,6 +498,7 @@ git -C '${repo.workdir()}' reset --hard ${newHeadSha} const filesStr = ast.sparse ? "-- .gitmodules" : "-a"; const checkoutIndexStr = `\ +git -C '${repo.workdir()}' checkout -- git -C '${repo.workdir()}' clean -f -d git -C '${repo.workdir()}' checkout-index -f ${filesStr} `; From 96742f1fd1d57b79883d0493f161064b5b969b67 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 1 May 2018 12:58:03 -0400 Subject: [PATCH 128/402] Fix issue with `Nodegit.Index.write` and sparse Apparently, when the index is written out this way, some information is recorded about what Git thinks is different between the worktree and the index. For example, take very simple setup: ```bash write-repos -=o 'a=B|x=%N:C2 s=Sa:1;H=2' cd x git meta reset git st ``` And Git will show `s` is deleted in the worktree, and curiously, that the `.gitmodules` file has changed even though `git diff` will not. I have changed all the places where we call `NodeGit.Index.write` on a meta-repo index to call into a `GitUtil.writeMetaIndex` method instead. If a repo is in sparse mode, this method runs `git checkout` on the it after calling `write()`, which seems to get Git to clear things up. --- node/lib/util/add.js | 3 ++- node/lib/util/add_submodule.js | 2 +- node/lib/util/checkout.js | 2 +- node/lib/util/cherry_pick_util.js | 2 +- node/lib/util/commit.js | 15 ++++++------ node/lib/util/git_util.js | 34 +++++++++++++++++++++++--- node/lib/util/merge_util.js | 6 ++--- node/lib/util/reset.js | 3 ++- node/lib/util/rm.js | 3 ++- node/lib/util/submodule_rebase_util.js | 2 +- 10 files changed, 52 insertions(+), 20 deletions(-) diff --git a/node/lib/util/add.js b/node/lib/util/add.js index 7d3004d88..f4aeff8a3 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -37,6 +37,7 @@ const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); +const GitUtil = require("./git_util"); const RepoStatus = require("./repo_status"); const StatusUtil = require("./status_util"); const SubmoduleUtil = require("./submodule_util"); @@ -115,6 +116,6 @@ exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update) { if (0 !== toAdd.length) { const index = yield repo.index(); yield toAdd.map(filename => index.addByPath(filename)); - yield index.write(); + yield GitUtil.writeMetaIndex(repo, index); } }); diff --git a/node/lib/util/add_submodule.js b/node/lib/util/add_submodule.js index afccdfc03..2c1482a1e 100644 --- a/node/lib/util/add_submodule.js +++ b/node/lib/util/add_submodule.js @@ -65,7 +65,7 @@ exports.addSubmodule = co.wrap(function *(repo, url, filename, importArg) { const urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); urls[filename] = url; yield SubmoduleConfigUtil.writeUrls(repo, index, urls); - yield index.write(); + yield GitUtil.writeMetaIndex(repo, index); const metaUrl = yield GitUtil.getOriginUrl(repo); const templatePath = yield SubmoduleConfigUtil.getTemplatePath(repo); diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 3efc36b98..505d27ae3 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -234,7 +234,7 @@ exports.checkoutCommit = co.wrap(function *(repo, commit, force) { repo.setHeadDetached(commit); }); yield DoWorkQueue.doInParallel(Object.keys(subs), doCheckout); - yield index.write(); + yield GitUtil.writeMetaIndex(repo, index); }); /** diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 94f489696..73a30d662 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -492,7 +492,7 @@ exports.rewriteCommit = co.wrap(function *(repo, commit) { errorMessage: errorMessage === "" ? null : errorMessage, newMetaCommit: null, }; - yield index.write(); + yield GitUtil.writeMetaIndex(repo, index); if ("" === errorMessage && (0 !== Object.keys(changes.simpleChanges).length || 0 !== Object.keys(picks.commits).length)) { diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index cbdff6e2f..61e410378 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -354,17 +354,18 @@ exports.formatEditorPrompt = function (status, cwd) { * otherwise, we could commit a staged commit in a submodule that would have * been reverted in its open repo. * - * @param {NodeGit.Index} index - * @param {Object} submodules name -> RepoStatus.Submodule + * @param {NodeGit.Repository} repo + * @param {NodeGit.Index} index + * @param {Object} submodules name -> RepoStatus.Submodule */ -const stageOpenSubmodules = co.wrap(function *(index, submodules) { +const stageOpenSubmodules = co.wrap(function *(repo, index, submodules) { yield Object.keys(submodules).map(co.wrap(function *(name) { const sub = submodules[name]; if (null !== sub.workdir) { yield index.addByPath(name); } })); - yield index.write(); + yield GitUtil.writeMetaIndex(repo, index); }); /** @@ -522,7 +523,7 @@ exports.commit = co.wrap(function *(metaRepo, } const index = yield metaRepo.index(); - yield stageOpenSubmodules(index, submodules); + yield stageOpenSubmodules(metaRepo, index, submodules); result.metaCommit = yield commitRepo(metaRepo, metaStatus.staged, @@ -602,7 +603,7 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message) { } } - yield index.write(); + yield GitUtil.writeMetaIndex(repo, index); // Use 'TreeUtil' to create a new tree having the required paths. @@ -1148,7 +1149,7 @@ exports.amendMetaRepo = co.wrap(function *(repo, if (null !== message) { const index = yield repo.index(); - yield stageOpenSubmodules(index, subs); + yield stageOpenSubmodules(repo, index, subs); yield stageFiles(repo, status.staged, index); metaCommit = yield exports.amendRepo(repo, message); } diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 663f087d9..2e37410d9 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -42,9 +42,11 @@ const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); -const ConfigUtil = require("./config_util"); -const DoWorkQueue = require("./do_work_queue"); -const UserError = require("./user_error"); +const ConfigUtil = require("./config_util"); +const DoWorkQueue = require("./do_work_queue"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); +const SubmoduleConfigUtil = require("./submodule_config_util"); +const UserError = require("./user_error"); /** * If the directory identified by the specified `dir` contains a ".git" @@ -1010,3 +1012,29 @@ exports.hashObject = co.wrap(function *(repo, data) { return res; }); +/** + * Write out the specified `index` for the specified meta-repo, `repo`. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Index} index + */ +exports.writeMetaIndex = co.wrap(function *(repo, index) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(index, NodeGit.Index); + + // If we're in sparse mode, manually set bits to skip the worktree since + // libgit2 will not. + + if (yield SparseCheckoutUtil.inSparseMode(repo)) { + const SKIP_WORKTREE = 1 << 14; + const NORMAL = 0; + for (const e of index.entries()) { + if (NORMAL === NodeGit.Index.entryStage(e) && + SubmoduleConfigUtil.modulesFileName !== index.path) { + e.flagsExtended |= SKIP_WORKTREE; + yield index.add(e); + } + } + } + yield index.write(); +}); diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index f0b7c198e..4e7fc2fdc 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -417,7 +417,7 @@ ${colors.red(commitSha)}.`); // We must write the index here or the staging we've done erlier will go // away. - yield index.write(); + yield GitUtil.writeMetaIndex(repo, index); if ("" !== errorMessage) { // We're about to fail due to conflict. First, record that there is a @@ -547,7 +547,7 @@ exports.continue = co.wrap(function *(repo) { }); const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo); yield DoWorkQueue.doInParallel(openSubs, continueSub); - yield index.write(); + yield GitUtil.writeMetaIndex(repo, index); if ("" !== errorMessage) { throw new UserError(errorMessage); @@ -616,7 +616,7 @@ exports.abort = co.wrap(function *(repo) { }); yield DoWorkQueue.doInParallel(openSubs, abortSub); yield index.conflictCleanup(); - yield index.write(); + yield GitUtil.writeMetaIndex(repo, index); yield resetMerge(repo); yield SequencerStateUtil.cleanSequencerState(repo.path()); }); diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index 8edd65b23..e7b9b75fb 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -236,9 +236,10 @@ exports.reset = co.wrap(function *(repo, commit, type) { const changedSubNames = Object.keys(changedSubs); const subsToTry = Array.from(new Set(changedSubNames.concat(openSubs))); yield DoWorkQueue.doInParallel(subsToTry, resetSubmodule); + // Write the index in case we've had to stage submodule changes. - yield index.write(); + yield GitUtil.writeMetaIndex(repo, index); }); /** diff --git a/node/lib/util/rm.js b/node/lib/util/rm.js index f49d507f9..835352f5e 100644 --- a/node/lib/util/rm.js +++ b/node/lib/util/rm.js @@ -38,6 +38,7 @@ const groupBy = require("group-by"); const path = require("path"); const NodeGit = require("nodegit"); +const GitUtil = require("./git_util"); const CloseUtil = require("./close_util"); const SubmoduleUtil = require("./submodule_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -424,7 +425,7 @@ exports.rmPaths = co.wrap(function *(repo, paths, options) { // https://github.com/nodegit/nodegit/issues/1487 if (toRemove.length !== 0) { yield index.removeAll(toRemove); - yield index.write(); + yield GitUtil.writeMetaIndex(repo, index); } // close to-be-deleted submodules diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index 7f071f16e..9a08d5d02 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -351,7 +351,7 @@ ${colors.green(rebaseInfo.onto)}.`); } }); yield DoWorkQueue.doInParallel(Object.keys(subs), continueSub); - yield index.write(); + yield GitUtil.writeMetaIndex(repo, index); const result = { errorMessage: "" === errorMessage ? null : errorMessage, commits: commits, From 87e8472bf219ba9a70210d2b6f69fbf4c8eb2cd1 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 1 May 2018 13:38:25 -0400 Subject: [PATCH 129/402] fix linting for print_status_util --- node/test/util/print_status_util.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js index c4b65fff1..03326d3bb 100644 --- a/node/test/util/print_status_util.js +++ b/node/test/util/print_status_util.js @@ -722,7 +722,7 @@ A merge is in progress. On branch ${colors.green("master")}. nothing to commit, working tree clean `, - shortExact: '\n', + shortExact: "\n", }, "sequencer": { input: new RepoStatus({ @@ -743,14 +743,14 @@ A rebase is in progress. (use "git meta rebase --abort" to check out the original branch) nothing to commit, working tree clean `, - shortExact: '\n', + shortExact: "\n", }, "detached": { input: new RepoStatus({ headCommit: "ffffaaaaffffaaaa", }), regex: /detached/, - shortExact: '\n', + shortExact: "\n", }, "dirty meta": { input: new RepoStatus({ @@ -760,7 +760,7 @@ nothing to commit, working tree clean }, }), regex: /.*qrst/, - shortExact: `${colors.green('A')} qrst + shortExact: `${colors.green("A")} qrst `, }, "dirty sub": { @@ -778,8 +778,8 @@ nothing to commit, working tree clean }, }), regex: /qrst\/x\/y\/z/, - shortExact: `${colors.green('A')} qrst -${colors.green('M')} qrst/x/y/z + shortExact: `${colors.green("A")} qrst +${colors.green("M")} qrst/x/y/z `, }, "dirty-and-staged sub": { @@ -800,8 +800,8 @@ ${colors.green('M')} qrst/x/y/z }, }), regex: /qrst\/x\/y\/z/, - shortExact: `${colors.green('A')} qrst -${colors.green('M')}${colors.red('M')} qrst/x/y/z + shortExact: `${colors.green("A")} qrst +${colors.green("M")}${colors.red("M")} qrst/x/y/z `, }, "cwd": { @@ -813,7 +813,7 @@ ${colors.green('M')}${colors.red('M')} qrst/x/y/z }), cwd: "u/v", regex: /\.\.\/\.\.\/qrst/, - shortExact: `${colors.green('A')} qrst + shortExact: `${colors.green("A")} qrst `, }, "untracked": { @@ -829,7 +829,7 @@ Untracked files: \t${colors.red("foo")} `, - shortExact: `${colors.red('?') + colors.red('?')} foo + shortExact: `${colors.red("?") + colors.red("?")} foo `, }, "change in sub workdir": { @@ -850,7 +850,7 @@ Changes to be committed: \t${colors.green("modified: zap")} (submodule, new commits) `, - shortExact: `${colors.green('M')} zap + shortExact: `${colors.green("M")} zap `, }, }; From c24da51198a2e0a9cd83d35cc51351d4396e55d5 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 1 May 2018 20:19:19 -0400 Subject: [PATCH 130/402] Skip empty commits during rebases Addresses https://github.com/twosigma/git-meta/issues/573 --- node/lib/util/submodule_rebase_util.js | 15 ++++++++++++--- node/test/util/submodule_rebase_util.js | 5 +++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index 9a08d5d02..fe9683985 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -173,9 +173,18 @@ exports.processRebase = co.wrap(function *(repo, rebase, op) { result.conflictedCommit = op.id().tostrS(); return result; // RETURN } - const newCommit = yield rebase.commit(null, signature, null); - const originalCommit = op.id().tostrS(); - result.commits[newCommit.tostrS()] = originalCommit; + let newCommit; + try { + newCommit = yield rebase.commit(null, signature, null); + } catch (e) { + // If there's nothing to commit, `NodeGit.Rebase.commit` will throw + // an error. If that's the case, we want to just ignore the + // operation and move on, as Git does. + } + if (undefined !== newCommit) { + const originalCommit = op.id().tostrS(); + result.commits[newCommit.tostrS()] = originalCommit; + } op = yield exports.callNext(rebase); } yield exports.callFinish(repo, rebase); diff --git a/node/test/util/submodule_rebase_util.js b/node/test/util/submodule_rebase_util.js index 513d70073..55063d631 100644 --- a/node/test/util/submodule_rebase_util.js +++ b/node/test/util/submodule_rebase_util.js @@ -69,6 +69,11 @@ describe("processRebase", function () { expected: "x=E:Crr-2 r=r;H=rr", conflictedCommit: null, }, + "nothing to commit": { + initial: "x=S:C2-1;Cr-1 ;Bmaster=2;Br=r", + expected: "x=E:H=2", + conflictedCommit: null, + }, "conflict": { initial: "x=S:C2-1;Cr-1 2=3;Bmaster=2;Br=r", expected: "x=E:I *2=~*2*3;W 2=u;H=2;Edetached HEAD,r,2", From a2c96a941c0e646fe1367677032ef470e308282e Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 1 May 2018 21:41:23 -0400 Subject: [PATCH 131/402] Fix reset to reset open sub heads Addresses: https://github.com/twosigma/git-meta/issues/572 --- node/lib/util/reset.js | 19 ++++++++++++++----- node/test/util/reset.js | 6 ++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index e7b9b75fb..58c078860 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -216,14 +216,23 @@ exports.reset = co.wrap(function *(repo, commit, type) { const subRepo = yield opener.getSubrepo(name); - // If the submodule wasn't changed, we'll just reset it to HEAD - let subCommit; + let subCommitSha; + if (undefined === change) { - subCommit = yield subRepo.getHeadCommit(); + // If there's no change, use what's been configured in the index. + + const entry = index.getByPath(name); + subCommitSha = entry.id.tostrS(); } else { - yield fetcher.fetchSha(subRepo, name, change.newSha); - subCommit = yield subRepo.getCommit(change.newSha); + subCommitSha = change.newSha; } + + yield fetcher.fetchSha(subRepo, name, subCommitSha); + const subCommit = yield subRepo.getCommit(subCommitSha); + + // We've already put the meta-repo index on the right commit; read it + // and reset to it. + yield NodeGit.Reset.reset(subRepo, subCommit, resetType); // Set the index to have the commit to which we just set the submodule; diff --git a/node/test/util/reset.js b/node/test/util/reset.js index 9dfe4f997..ea1d06b12 100644 --- a/node/test/util/reset.js +++ b/node/test/util/reset.js @@ -167,6 +167,12 @@ describe("reset", function () { to: "1", type: TYPE.HARD, }, + "hard with commit in sub": { + initial: "a=B:Ca-1;Ba=a|x=U:Os H=a", + expected: "x=U:Os", + to: "2", + type: TYPE.HARD, + }, "unchanged sub-repo not open": { initial: "a=B|x=U:C4-2 t=Sa:1;Bfoo=4", to: "4", From 6a1dea2bbd312e409df5e25d3804d636f06cfcef Mon Sep 17 00:00:00 2001 From: Lally Singh Date: Sun, 29 Apr 2018 01:20:38 -0400 Subject: [PATCH 132/402] Added synthic-ref-check whitelist. --- node/lib/util/synthetic_branch_util.js | 48 +++++++++++++++++++++++--- node/test/util/synthetic-branch.js | 11 ++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index b54e589e5..93dc55d70 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -54,6 +54,17 @@ function identity(v) { return v; } +function SyntheticBranchConfig(whitelistPattern) { + if (whitelistPattern.length > 0) { + const whitelistRE = new RegExp(whitelistPattern); + this.whitelistTest = function(url) { + return whitelistRE.test(url); + }; + } else { + this.whitelistTest = function() { return false; }; + } +} + /** * (This has to be public so we can mock it for testing) * @param {NodeGit.Commit} commit @@ -96,19 +107,41 @@ exports.urlToLocalPath = function *(repo, url) { } }; +/** + * Check that a given URL is on the URLs synthetic-ref-check whitelist, if + * such a whitelist exists. + * @async + * @param {SyntheticBranchConfig} cfg The configuration for + * synthetic_branch_util + * @param {String} url The configured URL of the submodule + * in the meta tree. + */ +function skipCheckForURL(cfg, url) { + assert.instanceOf(cfg, SyntheticBranchConfig); + assert.isString(url); + return cfg.whitelistTest(url); +} + /** * Check that a commit exists exists for a given submodule * at a given commit. * @async - * @param {NodeGit.Repostory} repo The meta repository - * @param {NodeGit.TreeEntry} submoduleEntry the submodule's tree entry - * @param {String} url the configured URL of the submodule + * @param {NodeGit.Repostory} repo The meta repository + * @param {SyntheticBranchConfig} cfg The configuration for + * synthetic_branch_util + * @param {NodeGit.TreeEntry} submoduleEntry the submodule's tree entry + * @param {String} url the configured URL of the submodule * in the meta tree. */ -function* checkSubmodule(repo, metaCommit, submoduleEntry, url) { +function* checkSubmodule(repo, cfg, metaCommit, submoduleEntry, url) { assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(cfg, SyntheticBranchConfig); assert.instanceOf(submoduleEntry, NodeGit.TreeEntry); + if (skipCheckForURL(cfg, url)) { + return true; + } + const localPath = yield *exports.urlToLocalPath(repo, url); const submoduleRepo = yield NodeGit.Repository.open(localPath); const submoduleCommitId = submoduleEntry.id(); @@ -128,6 +161,11 @@ function* checkSubmodules(repo, commit) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); + const config = yield repo.config(); + const whitelistPattern = ( + yield ConfigUtil.getConfigString( + config, "gitmeta.skipsyntheticrefpattern")) || ""; + const cfg = new SyntheticBranchConfig(whitelistPattern); const submodules = yield SubmoduleUtil.getSubmodulesForCommit(repo, commit, null); @@ -148,7 +186,7 @@ function* checkSubmodules(repo, commit) { const entry = yield commit.getEntry(path); const submodulePath = entry.path(); const url = submodules[submodulePath].url; - return yield *checkSubmodule(repo, commit, entry, url); + return yield *checkSubmodule(repo, cfg, commit, entry, url); }); return (yield result).every(identity); }); diff --git a/node/test/util/synthetic-branch.js b/node/test/util/synthetic-branch.js index f67712300..cf6c87aa0 100644 --- a/node/test/util/synthetic-branch.js +++ b/node/test/util/synthetic-branch.js @@ -45,6 +45,8 @@ describe("synthetic-branch", function () { const x = repos.x; const config = yield x.config(); yield config.setString("gitmeta.subreporootpath", "../../"); + yield config.setString("gitmeta.skipsyntheticrefpattern", + "whitelisted"); const head = yield x.getHeadCommit(); @@ -146,6 +148,15 @@ describe("synthetic-branch", function () { "|s=S:C8-7;C7-1;Bmaster=8", fails: true }, + // This one should fail, but is in whitelisted URIs, so it passes. + "with a submodule but no synthetic branch but ignored": { + input: "x=S:C2-1;C3-2 y=S/whitelisted:7;Bmaster=3" + + "|y=S:C4-1;Bmaster=4" + + "|u=S:C7-1;Bmaster=7", + expected: "x=S:C2-1;C3-2 y=S/whitelisted:7;Bmaster=3;" + + "N refs/notes/git-meta/subrepo-check 3=ok" + + "|y=S:C4-1;Bmaster=4|u=S:C7-1;Bmaster=7" + }, "with a submodule in a subdir, at meta commit": { input: "x=S:C2-1;C3-2 y/z=S/z:4;Bmaster=3|" + "z=S:C4-1;Bmaster=4", From 6f80f3cd8f1bfd0cb80e8fbd716ed35324be79de Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 3 May 2018 13:38:09 -0400 Subject: [PATCH 133/402] Use merge tracking branch only if remotes match Addresses: https://github.com/twosigma/git-meta/issues/577 --- node/lib/cmd/push.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/node/lib/cmd/push.js b/node/lib/cmd/push.js index b8f6a0451..af45c7022 100644 --- a/node/lib/cmd/push.js +++ b/node/lib/cmd/push.js @@ -103,20 +103,32 @@ exports.executeableSubcommand = co.wrap(function *(args) { const branch = yield repo.getCurrentBranch(); const tracking = (yield GitUtil.getTrackingInfo(repo, branch)) || {}; + // The repo is the value passed by the user, the tracking branch's remote, + // or just "origin", in order of preference. + + const remoteName = args.repository || tracking.pushRemoteName || "origin"; + let strRefspecs = []; if (0 === args.refspec.length) { + // We will use the `push.default` `upstream` strategy for now: (see + // https://git-scm.com/docs/git-config). + // That is, if there is a tracking (merge) branch configured, and the + // remote for that branch is the one we're pushing to, we'll use it. + // Otherwise, we fall back on the name of the active branch. + // + // TODO: read and adhere to the configured value for `push.default`. + const activeBranchName = branch.shorthand(); - const targetName = tracking.branchName || activeBranchName; + let targetName = activeBranchName; + if (null !== tracking.branchName && + remoteName === tracking.remoteName) { + targetName = tracking.branchName; + } strRefspecs.push(activeBranchName + ":" + targetName); } else { strRefspecs = strRefspecs.concat(args.refspec); } - // The repo is the value passed by the user, the tracking branch's remote, - // or just "origin", in order of preference. - - const remoteName = args.repository || tracking.pushRemoteName || "origin"; - yield strRefspecs.map(co.wrap(function *(strRefspec) { const refspec = GitUtil.parseRefspec(strRefspec); const force = args.force || refspec.force || false; From ad8450d12a6bb40c635c098c85179ef06315d05d Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 6 May 2018 09:50:10 -0400 Subject: [PATCH 134/402] Use work queue when committing paths --- node/lib/util/commit.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 61e410378..ca3a2b0a2 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -660,7 +660,7 @@ exports.commitPaths = co.wrap(function *(repo, status, message) { const committedSubs = {}; // map from name to RepoAST.Submodule const subs = status.submodules; - yield Object.keys(subs).map(co.wrap(function *(subName) { + const writeSubPaths = co.wrap(function *(subName) { const sub = subs[subName]; const workdir = sub.workdir; @@ -692,7 +692,9 @@ exports.commitPaths = co.wrap(function *(repo, status, message) { headCommit: sha, }), Submodule.COMMIT_RELATION.SAME) }); - })); + }); + + yield DoWorkQueue.doInParallel(Object.keys(subs), writeSubPaths); // We need a `RepoStatus` object containing only the set of the submodules // to commit to pass to `writeRepoPaths`. From a760c1fd56988233449269559c8badc32ac68a63 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 6 May 2018 12:36:41 -0400 Subject: [PATCH 135/402] Support for executable bit in test framework - Switched `RepoAST` to expect a `File` object where before it just used string - Shorthand parser support for executable bit -- when "+" is the first character in the data for a file - Test framework support for reading and writing executable state --- node/lib/util/read_repo_ast_util.js | 21 +- node/lib/util/repo_ast.js | 80 ++++++- node/lib/util/repo_ast_util.js | 34 +-- node/lib/util/shorthand_parser_util.js | 30 ++- node/lib/util/write_repo_ast_util.js | 22 +- node/test/util/read_repo_ast_util.js | 305 +++++++++++++++++++----- node/test/util/repo_ast.js | 149 +++++++----- node/test/util/repo_ast_util.js | 153 +++++++----- node/test/util/shorthand_parser_util.js | 145 +++++++---- node/test/util/submodule_rebase_util.js | 3 +- node/test/util/write_repo_ast_util.js | 8 + 11 files changed, 675 insertions(+), 275 deletions(-) diff --git a/node/lib/util/read_repo_ast_util.js b/node/lib/util/read_repo_ast_util.js index a45e91de3..f45715267 100644 --- a/node/lib/util/read_repo_ast_util.js +++ b/node/lib/util/read_repo_ast_util.js @@ -48,6 +48,9 @@ const SparseCheckoutUtil = require("./sparse_checkout_util"); const SequencerStateUtil = require("./sequencer_state_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); +const FILEMODE = NodeGit.TreeEntry.FILEMODE; +const File = RepoAST.File; + /** * Load the submodules objects from the specified `repo` on the specified * `commitId`. @@ -96,8 +99,15 @@ const loadIndexAndWorkdir = co.wrap(function *(repo, headCommit) { } catch (e) { // no file } + let isExecutable = false; + try { + yield fs.access(absPath, fs.constants.X_OK); + isExecutable = true; + } catch (e) { + // cannot execute + } if (undefined !== data) { - workdir[filePath] = data; + workdir[filePath] = new File(data, isExecutable); } }); @@ -106,11 +116,12 @@ const loadIndexAndWorkdir = co.wrap(function *(repo, headCommit) { return null; // RETURN } const oid = entry.id; - if (NodeGit.TreeEntry.FILEMODE.COMMIT === entry.mode) { + if (FILEMODE.COMMIT === entry.mode) { return new RepoAST.Submodule("", oid.tostrS()); } + const isExecutable = FILEMODE.EXECUTABLE === entry.mode; const blob = yield repo.getBlob(oid); - return blob.toString(); + return new File(blob.toString(), isExecutable); }); const stats = yield repo.getStatusExt(); @@ -305,8 +316,10 @@ exports.readRAST = co.wrap(function *(repo) { else if (!(path in submodules)) { // Skip submodules; we handle them later. const entry = yield commit.getEntry(path); + const isExecutable = + FILEMODE.EXECUTABLE === entry.filemode(); const blob = yield entry.getBlob(); - changes[path] = blob.toString(); + changes[path] = new File(blob.toString(), isExecutable); } } diff --git a/node/lib/util/repo_ast.js b/node/lib/util/repo_ast.js index 119ba33cb..f58739d0a 100644 --- a/node/lib/util/repo_ast.js +++ b/node/lib/util/repo_ast.js @@ -40,6 +40,61 @@ const deepCopy = require("deepcopy"); const Rebase = require("./rebase"); const SequencerState = require("./sequencer_state"); +/** + * @class {File} + * + * This class represents a file in a repository. + */ +class File { + /** + * Create a `File` object having the specified `contents` and + * `isExecutable` bit. + * + * @constructor + * @param {String} contents + * @param {Bool} isExecutable + */ + constructor(contents, isExecutable) { + assert.isString(contents); + assert.isBoolean(isExecutable); + this.d_contents = contents; + this.d_isExecutable = isExecutable; + Object.freeze(this); + } + + /** + * @property {String} contents the contents of the file + */ + get contents() { + return this.d_contents; + } + + /** + * @property {Bool} isExecutable true if the file is executable + */ + get isExecutable() { + return this.d_isExecutable; + } + + /** + * Return true if this object represents the same value as the specified + * `rhs` and false otherwise. Two `File` objects represent the same + * value if they have the same `contents` and `isExecutable` properties. + * + * @param {File} rhs + * @return {Bool} + */ + equal(rhs) { + return this.d_contents === rhs.d_contents && + this.d_isExecutable === rhs.d_isExecutable; + } +} + +File.prototype.toString = function () { + return `File(content=${this.d_contents}, \ +isExecutable=${this.d_isExecutable})`; +}; + /** * @class {Branch} * @@ -152,13 +207,13 @@ class Conflict { */ constructor(ancestor, our, their) { assert(null === ancestor || - "string" === typeof ancestor || + ancestor instanceof File || ancestor instanceof Submodule, ancestor); assert(null === our || - "string" === typeof our || + our instanceof File || our instanceof Submodule, our); assert(null === their || - "string" === typeof their || + their instanceof File || their instanceof Submodule, their); this.d_ancestor = ancestor; this.d_our = our; @@ -246,12 +301,12 @@ class Commit { if ("changes" in args) { assert.isObject(args.changes); for (let path in args.changes) { - const content = args.changes[path]; - - if (null !== content && !(content instanceof Submodule)) { - assert.isString(content, path); - } - this.d_changes[path] = content; + const file = args.changes[path]; + assert(file === null || + file instanceof File || + file instanceof Submodule, + `commit change at ${path} has invalid content ${file}`); + this.d_changes[path] = file; } } this.d_message = ""; @@ -609,7 +664,9 @@ in commit ${id}.`); assert.isFalse(this.d_bare); const change = workdir[path]; if (null !== change) { - assert.isString(change); + assert.instanceOf(change, + File, + `workdir change at ${path}`); } this.d_workdir[path] = change; } @@ -625,7 +682,7 @@ in commit ${id}.`); assert.isFalse(this.d_bare); const change = index[path]; assert(null === change || - "string" === typeof change || + change instanceof File || change instanceof Submodule || change instanceof Conflict, `Invalid value in index for ${path} -- ${change}`); @@ -912,6 +969,7 @@ in commit ${id}.`); AST.Branch = Branch; AST.Commit = Commit; AST.Conflict = Conflict; +AST.File = File; AST.Rebase = Rebase; AST.Remote = Remote; AST.SequencerState = SequencerState; diff --git a/node/lib/util/repo_ast_util.js b/node/lib/util/repo_ast_util.js index 15fc1707f..0b894e7ac 100644 --- a/node/lib/util/repo_ast_util.js +++ b/node/lib/util/repo_ast_util.js @@ -136,25 +136,25 @@ function diffChanges(actual, expected) { const actualChange = actual[path]; const expectedChange = expected[path]; let different; - if ("string" === typeof actualChange && - "string" === typeof expectedChange && - expectedChange.startsWith("^")) { - const exp = expectedChange.substr(1); - const matcher = new RegExp(exp); - const match = matcher.exec(actualChange); - different = null === match; - } else { - different = actualChange !== expectedChange; - } - if (different && - actualChange instanceof RepoAST.Submodule && - expectedChange instanceof RepoAST.Submodule) { + if (actualChange instanceof RepoAST.File && + expectedChange instanceof RepoAST.File) { + if (expectedChange.contents.startsWith("^")) { + const exp = expectedChange.contents.substr(1); + const matcher = new RegExp(exp); + const match = matcher.exec(actualChange.contents); + different = null === match || + actualChange.isExecutable !== expectedChange.isExecutable; + } else { + different = !actualChange.equal(expectedChange); + } + } else if (actualChange instanceof RepoAST.Submodule && + expectedChange instanceof RepoAST.Submodule) { different = !actualChange.equal(expectedChange); - } - else if (different && - actualChange instanceof RepoAST.Conflict && - expectedChange instanceof RepoAST.Conflict) { + } else if (actualChange instanceof RepoAST.Conflict && + expectedChange instanceof RepoAST.Conflict) { different = !actualChange.equal(expectedChange); + } else { + different = actualChange !== null || expectedChange !== null; } if (different) { result.push(`\ diff --git a/node/lib/util/shorthand_parser_util.js b/node/lib/util/shorthand_parser_util.js index 4d274cfef..9a2ce761b 100644 --- a/node/lib/util/shorthand_parser_util.js +++ b/node/lib/util/shorthand_parser_util.js @@ -35,6 +35,7 @@ const assert = require("chai").assert; const RepoAST = require("../util/repo_ast"); const RepoASTUtil = require("../util/repo_ast_util"); +const File = RepoAST.File; const SequencerState = RepoAST.SequencerState; /** @@ -70,11 +71,12 @@ const SequencerState = RepoAST.SequencerState; * current branch = '*=' * new commit = 'C'[message#]['-'(,)*] * [' '(',\s*'*)] - * change = path ['=' | "~" | ] | + * change = path ['=' | "~" | ] | * *path='*''*' - * conflict item = | + * conflict item = | * path = (|'/')+ * submodule = Surl:[] + * file = (['^'] | ['+']) * data = ('0-9'|'a-z'|'A-Z'|' ')* basically non-delimiter ascii * remote = R=[] * [' '=[](',\s*'=[])*] @@ -129,10 +131,14 @@ const SequencerState = RepoAST.SequencerState; * commit specifies removal of the branch from the remote in the base repo. * * A change generally indicates: - * - textual data + * - a file * - a submodule definition * - if the '=' is omitted, a deletion * - if `~` removes the change from the base, if an override + * - Use '+' at the beginning of the data to indicate an executable file. + * - When the text for the file in shorthand used to specify *expected* values + * starts with '^', the rest of the content will be treated as a regular + * expression when validating the actual contents. * * An Index override indicates changes staged in the repository index. * @@ -194,6 +200,8 @@ const SequencerState = RepoAST.SequencerState; * as conflicted, having a base content of 'a', * "our" content as 'b', and "their" content as * 'c'. + * S:I foo=+bar -- A file named foo with its executable bit set + * and the contents of "bar" has been staged. * QR a:refs/heads/master q: 1 c,d,a * -- A sequencer is in progress that is a * -- rebase. When the rebase started, HEAD @@ -308,7 +316,13 @@ function parseChangeData(commitData) { } const end = commitData.length; if (0 === end || "S" !== commitData[0]) { - return commitData; // RETURN + let contents = commitData; + let isExecutable = false; + if (contents.startsWith("+")) { + isExecutable = true; + contents = contents.substr(1); + } + return new RepoAST.File(contents, isExecutable); // RETURN } assert(1 !== commitData.length); return parseSubmodule(commitData.substr(1)); @@ -797,7 +811,7 @@ function parseOverrides(shorthand, begin, end, delimiter) { changes = parseChanges(parentsEnd + 1, end); } else { - changes[commitId] = commitId; + changes[commitId] = new File(commitId, false); } commits[commitId] = new RepoAST.Commit({ parents: parents, @@ -1159,7 +1173,7 @@ function getBaseRepo(type, data) { if ("A" === type) { let commits = {}; let changes = {}; - changes[data] = data; + changes[data] = new File(data, false); commits[data] = new RepoAST.Commit({ changes: changes, message: `changed ${data}`, @@ -1529,7 +1543,7 @@ exports.RepoType = (() => { commits: { "1": new RepoAST.Commit({ changes: { - "README.md": "hello world" + "README.md": new File("hello world", false), }, message: "the first commit", }), @@ -1553,7 +1567,7 @@ exports.RepoType = (() => { commits: { "1": new RepoAST.Commit({ changes: { - "README.md": "hello world", + "README.md": new File("hello world", false), }, message: "the first commit", }), diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index 5fb7fd88f..99ea61021 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -124,11 +124,12 @@ const writeTree = co.wrap(function *(repo, pathToChange[filename] = null; } - else if ("string" === typeof entry) { - // A string is just plain data. - - const id = (yield GitUtil.hashObject(repo, entry)).tostrS(); - pathToChange[filename] = new TreeUtil.Change(id, FILEMODE.BLOB); + else if (entry instanceof RepoAST.File) { + const id = + (yield GitUtil.hashObject(repo, entry.contents)).tostrS(); + const mode = + entry.isExecutable ? FILEMODE.EXECUTABLE : FILEMODE.BLOB; + pathToChange[filename] = new TreeUtil.Change(id, mode); } else if (entry instanceof RepoAST.Submodule) { // For submodules, we must map the logical sha it contains to the @@ -328,9 +329,9 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) { const sha = commitMap[data.sha]; return new ConflictUtil.ConflictEntry(FILEMODE.COMMIT, sha); } - const id = yield GitUtil.hashObject(repo, data); - const BLOB = FILEMODE.BLOB; - return new ConflictUtil.ConflictEntry(BLOB, id.tostrS()); + const id = yield GitUtil.hashObject(repo, data.contents); + const mode = data.isExecutable ? FILEMODE.EXECUTABLE : FILEMODE.BLOB; + return new ConflictUtil.ConflictEntry(mode, id.tostrS()); }); const makeRef = co.wrap(function *(name, commit) { @@ -516,7 +517,10 @@ git -C '${repo.workdir()}' checkout-index -f ${filesStr} else { const dirname = path.dirname(absPath); mkdirp.sync(dirname); - yield fs.writeFile(absPath, change); + yield fs.writeFile(absPath, change.contents); + if (change.isExecutable) { + yield fs.chmod(absPath, "755"); + } } } } diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index 2c3d910e7..7d9044338 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -49,6 +49,7 @@ const SubmoduleConfigUtil = require("../../lib/util/submodule_config_util"); const TestUtil = require("../../lib/util/test_util"); const CommitAndRef = SequencerState.CommitAndRef; +const File = RepoAST.File; // Test utilities @@ -66,7 +67,7 @@ const astFromSimpleRepo = co.wrap(function *(repo) { const commit = headId.id().tostrS(); let commits = {}; commits[commit] = new RepoAST.Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); return new RepoAST({ @@ -102,14 +103,14 @@ const repoWithCommit = co.wrap(function *() { const secondCommit = anotherCommit.id().tostrS(); let commits = {}; commits[firstCommit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); commits[secondCommit] = new Commit({ parents: [firstCommit], changes: { - "README.md": "bleh", - "foobar": "meh", + "README.md": new File("bleh", false), + "foobar": new File("meh", false), }, message: "message\n", }); @@ -151,11 +152,11 @@ const repoWithDeeperCommits = co.wrap(function *() { let commits = {}; commits[firstCommit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); commits[secondCommit] = new Commit({ - changes: { "README.md": "bleh" }, + changes: { "README.md": new File("bleh", false) }, parents: [firstCommit], message: "message\n", }); @@ -215,7 +216,7 @@ describe("readRAST", function () { const commit = headId.id().tostrS(); let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -238,7 +239,7 @@ describe("readRAST", function () { let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -267,12 +268,12 @@ describe("readRAST", function () { let commits = {}; commits[firstSha] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); commits[secondSha] = new Commit({ parents: [firstSha], - changes: { "foo/bar": "meh" }, + changes: { "foo/bar": new File("meh", false) }, message: "message\n", }); const expected = new RepoAST({ @@ -297,7 +298,7 @@ describe("readRAST", function () { const delCommit = yield TestUtil.makeCommit(r, []); const delSha = delCommit.id().tostrS(); commits[headSha] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); commits[delSha] = new Commit({ @@ -325,7 +326,7 @@ describe("readRAST", function () { const commit = headId.id().tostrS(); let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -344,7 +345,7 @@ describe("readRAST", function () { const commit = headId.id().tostrS(); let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -383,7 +384,7 @@ describe("readRAST", function () { const commit = headId.id().tostrS(); let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -442,7 +443,7 @@ describe("readRAST", function () { const commit = headId.id().tostrS(); let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const path = repos.bare.path(); @@ -476,7 +477,7 @@ describe("readRAST", function () { const commit = headId.tostrS(); let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const workdir = base.workdir(); @@ -509,7 +510,7 @@ describe("readRAST", function () { const commit = headId.id().tostrS(); let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -542,7 +543,7 @@ describe("readRAST", function () { yield SubmoduleConfigUtil.deinit(repo, "x/y"); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ - changes: {"README.md":""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); commits[commit.id().tostrS()] = new Commit({ @@ -590,7 +591,7 @@ describe("readRAST", function () { yield SubmoduleConfigUtil.deinit(repo, "x/y"); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ - changes: {"README.md":""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); commits[commit.id().tostrS()] = new Commit({ @@ -642,7 +643,7 @@ describe("readRAST", function () { let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ - changes: {"README.md":""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); commits[commit.id().tostrS()] = new Commit({ @@ -688,7 +689,7 @@ describe("readRAST", function () { let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -696,7 +697,7 @@ describe("readRAST", function () { branches: { "master": new RepoAST.Branch(commit, null), }, head: commit, currentBranchName: "master", - index: { "README.md": "foo" }, + index: { "README.md": new File("foo", false) }, }); const ast = yield ReadRepoASTUtil.readRAST(r); RepoASTUtil.assertEqualASTs(ast, expected); @@ -716,7 +717,7 @@ describe("readRAST", function () { let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -724,7 +725,7 @@ describe("readRAST", function () { branches: { "master": new RepoAST.Branch(commit, null), }, head: commit, currentBranchName: "master", - index: { "foo": "foo" }, + index: { "foo": new File("foo", false), }, }); const ast = yield ReadRepoASTUtil.readRAST(r); RepoASTUtil.assertEqualASTs(ast, expected); @@ -746,7 +747,7 @@ describe("readRAST", function () { let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -754,7 +755,7 @@ describe("readRAST", function () { branches: { "master": new RepoAST.Branch(commit, null), }, head: commit, currentBranchName: "master", - index: { "foo": "foo" }, + index: { "foo": new File("foo", false), }, workdir: { "foo": null }, }); const ast = yield ReadRepoASTUtil.readRAST(r); @@ -775,7 +776,7 @@ describe("readRAST", function () { let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -803,7 +804,7 @@ describe("readRAST", function () { let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ - changes: {"README.md":""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -848,7 +849,7 @@ describe("readRAST", function () { let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ - changes: {"README.md":""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); commits[commit.id().tostrS()] = new Commit({ @@ -881,7 +882,7 @@ describe("readRAST", function () { yield fs.unlink(path.join(r.workdir(), "README.md")); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ - changes: {"README.md":""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -903,7 +904,7 @@ describe("readRAST", function () { yield fs.appendFile(path.join(r.workdir(), "foo"), "x"); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ - changes: {"README.md":""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -913,7 +914,7 @@ describe("readRAST", function () { }, currentBranchName: "master", head: headCommit.id().tostrS(), - workdir: { foo: "x" }, + workdir: { foo: new File("x", false), }, }); const actual = yield ReadRepoASTUtil.readRAST(r); RepoASTUtil.assertEqualASTs(actual, expected); @@ -925,7 +926,7 @@ describe("readRAST", function () { yield fs.appendFile(path.join(r.workdir(), "README.md"), "x"); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ - changes: {"README.md":""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -935,7 +936,7 @@ describe("readRAST", function () { }, currentBranchName: "master", head: headCommit.id().tostrS(), - workdir: { "README.md": "x" }, + workdir: { "README.md": new File("x", false) }, }); const actual = yield ReadRepoASTUtil.readRAST(r); RepoASTUtil.assertEqualASTs(actual, expected); @@ -951,7 +952,7 @@ describe("readRAST", function () { yield fs.appendFile(path.join(r.workdir(), "README.md"), "y"); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ - changes: {"README.md":""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -961,8 +962,8 @@ describe("readRAST", function () { }, currentBranchName: "master", head: headCommit.id().tostrS(), - index: { "README.md": "x" }, - workdir: { "README.md": "xy" }, + index: { "README.md": new File("x", false), }, + workdir: { "README.md": new File("xy", false), }, }); const actual = yield ReadRepoASTUtil.readRAST(r); RepoASTUtil.assertEqualASTs(actual, expected); @@ -1026,22 +1027,22 @@ describe("readRAST", function () { const commits = {}; const Commit = RepoAST.Commit; commits[firstSha] = new Commit({ - changes: { "README.md": "", }, + changes: { "README.md": new File("", false)}, message: "first commit", }); commits[bSha] = new Commit({ parents: [firstSha], - changes: { foo: "foo" }, + changes: { foo: new File("foo", false) }, message: "message\n", }); commits[cSha] = new Commit({ parents: [firstSha], - changes: { bar: "bar" }, + changes: { bar: new File("bar", false) }, message: "message\n", }); commits[mergeSha] = new Commit({ parents: [cSha, bSha], - changes: { foo: "foo" }, + changes: { foo: new File("foo", false) }, message: "Merge branch 'b'", }); const expected = new RepoAST({ @@ -1146,23 +1147,23 @@ describe("readRAST", function () { const subCommits = {}; subCommits[baseMasterSha] = new Commit({ - changes: { "README.md": "", }, + changes: { "README.md": new File("", false)}, message: "first commit", }); subCommits[fooSha] = new Commit({ parents: [baseMasterSha], - changes: { foo: "foo" }, + changes: { foo: new File("foo", false), }, message: "message\n", }); subCommits[barSha] = new Commit({ parents: [baseMasterSha], - changes: { bar: "bar" }, + changes: { bar: new File("bar", false) }, message: "message\n", }); const commits = {}; commits[firstSha] = new Commit({ - changes: { "README.md": "", }, + changes: { "README.md": new File("", false)}, message: "first commit", }); commits[subSha] = new Commit({ @@ -1298,23 +1299,23 @@ describe("readRAST", function () { const subCommits = {}; subCommits[baseMasterSha] = new Commit({ - changes: { "README.md": "", }, + changes: { "README.md": new File("", false)}, message: "first commit", }); subCommits[fooSha] = new Commit({ parents: [baseMasterSha], - changes: { foo: "foo" }, + changes: { foo: new File("foo", false) }, message: "message\n", }); subCommits[barSha] = new Commit({ parents: [baseMasterSha], - changes: { bar: "bar" }, + changes: { bar: new File("bar", false) }, message: "message\n", }); const commits = {}; commits[firstSha] = new Commit({ - changes: { "README.md": "", }, + changes: { "README.md": new File("", false)}, message: "first commit", }); commits[subSha] = new Commit({ @@ -1379,7 +1380,7 @@ describe("readRAST", function () { const commit = headId.tostrS(); let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const note = {}; @@ -1586,7 +1587,7 @@ describe("readRAST", function () { const commit = headId.id().tostrS(); let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -1637,7 +1638,7 @@ describe("readRAST", function () { }; subCommits[subCommit.id().tostrS()] = new RepoAST.Commit({ changes: { - "README.md": "data", + "README.md": new File("data", false), }, message: "message\n", }); @@ -1646,7 +1647,7 @@ describe("readRAST", function () { const commit = headId.id().tostrS(); let commits = {}; commits[commit] = new Commit({ - changes: { "README.md": ""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); const expected = new RepoAST({ @@ -1713,10 +1714,12 @@ describe("readRAST", function () { const simple = yield astFromSimpleRepo(repo); const expected = simple.copy({ index: { - "README.md": new RepoAST.Conflict("xxx", "yyy", "zzz"), + "README.md": new RepoAST.Conflict(new File("xxx", false), + new File("yyy", false), + new File("zzz", false)), }, workdir: { - "README.md": "conflicted", + "README.md": new File("conflicted", false), }, }); RepoASTUtil.assertEqualASTs(result, expected); @@ -1740,10 +1743,12 @@ describe("readRAST", function () { const simple = yield astFromSimpleRepo(repo); const expected = simple.copy({ index: { - "README.md": new RepoAST.Conflict("xxx", null, "zzz"), + "README.md": new RepoAST.Conflict(new File("xxx", false), + null, + new File("zzz", false)), }, workdir: { - "README.md": "conflicted", + "README.md": new File("conflicted", false), }, }); RepoASTUtil.assertEqualASTs(result, expected); @@ -1788,7 +1793,7 @@ describe("readRAST", function () { yield index.write(); let commits = {}; commits[sha] = new Commit({ - changes: {"README.md":""}, + changes: { "README.md": new File("", false)}, message: "first commit", }); commits[commit.id().tostrS()] = new Commit({ @@ -1846,5 +1851,189 @@ describe("readRAST", function () { assert.equal(result.sparse, true); assert.deepEqual(result.workdir, {}); })); + it("workdir exec bit change", co.wrap(function *() { + const r = yield TestUtil.createSimpleRepository(); + + // Make readme executable + yield fs.chmod(path.join(r.workdir(), "README.md"), "755"); + + const ast = yield ReadRepoASTUtil.readRAST(r); + const headId = yield r.getHeadCommit(); + const commit = headId.id().tostrS(); + let commits = {}; + commits[commit] = new Commit({ + changes: { "README.md": new File("", false)}, + message: "first commit", + }); + + const expected = new RepoAST({ + commits: commits, + branches: { "master": new RepoAST.Branch(commit, null), }, + head: commit, + currentBranchName: "master", + workdir: { + "README.md": new File("", true), + }, + }); + RepoASTUtil.assertEqualASTs(ast, expected); + })); + it("new, executable file", co.wrap(function *() { + const r = yield TestUtil.createSimpleRepository(); + + // Make readme executable + const filePath = path.join(r.workdir(), "foo"); + yield fs.writeFile(filePath, "meh"); + yield fs.chmod(filePath, "755"); + + const ast = yield ReadRepoASTUtil.readRAST(r); + const headId = yield r.getHeadCommit(); + const commit = headId.id().tostrS(); + let commits = {}; + commits[commit] = new Commit({ + changes: { "README.md": new File("", false)}, + message: "first commit", + }); + + const expected = new RepoAST({ + commits: commits, + branches: { "master": new RepoAST.Branch(commit, null), }, + head: commit, + currentBranchName: "master", + workdir: { + foo: new File("meh", true), + }, + }); + RepoASTUtil.assertEqualASTs(ast, expected); + })); + it("executable change in index", co.wrap(function *() { + const r = yield TestUtil.createSimpleRepository(); + + // Make readme executable and stage it + yield fs.chmod(path.join(r.workdir(), "README.md"), "755"); + const index = yield r.index(); + yield index.addByPath("README.md"); + + const ast = yield ReadRepoASTUtil.readRAST(r); + const headId = yield r.getHeadCommit(); + const commit = headId.id().tostrS(); + let commits = {}; + commits[commit] = new Commit({ + changes: { "README.md": new File("", false)}, + message: "first commit", + }); + + const expected = new RepoAST({ + commits: commits, + branches: { "master": new RepoAST.Branch(commit, null), }, + head: commit, + currentBranchName: "master", + index: { + "README.md": new File("", true), + }, + }); + RepoASTUtil.assertEqualASTs(ast, expected); + })); + it("new, executable file in index", co.wrap(function *() { + const r = yield TestUtil.createSimpleRepository(); + + // Make readme executable + const filePath = path.join(r.workdir(), "foo"); + yield fs.writeFile(filePath, "meh"); + yield fs.chmod(filePath, "755"); + const index = yield r.index(); + yield index.addByPath("foo"); + + const ast = yield ReadRepoASTUtil.readRAST(r); + const headId = yield r.getHeadCommit(); + const commit = headId.id().tostrS(); + let commits = {}; + commits[commit] = new Commit({ + changes: { "README.md": new File("", false)}, + message: "first commit", + }); + + const expected = new RepoAST({ + commits: commits, + branches: { "master": new RepoAST.Branch(commit, null), }, + head: commit, + currentBranchName: "master", + index: { + foo: new File("meh", true), + }, + }); + RepoASTUtil.assertEqualASTs(ast, expected); + })); + it("executable change in commit", co.wrap(function *() { + const r = yield TestUtil.createSimpleRepository(); + + const headId = yield r.getHeadCommit(); + const commit = headId.id().tostrS(); + + // Make readme executable and stage it + yield fs.chmod(path.join(r.workdir(), "README.md"), "755"); + const index = yield r.index(); + yield index.addByPath("README.md"); + const execCommit = yield TestUtil.makeCommit(r, ["README.md"]); + const execSha = execCommit.id().tostrS(); + + const ast = yield ReadRepoASTUtil.readRAST(r); + + let commits = {}; + commits[commit] = new Commit({ + changes: { "README.md": new File("", false)}, + message: "first commit", + }); + commits[execSha] = new Commit({ + changes: { + "README.md": new File("", true), + }, + parents: [commit], + message: "message\n", + }); + const expected = new RepoAST({ + commits: commits, + branches: { "master": new RepoAST.Branch(execSha, null), }, + head: execSha, + currentBranchName: "master", + }); + RepoASTUtil.assertEqualASTs(ast, expected); + })); + it("executable new file in commit", co.wrap(function *() { + const r = yield TestUtil.createSimpleRepository(); + + const headId = yield r.getHeadCommit(); + const commit = headId.id().tostrS(); + + // Make readme executable and stage it + const filePath = path.join(r.workdir(), "foo"); + yield fs.writeFile(filePath, "meh"); + yield fs.chmod(filePath, "755"); + const index = yield r.index(); + yield index.addByPath("foo"); + const execCommit = yield TestUtil.makeCommit(r, ["foo"]); + const execSha = execCommit.id().tostrS(); + + const ast = yield ReadRepoASTUtil.readRAST(r); + + let commits = {}; + commits[commit] = new Commit({ + changes: { "README.md": new File("", false)}, + message: "first commit", + }); + commits[execSha] = new Commit({ + changes: { + "foo": new File("meh", true), + }, + parents: [commit], + message: "message\n", + }); + const expected = new RepoAST({ + commits: commits, + branches: { "master": new RepoAST.Branch(execSha, null), }, + head: execSha, + currentBranchName: "master", + }); + RepoASTUtil.assertEqualASTs(ast, expected); + })); }); diff --git a/node/test/util/repo_ast.js b/node/test/util/repo_ast.js index bf79a8099..5cd7d9c9a 100644 --- a/node/test/util/repo_ast.js +++ b/node/test/util/repo_ast.js @@ -35,6 +35,7 @@ const assert = require("chai").assert; const RepoAST = require("../../lib/util/repo_ast"); describe("RepoAST", function () { +const File = RepoAST.File; const SequencerState = RepoAST.SequencerState; const CommitAndRef = SequencerState.CommitAndRef; const REBASE = SequencerState.TYPE.REBASE; @@ -52,6 +53,14 @@ const REBASE = SequencerState.TYPE.REBASE; }); }); + describe("File", function () { + it("breath", function () { + const f = new RepoAST.File("foo", true); + assert.equal(f.contents, "foo"); + assert.equal(f.isExecutable, true); + }); + }); + describe("Submodule", function () { it("breath", function () { const s = new RepoAST.Submodule("foo", "bar"); @@ -92,10 +101,12 @@ const REBASE = SequencerState.TYPE.REBASE; describe("Conflict", function () { it("breath", function () { - const c = new RepoAST.Conflict("foo", "bar", "baz"); - assert.equal(c.ancestor, "foo"); - assert.equal(c.our, "bar"); - assert.equal(c.their, "baz"); + const c = new RepoAST.Conflict(new File("foo", false), + new File("bar", true), + new File("baz", false)); + assert.equal(c.ancestor.contents, "foo"); + assert.equal(c.our.contents, "bar"); + assert.equal(c.their.contents, "baz"); }); it("nulls", function () { const c = new RepoAST.Conflict(null, null, null); @@ -114,25 +125,33 @@ const REBASE = SequencerState.TYPE.REBASE; }); it("equal", function () { const Conflict = RepoAST.Conflict; + const foo = new File("foo", false); + const bam = new File("bam", false); + const bar = new File("bar", false); + const baz = new File("baz", false); + const food = new File("food", true); + const bark = new File("bark", false); const cases = { "same": { - lhs: new Conflict("foo", "bar", "baz"), - rhs: new Conflict("foo", "bar", "baz"), + lhs: new Conflict(foo, bar, baz), + rhs: new Conflict(new File("foo", false), + new File("bar", false), + new File("baz", false)), expected: true, }, "diff ancestor": { - lhs: new Conflict("foo", "bar", "baz"), - rhs: new Conflict("food", "bar", "baz"), + lhs: new Conflict(foo, bar, baz), + rhs: new Conflict(food, bar, baz), expected: false, }, "diff ours": { - lhs: new Conflict("foo", "bar", "baz"), - rhs: new Conflict("foo", "bark", "baz"), + lhs: new Conflict(foo, bar, baz), + rhs: new Conflict(foo, bark, baz), expected: false, }, "diff theirs": { - lhs: new Conflict("foo", "bar", "baz"), - rhs: new Conflict("foo", "bar", "bam"), + lhs: new Conflict(foo, bar, baz), + rhs: new Conflict(foo, bar, bam), expected: false, }, }; @@ -157,11 +176,11 @@ const REBASE = SequencerState.TYPE.REBASE; "simple": { input: { parents: ["foo"], - changes: { a: "b" }, + changes: { a: new File("b", true) }, message: "bam", }, eparents: ["foo"], - echanges: { a: "b"}, + echanges: { a: new File("b", true) }, emessage: "bam", }, "delete change": { @@ -281,6 +300,11 @@ const REBASE = SequencerState.TYPE.REBASE; }; } + const barFile = new File("bar", true); + const yFile = new File("y", true); + const aConflict = new RepoAST.Conflict(new File("foo", false), + new File("bar", true), + new File("baz", false)); const cases = { "trivial": m(undefined, undefined, false), "simple" : m( @@ -299,13 +323,13 @@ const REBASE = SequencerState.TYPE.REBASE; "with bare": m({ bare: true }, { bare: true} , false), "bad bare with index": m({ bare: true, - index: { foo: "bar" }, + index: { foo: barFile }, commits: {"1": c1 }, head: "1", }, undefined, true), "bad bare with workdir": m({ bare: true, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, commits: {"1": c1 }, head: "1", }, undefined, true), @@ -446,20 +470,20 @@ const REBASE = SequencerState.TYPE.REBASE; "index": m({ commits: { "1": c1}, head: "1", - index: { foo: "bar"}, + index: { foo: barFile }, }, { commits: { "1": c1}, head: "1", - index: { foo: "bar"}, + index: { foo: barFile }, }, false), "index without head": m({ commits: { "1": c1}, head: null, - index: { foo: "bar"}, + index: { foo: barFile }, }, { commits: { "1": c1}, head: "1", - index: { foo: "bar"}, + index: { foo: barFile}, }, true), "index with submodule": m({ commits: { "1": c1}, @@ -473,31 +497,31 @@ const REBASE = SequencerState.TYPE.REBASE; "index with conflict": m({ commits: { "1": c1}, head: "1", - index: { foo: new RepoAST.Conflict("foo", "bar", "baz") }, - workdir: { foo: "bar" }, + index: { foo: aConflict }, + workdir: { foo: barFile }, }, { commits: { "1": c1}, head: "1", - index: { foo: new RepoAST.Conflict("foo", "bar", "baz") }, - workdir: { foo: "bar" }, + index: { foo: aConflict }, + workdir: { foo: barFile }, }, false), "workdir": m({ commits: { "1": c1}, head: "1", - workdir: { foo: "bar"}, + workdir: { foo: barFile }, }, { commits: { "1": c1}, head: "1", - workdir: { foo: "bar"}, + workdir: { foo: barFile }, }, false), "workdir without head": m({ commits: { "1": c1}, head: null, - workdir: { foo: "bar"}, + workdir: { foo: barFile }, }, { commits: { "1": c1}, head: "1", - workdir: { foo: "bar"}, + workdir: { foo: barFile }, }, true), "openSubmodules": m({ commits: { "1": cWithSubmodule }, @@ -516,45 +540,45 @@ const REBASE = SequencerState.TYPE.REBASE; }, true), "bad commit change": m({ commits: { - "1": new Commit({ changes: { x: "y"}}), + "1": new Commit({ changes: { x: yFile }}), "2": new Commit({ parents: ["1"], - changes: { x: "y"}, + changes: { x: yFile }, }), }, head: "2", }, {}, true), "bad commit change from ancestor": m({ commits: { - "1": new Commit({ changes: { x: "y"}}), + "1": new Commit({ changes: { x: yFile }}), "2": new Commit({ parents: ["1"], - changes: { y: "y"}, + changes: { y: yFile }, }), "3": new Commit({ parents: ["2"], - changes: { x: "y"}, + changes: { x: yFile }, }), }, head: "2", }, {}, true), "ok commit duplicting right-hand ancestory": m({ commits: { - "1": new Commit({ changes: { x: "y"}}), - "2": new Commit({ changes: { y: "y"}, }), + "1": new Commit({ changes: { x: yFile }}), + "2": new Commit({ changes: { y: yFile }, }), "3": new Commit({ parents: ["1","2"], - changes: { y: "y"}, + changes: { y: yFile }, }), }, head: "3", }, { commits: { - "1": new Commit({ changes: { x: "y"}}), - "2": new Commit({ changes: { y: "y"}, }), + "1": new Commit({ changes: { x: yFile }}), + "2": new Commit({ changes: { y: yFile }, }), "3": new Commit({ parents: ["1","2"], - changes: { y: "y"}, + changes: { y: yFile }, }), }, head: "3", @@ -735,9 +759,12 @@ const REBASE = SequencerState.TYPE.REBASE; }); }); + const barFile = new File("bar", false); + const bazFile = new File("baz", false); + describe("renderCommit", function () { const Commit = RepoAST.Commit; - const c1 = new Commit({ changes: { foo: "bar" }}); + const c1 = new Commit({ changes: { foo: barFile }}); const deleter = new Commit({ parents: ["1"], changes: { foo: null } @@ -751,9 +778,9 @@ const REBASE = SequencerState.TYPE.REBASE; "one": { commits: { "1": c1}, from: "1", - expected: { foo: "bar" }, + expected: { foo: barFile }, ecache: { - "1": { foo: "bar" }, + "1": { foo: barFile }, }, }, "deletion": { @@ -768,18 +795,18 @@ const REBASE = SequencerState.TYPE.REBASE; "with sub": { commits: { "1": c1, "2": subCommit }, from: "2", - expected: { foo: "bar", baz: submodule }, + expected: { foo: barFile, baz: submodule }, ecache: { "1": c1.changes, - "2": { foo: "bar", baz: submodule }, + "2": { foo: barFile, baz: submodule }, }, }, "use the cache": { commits: { "1": c1 }, from: "1", - cache: { "1": { foo: "baz" } }, - expected: { foo: "baz" }, - ecache: { "1": { foo: "baz"}, }, + cache: { "1": { foo: bazFile } }, + expected: { foo: bazFile }, + ecache: { "1": { foo: bazFile }, }, }, }; Object.keys(cases).forEach(caseName => { @@ -798,14 +825,15 @@ const REBASE = SequencerState.TYPE.REBASE; describe("AST.copy", function () { const Rebase = RepoAST.Rebase; + const barFile = new File("bar", false); const base = new RepoAST({ commits: { "1": new RepoAST.Commit()}, branches: { "master": new RepoAST.Branch("1", null) }, refs: { "a/b": "1"}, head: "1", currentBranchName: "master", - index: { foo: "bar" }, - workdir: { foo: "bar" }, + index: { foo: barFile }, + workdir: { foo: barFile }, rebase: new Rebase("hello", "1", "1"), sequencerState: new SequencerState({ type: REBASE, @@ -824,8 +852,8 @@ const REBASE = SequencerState.TYPE.REBASE; head: "2", currentBranchName: "foo", remotes: { "foo": new RepoAST.Remote("meeeee") }, - index: { foo: "bar" }, - workdir: { foo: "bar" }, + index: { foo: barFile }, + workdir: { foo: barFile }, rebase: new Rebase("hello world", "2", "2"), sequencerState: new SequencerState({ type: REBASE, @@ -908,26 +936,33 @@ const REBASE = SequencerState.TYPE.REBASE; // `accumulateChanges`. We just need to make sure they're put // together properly. + const bbFile = new File("bb", false); + const fooFile = new File("foo", false); + const barFile = new File("bar", true); + const zFile = new File("z", false); const Commit = RepoAST.Commit; const Conflict = RepoAST.Conflict; - const c1 = new Commit({ changes: { foo: "bar" }}); + const c1 = new Commit({ changes: { foo: barFile }}); const cases = { "no index": { commits: { "1": c1}, from: "1", - expected: { foo: "bar" }, + expected: { foo: barFile }, }, "with index": { commits: { "1": c1}, from: "1", - index: { y: "z" }, - expected: { foo: "bar", y: "z" }, + index: { y: zFile }, + expected: { foo: barFile, y: zFile }, }, "ignore conflict": { commits: { "1": c1}, from: "1", - index: { y: "z", foo: new Conflict("foo", "bar", "bb") }, - expected: { foo: "bar", y: "z" }, + index: { + y: zFile, + foo: new Conflict(fooFile, barFile, bbFile), + }, + expected: { foo: barFile, y: zFile }, }, }; Object.keys(cases).forEach(caseName => { diff --git a/node/test/util/repo_ast_util.js b/node/test/util/repo_ast_util.js index ae5c2f6fe..1a7e38f04 100644 --- a/node/test/util/repo_ast_util.js +++ b/node/test/util/repo_ast_util.js @@ -35,6 +35,10 @@ const assert = require("chai").assert; const RepoAST = require("../../lib/util/repo_ast"); const RepoASTUtil = require("../../lib/util/repo_ast_util"); +const File = RepoAST.File; +const barFile = new File("bar", false); +const bamFile = new File("bam", true); + describe("RepoAstUtil", function () { const Conflict = RepoAST.Conflict; const Sequencer = RepoAST.SequencerState; @@ -51,12 +55,24 @@ describe("RepoAstUtil", function () { "with data": { actual: new Commit({ parents: ["1"], - changes: { foo: "bar" }, + changes: { foo: barFile }, message: "foo", }), expected: new Commit({ parents: ["1"], - changes: { foo: "bar" }, + changes: { foo: barFile }, + message: "foo", + }), + }, + "with null data": { + actual: new Commit({ + parents: ["1"], + changes: { foo: null }, + message: "foo", + }), + expected: new Commit({ + parents: ["1"], + changes: { foo: null }, message: "foo", }), }, @@ -67,44 +83,44 @@ describe("RepoAstUtil", function () { "bad parents": { actual: new Commit({ parents: ["1"], - changes: { foo: "bar" }, + changes: { foo: barFile }, }), expected: new Commit({ parents: ["2"], - changes: { foo: "bar" }, + changes: { foo: barFile }, }), fails: true, }, "wrong change": { actual: new Commit({ parents: ["1"], - changes: { foo: "bar" }, + changes: { foo: barFile }, }), expected: new Commit({ parents: ["2"], - changes: { foo: "z" }, + changes: { foo: new File("z", false) }, }), fails: true, }, "extra change": { actual: new Commit({ parents: ["1"], - changes: { foo: "bar", z: "q" }, + changes: { foo: barFile, z: new File("q", false) }, }), expected: new Commit({ parents: ["2"], - changes: { foo: "bar" }, + changes: { foo: barFile }, }), fails: true, }, "missing change": { actual: new Commit({ parents: ["1"], - changes: { foo: "bar" }, + changes: { foo: barFile }, }), expected: new Commit({ parents: ["1"], - changes: { foo: "bar", k: "z" }, + changes: { foo: barFile, k: new File("z", false) }, }), fails: true, }, @@ -141,7 +157,7 @@ describe("RepoAstUtil", function () { const Remote = AST.Remote; const Submodule = AST.Submodule; - const aCommit = new Commit({ changes: { x: "y" } }); + const aCommit = new Commit({ changes: { x: new File("y", true) } }); const aRemote = new Remote("/z"); const aSubmodule = new Submodule("/y", "1"); const anAST = new RepoAST({ @@ -167,7 +183,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, rebase: new Rebase("foo", "1", "1"), sequencerState: new Sequencer({ @@ -188,7 +204,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, rebase: new Rebase("foo", "1", "1"), sequencerState: new Sequencer({ @@ -210,14 +226,16 @@ describe("RepoAstUtil", function () { "wrong commit": { actual: new AST({ commits: { - "1": new Commit({ changes: { x: "z" } }), + "1": new Commit({ + changes: { x: new File("z", false) } + }), }, branches: { master: new RepoAST.Branch("1", null) }, head: "1", currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), expected: new AST({ @@ -227,7 +245,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), fails: true, @@ -240,7 +258,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), expected: new AST({ @@ -253,7 +271,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), fails: true, @@ -267,7 +285,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), expected: new AST({ @@ -278,7 +296,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), fails: true, @@ -293,7 +311,7 @@ describe("RepoAstUtil", function () { head: "2", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), expected: new AST({ @@ -305,7 +323,7 @@ describe("RepoAstUtil", function () { head: "1", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), fails: true, @@ -320,7 +338,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), expected: new AST({ @@ -329,7 +347,7 @@ describe("RepoAstUtil", function () { head: "1", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), fails: true, @@ -342,7 +360,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote, yyyy: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), expected: new AST({ @@ -352,7 +370,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), fails: true, @@ -364,8 +382,8 @@ describe("RepoAstUtil", function () { head: "1", currentBranchName: "master", remotes: { origin: aRemote }, - index: { y: aSubmodule, x: "xxxx" }, - workdir: { foo: "bar" }, + index: { y: aSubmodule, x: new File("xxxx", false) }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), expected: new AST({ @@ -375,38 +393,47 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), fails: true, }, "wrong file data": { actual: new AST({ - index: { foo: "bar", }, + index: { foo: barFile, }, }), expected: new AST({ - index: { foo: "baz", }, + index: { foo: new File("baz", false), }, }), fails: true, }, "regex match data miss": { actual: new AST({ - index: { foo: "bar", }, + index: { foo: barFile, }, }), expected: new AST({ - index: { foo: "^^ar", }, + index: { foo: new File("^^ar", false) }, }), fails: true, }, "regex match data hit": { actual: new AST({ - index: { foo: "bar", }, + index: { foo: barFile, }, }), expected: new AST({ - index: { foo: "^^ba", }, + index: { foo: new File("^^ba", false), }, }), fails: false, }, + "regex match data hit but bad bit": { + actual: new AST({ + index: { foo: barFile, }, + }), + expected: new AST({ + index: { foo: new File("^^ba", true), }, + }), + fails: true, + }, "bad workdir": { actual: new AST({ commits: { "1": aCommit}, @@ -415,7 +442,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), expected: new AST({ @@ -425,7 +452,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { oo: "bar" }, + workdir: { oo: barFile }, openSubmodules: { y: anAST }, }), fails: true, @@ -438,7 +465,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, }), expected: new AST({ commits: { "1": aCommit}, @@ -447,7 +474,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), fails: true, @@ -460,7 +487,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: anAST }, }), expected: new AST({ @@ -470,7 +497,7 @@ describe("RepoAstUtil", function () { currentBranchName: "master", remotes: { origin: aRemote }, index: { y: aSubmodule }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, openSubmodules: { y: new AST({ commits: { "4": aCommit }, head: "4", @@ -611,36 +638,44 @@ describe("RepoAstUtil", function () { "same Conflict": { actual: new AST({ index: { - "foo": new Conflict("foo", "bar", "baz"), + "foo": new Conflict(new File("foo", false), + new File("bar", false), + new File("baz", true)), }, workdir: { - "foo": "boo", + "foo": new File("boo", false), }, }), expected: new AST({ index: { - "foo": new Conflict("foo", "bar", "baz"), + "foo": new Conflict(new File("foo", false), + new File("bar", false), + new File("baz", true)), }, workdir: { - "foo": "boo", + "foo": new File("boo", false), }, }), }, "diff Conflict": { actual: new AST({ index: { - "foo": new Conflict("foo", "bar", "baz"), + "foo": new Conflict(new File("foo", false), + new File("bar", false), + new File("baz", true)), }, workdir: { - "foo": "boo", + "foo": new File("boo", false), }, }), expected: new AST({ index: { - "foo": new Conflict("foo", "bar", "bam"), + "foo": new Conflict(new File("foo", true), + new File("bar", false), + new File("baz", true)), }, workdir: { - "foo": "boo", + "foo": new File("boo", false), }, }), fails: true, @@ -890,7 +925,7 @@ describe("RepoAstUtil", function () { remotes: { foo: new RepoAST.Remote("my-url"), }, - index: { foo: "bar" }, + index: { foo: barFile }, }), m: { "1": "2"}, e: new RepoAST({ @@ -899,7 +934,7 @@ describe("RepoAstUtil", function () { remotes: { foo: new RepoAST.Remote("my-url"), }, - index: { foo: "bar" }, + index: { foo: barFile }, }), }, "index unchanged submodule": { @@ -910,7 +945,7 @@ describe("RepoAstUtil", function () { foo: new RepoAST.Remote("my-url"), }, index: { - foo: "bar", + foo: barFile, baz: new RepoAST.Submodule("x", "y"), }, }), @@ -923,7 +958,7 @@ describe("RepoAstUtil", function () { foo: new RepoAST.Remote("my-url"), }, index: { - foo: "bar", + foo: barFile, baz: new RepoAST.Submodule("x", "y"), }, }), @@ -936,7 +971,7 @@ describe("RepoAstUtil", function () { foo: new RepoAST.Remote("my-url"), }, index: { - foo: "bar", + foo: barFile, baz: new RepoAST.Submodule("q", "1"), }, }), @@ -949,7 +984,7 @@ describe("RepoAstUtil", function () { foo: new RepoAST.Remote("my-url"), }, index: { - foo: "bar", + foo: barFile, baz: new RepoAST.Submodule("z", "2"), }, }), @@ -983,7 +1018,7 @@ describe("RepoAstUtil", function () { remotes: { foo: new RepoAST.Remote("my-url"), }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, }), m: { "1": "2"}, e: new RepoAST({ @@ -992,7 +1027,7 @@ describe("RepoAstUtil", function () { remotes: { foo: new RepoAST.Remote("my-url"), }, - workdir: { foo: "bar" }, + workdir: { foo: barFile }, }), }, "submodule with changes": { @@ -1098,8 +1133,8 @@ describe("RepoAstUtil", function () { const AST = RepoAST; const Commit = AST.Commit; const Remote = AST.Remote; - const c1 = new Commit({ changes: { foo: "bar" } }); - const c2 = new Commit({ changes: { baz: "bam" } }); + const c1 = new Commit({ changes: { foo: barFile } }); + const c2 = new Commit({ changes: { baz: bamFile } }); const child = new Commit({ parents: ["1"] }); const cases = { "sipmlest": { diff --git a/node/test/util/shorthand_parser_util.js b/node/test/util/shorthand_parser_util.js index 658bca090..d3c75990e 100644 --- a/node/test/util/shorthand_parser_util.js +++ b/node/test/util/shorthand_parser_util.js @@ -36,6 +36,8 @@ const RepoAST = require("../../lib/util/repo_ast"); const RepoASTUtil = require("../../lib/util/repo_ast_util"); const ShorthandParserUtil = require("../../lib/util/shorthand_parser_util"); +const File = RepoAST.File; + describe("ShorthandParserUtil", function () { const SequencerState = RepoAST.SequencerState; const CommitAndRef = SequencerState.CommitAndRef; @@ -177,7 +179,7 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ parents: ["2"], - changes: { "1": "1"}, + changes: { "1": new File("1", false)}, message: "message\n", }), } @@ -197,7 +199,7 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ parents: [], - changes: { y: "2" }, + changes: { y: new File("2", false) }, message: "message\n", }), }, @@ -207,7 +209,7 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ parents: ["2"], - changes: { "1": "1"}, + changes: { "1": new File("1", false) }, message: "hello world", }), } @@ -216,7 +218,7 @@ describe("ShorthandParserUtil", function () { commits: { "3": new Commit({ parents: ["1","2"], - changes: { "3": "3"}, + changes: { "3": new File("3", false) }, message: "message\n", }), }, @@ -225,7 +227,10 @@ describe("ShorthandParserUtil", function () { commits: { "3": new Commit({ parents: ["1","2"], - changes: { x: "y", q: "r" }, + changes: { + x: new File("y", false), + q: new File("r", false), + }, message: "hello", }), }, @@ -234,7 +239,7 @@ describe("ShorthandParserUtil", function () { commits: { "xxx2": new Commit({ parents: ["yy"], - changes: { "xxx2": "xxx2"}, + changes: { "xxx2": new File("xxx2", false) }, message: "message\n", }), } @@ -243,7 +248,7 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ parents: ["2"], - changes: { "foo": "bar"}, + changes: { "foo": new File("bar", false) }, message: "message\n", }), } @@ -252,7 +257,7 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ parents: ["2"], - changes: { "foo": ""}, + changes: { "foo": new File("", false) }, message: "message\n", }), } @@ -261,7 +266,10 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ parents: ["2"], - changes: { "foo": "bar", "b": "z"}, + changes: { + "foo": new File("bar", false), + "b": new File("z", false), + }, message: "message\n", }), } @@ -270,7 +278,10 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ parents: ["2"], - changes: { "foo": "bar", "b": "z"}, + changes: { + "foo": new File("bar", false), + "b": new File("z", false), + }, message: "message\n", }), } @@ -286,7 +297,7 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ parents: ["2"], - changes: { "1": "1"}, + changes: { "1": new File("1", false) }, message: "message\n", })}, branches: { m: null }, @@ -299,7 +310,7 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ parents: ["2"], - changes: { "1": "1"}, + changes: { "1": new File("1", false) }, message: "message\n", })}, branches: { m: null }, @@ -321,12 +332,12 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ parents: ["2"], - changes: { "1": "1"}, + changes: { "1": new File("1", false) }, message: "message\n", }), "3": new Commit({ parents: ["4"], - changes: { "3": "3"}, + changes: { "3": new File("3", false) }, message: "message\n", }), }, @@ -425,21 +436,29 @@ describe("ShorthandParserUtil", function () { i: "S:I x=y", e: m({ type: "S", - index: { x: "y" }, + index: { x: new File("y", false) }, }), }, "index deletion and changes": { i: "S:I x=y,q,z=r", e: m({ type: "S", - index: { x: "y", q: null, z: "r" }, + index: { + x: new File("y", false), + q: null, + z: new File("r", false), + }, }), }, "index deletion and removal": { i: "S:I x=y,q=~,z=r", e: m({ type: "S", - index: { x: "y", q: undefined, z: "r" }, + index: { + x: new File("y", false), + q: undefined, + z: new File("r", false), + }, }), }, "index submodule change": { @@ -454,8 +473,10 @@ describe("ShorthandParserUtil", function () { e: m({ type: "S", index: { - a: new Conflict("x", "y", new Submodule("/x", "2")), - b: "q", + a: new Conflict(new File("x", false), + new File("y", false), + new Submodule("/x", "2")), + b: new File("q", false), } }), }, @@ -464,8 +485,10 @@ describe("ShorthandParserUtil", function () { e: m({ type: "S", index: { - a: new Conflict("", null, ""), - b: "q", + a: new Conflict(new File("", false), + null, + new File("", false)), + b: new File("q", false), } }), }, @@ -473,21 +496,36 @@ describe("ShorthandParserUtil", function () { i: "S:W x=y", e: m({ type: "S", - workdir: { x: "y" }, + workdir: { x: new File("y", false) }, + }), + }, + "workdir change, executable bit set": { + i: "S:W x=+y", + e: m({ + type: "S", + workdir: { x: new File("y", true) }, }), }, "workdir deletion and changes": { i: "S:W x=y,q,z=r", e: m({ type: "S", - workdir: { x: "y", q: null, z: "r" }, + workdir: { + x: new File("y", false), + q: null, + z: new File("r", false), + }, }), }, "workdir removal and changes": { i: "S:W x=y,q=~,z=r", e: m({ type: "S", - workdir: { x: "y", q: undefined, z: "r" }, + workdir: { + x: new File("y", false), + q: undefined, + z: new File("r", false), + }, }), }, "workdir submodule change": { @@ -526,7 +564,7 @@ describe("ShorthandParserUtil", function () { branches: { master: new RepoAST.Branch("foo", null), }, - workdir: { x: "z" }, + workdir: { x: new File("z", false) }, }), }, }), @@ -568,7 +606,7 @@ describe("ShorthandParserUtil", function () { commits: { "2": new Commit({ parents: ["1"], - changes: { "2": "2"}, + changes: { "2": new File("2", false) }, message: "message\n", }), }, @@ -586,7 +624,7 @@ describe("ShorthandParserUtil", function () { commits: { "2": new Commit({ parents: ["1"], - changes: { "2": "2"}, + changes: { "2": new File("2", false) }, message: "message\n", }), }, @@ -735,6 +773,7 @@ describe("ShorthandParserUtil", function () { assert.deepEqual(r.branches, e.branches); assert.deepEqual(r.remotes, e.remotes); assert.deepEqual(r.index, e.index); + assert.deepEqual(r.workdir, e.workdir); assert.equal(r.head, e.head); assert.equal(r.currentBranchName, e.currentBranchName); assert.deepEqual(r.openSubmodules, e.openSubmodules); @@ -775,7 +814,7 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ changes: { - "1": "1", + "1": new File("1", false), }, message: "message\n", }), @@ -801,7 +840,7 @@ describe("ShorthandParserUtil", function () { commits: { "1": B.commits["1"], "2": new Commit({ - changes: { "2": "2" }, + changes: { "2": new File("2", false) }, message: "message\n", parents: ["1"], }), @@ -818,7 +857,7 @@ describe("ShorthandParserUtil", function () { commits: { xyz: new Commit({ changes: { - xyz: "xyz", + xyz: new File("xyz", false), }, message: "changed xyz", }), @@ -847,7 +886,7 @@ describe("ShorthandParserUtil", function () { let commits = S.commits; commits[2] = new Commit({ parents: ["1"], - changes: { "2": "2"}, + changes: { "2": new File("2", false) }, message: "message\n", }); return commits; @@ -867,7 +906,7 @@ describe("ShorthandParserUtil", function () { let commits = S.commits; commits[2] = new Commit({ parents: ["1"], - changes: { "2": "2"}, + changes: { "2": new File("2", false) }, message: "message\n", }); return commits; @@ -896,7 +935,7 @@ describe("ShorthandParserUtil", function () { i: "S:I a=b", e: S.copy({ index: { - a: "b", + a: new File("b", false), } }), }, @@ -965,7 +1004,7 @@ describe("ShorthandParserUtil", function () { commits: { "1": B.commits["1"], "2": new Commit({ - changes: { "2": "2" }, + changes: { "2": new File("2", false) }, message: "message\n", parents: ["1"], }), @@ -1130,13 +1169,15 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ changes: { - "README.md": "hello world" + "README.md": new File( + "hello world", + false), }, message: "the first commit", }), "2": new Commit({ parents: ["1"], - changes: { "2": "2" }, + changes: { "2": new File("2", false) }, message: "message\n", }), }, @@ -1160,8 +1201,8 @@ describe("ShorthandParserUtil", function () { openSubmodules: { foo: RepoASTUtil.cloneRepo(S, "a").copy({ branches: {}, - index: { x: "y"}, - workdir: { u: "2" }, + index: { x: new File("y", false) }, + workdir: { u: new File("2", false) }, currentBranchName: null, remotes: { origin: new Remote("a") }, }) @@ -1182,7 +1223,7 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ changes: { - "README.md": "hello world", + "README.md": new File("hello world", false) }, message: "the first commit", }), @@ -1210,7 +1251,7 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ changes: { - "README.md": "hello world", + "README.md": new File("hello world", false) }, message: "the first commit", }), @@ -1243,13 +1284,15 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ changes: { - "README.md": "hello world" + "README.md": new File( + "hello world", + false), }, message: "the first commit", }), "2": new Commit({ parents: ["1"], - changes: { "2": "2" }, + changes: { "2": new File("2", false) }, message: "message\n", }), }, @@ -1291,7 +1334,7 @@ describe("ShorthandParserUtil", function () { commits: { "1": new RepoAST.Commit({ changes: { - "README.md": "hello world" + "README.md": new File("hello world", false) }, message: "the first commit", }), @@ -1318,13 +1361,13 @@ describe("ShorthandParserUtil", function () { commits: { "1": new Commit({ changes: { - "README.md": "hello world" + "README.md": new File("hello world", false) }, message: "the first commit", }), "8": new Commit({ parents: ["1"], - changes: { "8": "8" }, + changes: { "8": new File("8", false) }, message: "message\n", }), }, @@ -1355,18 +1398,18 @@ x=S:Efoo,8,9`, commits: { "1": new Commit({ changes: { - "README.md": "hello world" + "README.md": new File("hello world", false) }, message: "the first commit", }), "8": new Commit({ parents: ["1"], - changes: { "8": "8" }, + changes: { "8": new File("8", false) }, message: "message\n", }), "9": new Commit({ parents: ["1"], - changes: { "9": "9" }, + changes: { "9": new File("9", false) }, message: "message\n", }), }, @@ -1380,18 +1423,18 @@ x=S:Efoo,8,9`, commits: { "1": new Commit({ changes: { - "README.md": "hello world" + "README.md": new File("hello world", false) }, message: "the first commit", }), "8": new Commit({ parents: ["1"], - changes: { "8": "8" }, + changes: { "8": new File("8", false) }, message: "message\n", }), "9": new Commit({ parents: ["1"], - changes: { "9": "9" }, + changes: { "9": new File("9", false) }, message: "message\n", }), }, diff --git a/node/test/util/submodule_rebase_util.js b/node/test/util/submodule_rebase_util.js index 55063d631..e187bb395 100644 --- a/node/test/util/submodule_rebase_util.js +++ b/node/test/util/submodule_rebase_util.js @@ -35,6 +35,7 @@ const co = require("co"); const colors = require("colors"); const NodeGit = require("nodegit"); +const RepoAST = require("../../lib/util/repo_ast"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); const StatusUtil = require("../../lib/util/status_util"); const SubmoduleRebaseUtil = require("../../lib/util/submodule_rebase_util"); @@ -88,7 +89,7 @@ describe("processRebase", function () { `; expected.x = expected.x.copy({ workdir: { - "2": content, + "2": new RepoAST.File(content, false), }, }); return expected; diff --git a/node/test/util/write_repo_ast_util.js b/node/test/util/write_repo_ast_util.js index f74885692..7a97cf80c 100644 --- a/node/test/util/write_repo_ast_util.js +++ b/node/test/util/write_repo_ast_util.js @@ -181,6 +181,10 @@ B:C2-1 x/y/qq=Sa:1,aa/bb/cc=Sb:1;C3-2 x/y/zz=Sq:1,aa/bb/dd=Sy:1; C7-6 aa/n/q=Spp:1,x/n/q=Sqq:1;Bm=7`, shas: [["1", "2"], ["3"], ["4"], ["5"], ["6"], ["7"]], }, + "file with exec bit": { + input: "N:C1 s=+2;Bmaster=1", + shas: [["1"]], + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; @@ -352,7 +356,11 @@ S:C2-1 x=y;C3-1 x=z;Bmaster=2;Bfoo=3;Erefs/heads/master,2,3;I x=q;H=3", expected: new RepoAST(), }, "conflict": "S:I *README.md=aa*bb*cc;W README.md=yyy", + "conflict with exec": "S:I *README.md=aa*+bb*cc;W README.md=yyy", "submodule conflict": "S:I *README.md=aa*S:1*cc;W README.md=yyy", + "index exec change": "S:I README.md=+hello world", + "workdir exec change": "S:W README.md=+hello world", + "workdir new exec file": "S:W foo=+hello world", }; Object.keys(cases).forEach(caseName => { From a4a390c1e526767fee76135a1a330e23e13041fc Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 6 May 2018 13:34:15 -0400 Subject: [PATCH 136/402] Fix path-based commit to record executable bit Addresses: https://github.com/twosigma/git-meta/issues/581 --- node/lib/util/commit.js | 29 +++++++++++++++++++++++++++-- node/test/util/commit.js | 12 ++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index ca3a2b0a2..36c9ba8e4 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -40,6 +40,7 @@ const assert = require("chai").assert; const co = require("co"); const colors = require("colors"); +const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); @@ -535,6 +536,29 @@ exports.commit = co.wrap(function *(metaRepo, return result; }); +/** + * Return true if the specified `filename` in the specified `repo` is + * executable and false otherwise. + * + * TODO: move this out somewhere lower-level and make an independent test. + * + * @param {Nodegit.Repository} repo + * @param {String} filename + * @return {Bool} + */ +const isExecutable = co.wrap(function *(repo, filename) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(filename); + const fullPath = path.join(repo.workdir(), filename); + try { + yield fs.access(fullPath, fs.constants.X_OK); + return true; + } catch (e) { + // cannot execute + return false; + } +}); + /** * Write a commit for the specified `repo` having the specified * `status` using the specified commit `message` and return the ID of the new @@ -574,8 +598,9 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message) { } else { const blobId = yield TreeUtil.hashFile(repo, filename); - changes[filename] = new Change(blobId, FILEMODE.BLOB); - + const executable = yield isExecutable(repo, filename); + const mode = executable ? FILEMODE.EXECUTABLE : FILEMODE.BLOB; + changes[filename] = new Change(blobId, mode); yield index.addByPath(filename); } } diff --git a/node/test/util/commit.js b/node/test/util/commit.js index c5ed9eb95..ce67ceb50 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -1827,6 +1827,13 @@ my message }, expected: "x=S:Cx-1 README.md=haha;Bmaster=x", }, + "staged change with exec bit": { + state: "x=S:I README.md=+haha", + fileChanges: { + "README.md": FILESTATUS.MODIFIED, + }, + expected: "x=S:Cx-1 README.md=+haha;Bmaster=x", + }, "simple staged change, added eol": { state: "x=S:I README.md=haha", message: "hah", @@ -1840,6 +1847,11 @@ my message fileChanges: { "README.md": FILESTATUS.MODIFIED, }, expected: "x=S:Cx-1 README.md=haha;Bmaster=x", }, + "workdir change with exec bit": { + state: "x=S:W README.md=+haha", + fileChanges: { "README.md": FILESTATUS.MODIFIED, }, + expected: "x=S:Cx-1 README.md=+haha;Bmaster=x", + }, "simple change with message": { state: "x=S:I README.md=haha", fileChanges: { "README.md": FILESTATUS.MODIFIED, }, From ce083151d7eca027d372d51c6594dd979516b6f0 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 8 May 2018 13:30:40 -0400 Subject: [PATCH 137/402] Write out index at end of `rm`. Addresses: https://github.com/twosigma/git-meta/issues/584 --- node/lib/util/rm.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node/lib/util/rm.js b/node/lib/util/rm.js index 835352f5e..b54780076 100644 --- a/node/lib/util/rm.js +++ b/node/lib/util/rm.js @@ -471,4 +471,5 @@ exports.rmPaths = co.wrap(function *(repo, paths, options) { } yield SubmoduleConfigUtil.writeUrls(repo, index, modules, options.cached); + yield index.write(); }); From ed9d5786c4b8bfc6e2c1e87d9dcb1ed701de51a4 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 9 May 2018 11:10:43 -0400 Subject: [PATCH 138/402] use increment instead of epoch for shadow times Commits with old times confuse (as in makes very expensive) Git's fetch logic, so instead of using an epoch time to make sure subsequent shadow commits of unchanged code have the same sha, we'll use the time of HEAD + 1. --- node/lib/cmd/commit-shadow.js | 14 ++++++++++++-- node/lib/util/stash_util.js | 17 +++++++++++------ node/test/util/stash_util.js | 23 ++++++++++++----------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/node/lib/cmd/commit-shadow.js b/node/lib/cmd/commit-shadow.js index 7d4a6152d..5fb360251 100644 --- a/node/lib/cmd/commit-shadow.js +++ b/node/lib/cmd/commit-shadow.js @@ -80,7 +80,15 @@ exports.configureParser = function (parser) { action: "storeConst", constant: true, defaultValue: false, - help: "include timestamp in the shadow commit", + help: "deprecated, but same as '--increment-timestamp'", + }); + + parser.addArgument(["-i", "--increment-timestamp"], { + required: false, + action: "storeConst", + constant: true, + defaultValue: false, + help: "use timestamp of HEAD + 1 instead of current time", }); }; @@ -96,9 +104,11 @@ exports.executeableSubcommand = co.wrap(function *(args) { const StashUtil = require("../util/stash_util"); const repo = yield GitUtil.getCurrentRepo(); + const incrementTimestamp = + args.increment_timestamp || args.epoch_timestamp; const result = yield StashUtil.makeShadowCommit(repo, args.message, - args.epoch_timestamp, + incrementTimestamp, false, args.include_untracked); if (null === result) { diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 2ebcf44a1..a1f4eb376 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -473,25 +473,27 @@ exports.list = co.wrap(function *(repo) { * Make a shadow commit for the specified `repo` having the specified `status`; * use the specified commit `message`. Ignored untracked files unless the * specified `includeUntracked` is true. Return the sha of the created commit. - * Note that this method does not recurse into submodules. + * Note that this method does not recurse into submodules. If the specified + * `incrementTimestamp` is true, use the timestamp of HEAD + 1; otherwise, use + * the current time. * * @param {NodeGit.Repository} repo * @param {RepoStatus} status * @param {String} message - * @param {Bool} useEpochTimestamp + * @param {Bool} incrementTimestamp * @param {Bool} includeUntracked * @return {String} */ const makeShadowCommitForRepo = co.wrap(function *(repo, status, message, - useEpochTimestamp, + incrementTimestamp, includeUntracked) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(status, RepoStatus); assert.isString(message); assert.isBoolean(includeUntracked); - assert.isBoolean(useEpochTimestamp); + assert.isBoolean(incrementTimestamp); const changes = yield TreeUtil.listWorkdirChanges(repo, status, @@ -508,8 +510,11 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, const newTree = yield TreeUtil.writeTree(repo, indexTree, changes); let sig = repo.defaultSignature(); - if (useEpochTimestamp) { - sig = NodeGit.Signature.create(sig.name(), sig.email(), 0, 0); + if (incrementTimestamp && null !== head) { + sig = NodeGit.Signature.create(sig.name(), + sig.email(), + head.time() + 1, + head.timeOffset()); } const id = yield NodeGit.Commit.create(repo, null, diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index da0b473a1..6ec0d2e9d 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -720,7 +720,7 @@ meta-stash@{1}: log of 1 expected: "x=E:Cfoo\n#m-1 foo/bar=2;Bm=m", message: "foo", includeMeta: true, - useEpochTimestamp: true, + incrementTimestamp: true, }, "deleted file": { state: "x=S:W README.md", @@ -786,22 +786,23 @@ x=E:Cm-1 s=Sa:s;Bm=m;Os Cs foo=bar!Bs=s!W foo=bar` const meta = c.includeMeta; const includeUntracked = undefined === c.includeUntracked || c.includeUntracked; - const useEpochTimestamp = - (undefined === c.useEpochTimestamp) ? - false : c.useEpochTimestamp; + const incrementTimestamp = + (undefined === c.incrementTimestamp) ? + false : c.incrementTimestamp; const result = yield StashUtil.makeShadowCommit( - repo, - message, - useEpochTimestamp, - meta, - includeUntracked); + repo, + message, + incrementTimestamp, + meta, + includeUntracked); const commitMap = {}; if (null !== result) { const metaSha = result.metaCommit; const commit = yield repo.getCommit(metaSha); - if (useEpochTimestamp) { - assert.equal(commit.time(), 0); + const head = yield repo.getHeadCommit(); + if (incrementTimestamp) { + assert.equal(commit.time(), head.time() + 1); } commitMap[metaSha] = "m"; yield NodeGit.Branch.create(repo, "m", commit, 1); From 994c312296281d9c7dd65a4ea27c1773bdbc17b4 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 9 May 2018 16:45:33 -0400 Subject: [PATCH 139/402] Default to tracking branch for merge and rebase Addresses: https://github.com/twosigma/git-meta/issues/587 --- node/lib/cmd/merge.js | 12 ++++++++---- node/lib/cmd/rebase.js | 10 +++++++--- node/lib/util/git_util.js | 22 ++++++++++++++++++++++ node/test/util/git_util.js | 30 ++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index 3ef617978..1b5bf36cc 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -142,17 +142,21 @@ exports.executeableSubcommand = co.wrap(function *(args) { yield MergeUtil.abort(repo); return; // RETURN } - if (null === args.commit) { + let commitName = args.commit; + if (null === commitName) { + commitName = yield GitUtil.getCurrentTrackingBranchName(repo); + } + if (null === commitName) { throw new UserError("Commit required."); } - const commitish = yield GitUtil.resolveCommitish(repo, args.commit); + const commitish = yield GitUtil.resolveCommitish(repo, commitName); if (null === commitish) { throw new UserError(`\ -Could not resolve ${colors.red(args.commit)} to a commit.`); +Could not resolve ${colors.red(commitName)} to a commit.`); } const editMessage = function () { const message = `\ -Merge of '${args.commit}' +Merge of '${commitName}' # please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. diff --git a/node/lib/cmd/rebase.js b/node/lib/cmd/rebase.js index d8ee74979..dced3fb76 100644 --- a/node/lib/cmd/rebase.js +++ b/node/lib/cmd/rebase.js @@ -102,13 +102,17 @@ exports.executeableSubcommand = co.wrap(function *(args) { } } else { - if (null === args.commit) { + let commitName = args.commit; + if (null === commitName) { + commitName = yield GitUtil.getCurrentTrackingBranchName(repo); + } + if (null === commitName) { throw new UserError(`No onto committish specified.`); } - const committish = yield GitUtil.resolveCommitish(repo, args.commit); + const committish = yield GitUtil.resolveCommitish(repo, commitName); if (null === committish) { throw new UserError( - `Could not resolve ${colors.red(args.commit)} to a commit.`); + `Could not resolve ${colors.red(commitName)} to a commit.`); } const commit = yield repo.getCommit(committish.id()); const result = yield RebaseUtil.rebase(repo, commit); diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 2e37410d9..2f49a0902 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -168,6 +168,28 @@ exports.getTrackingInfo = co.wrap(function *(repo, branch) { }; }); +/** + * If the current branch branch in the specified `repo` tracks another branch, + * return its name (qualified by $remote-name if it's tracking a remote + * branch), or return null if it tracks no branch. + * + * @param {NodeGit.Repository} repo + * @return {String|null} + */ +exports.getCurrentTrackingBranchName = co.wrap(function *(repo) { + assert.instanceOf(repo, NodeGit.Repository); + + const head = yield repo.head(); + const tracking = yield exports.getTrackingInfo(repo, head); + if (null === tracking) { + return null; // RETURN + } + if (null === tracking.remoteName) { + return tracking.branchName; // RETURN + } + return `${tracking.remoteName}/${tracking.branchName}`; +}); + /** * Return the remote associated with the upstream reference of the specified * `branch` in the specified `repo`. diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index df77a6437..b0f5d1752 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -138,6 +138,36 @@ describe("GitUtil", function () { })); }); }); + describe("getCurrentTrackingBranchName", function () { + const cases = { + "no tracking": { + state: "S", + expected: null, + }, + "local tracking": { + state: "S:Bfoo=1;Bblah=1 foo;*=blah", + expected: "foo", + }, + "no branch": { + state: "S:H=1", + expected: null, + }, + "with remote": { + state: "S:Rhoo=/a gob=1;Bmaster=1 hoo/gob", + expected: "hoo/gob", + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(c.state); + const repo = written.repo; + const result = + yield GitUtil.getCurrentTrackingBranchName(repo); + assert.equal(result, c.expected); + })); + }); + }); describe("getRemoteForBranch", function () { it("no upstream", co.wrap(function *() { const written = yield RepoASTTestUtil.createRepo("S"); From 1b5b0aa6d19ffa19e24b3eac1197a3adebbad5a3 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 9 May 2018 16:55:12 -0400 Subject: [PATCH 140/402] Detect when up-to-date in `merge` Right now it will make a new merge-commit even if the target commit already exists in history. --- node/lib/util/merge_util.js | 9 +++++++++ node/test/util/merge_util.js | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 4e7fc2fdc..cbc64b7a3 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -349,6 +349,15 @@ running merge.`); return result; } + const upToDate = yield NodeGit.Graph.descendantOf(repo, + head.id().tostrS(), + commitSha); + + if (upToDate) { + console.log("Up-to-date."); + return result; + } + const canFF = yield NodeGit.Graph.descendantOf(repo, commitSha, head.id().tostrS()); diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index 5cb34c2f8..1455db274 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -221,6 +221,10 @@ x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, initial: "x=S", fromCommit: "1", }, + "up-to-date": { + initial: "a=B|x=U:C3-2 t=Sa:1;Bmaster=3;Bfoo=2", + fromCommit: "2", + }, "trivial -- nothing to do, has untracked change": { initial: "a=B|x=U:Os W foo=8", fromCommit: "2", From 804eb3907a962ca9bab9fb32af78e5e96ccb3547 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 9 May 2018 17:26:55 -0400 Subject: [PATCH 141/402] Stop confusing Git about the status of .gitmodules --- node/lib/util/submodule_config_util.js | 39 ++++++++++++++++++++------ 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index d503c3bb6..ae645b311 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -645,16 +645,39 @@ exports.writeUrls = co.wrap(function *(repo, index, urls, cached) { } } else { - const oid = yield GitUtil.hashObject(repo, newConf); - const sha = oid.toString(); if (!cached) { yield fs.writeFile(modulesPath, newConf); + yield index.addByPath(exports.modulesFileName); + } else { + // If we use this method of staging the change along with the + // `fs.writeFile` above in the `!cached` case, it will randomly + // confuse Git into thinking the `.gitmodules` file is modified + // even though a `git diff` shows no changes. I suspect we're + // writing some garbage flags somwewhere. You can replicate (after + // reverting the change that introduces this comment: + //```bash + //$ while :; + //> do + //> write-repos -o 'a=B|x=U:C3-2 t=Sa:1;Bmaster=3;Bfoo=2' + //> git -C x meta cherry-pick foo + //> git -C x status + //> sleep 0.01 + //> done + // + // and wait, about 1/4 times the `status` command will show a dirty + // `.gitmodules` file. + // + // TODO: track down why this confuses libgit2, *or* get rid of the + // caching logic; I don't think it buys us anything. + + const oid = yield GitUtil.hashObject(repo, newConf); + const sha = oid.toString(); + const entry = new NodeGit.IndexEntry(); + entry.path = exports.modulesFileName; + entry.mode = NodeGit.TreeEntry.FILEMODE.BLOB; + entry.id = NodeGit.Oid.fromString(sha); + entry.flags = entry.flagsExtended = 0; + yield index.add(entry); } - const entry = new NodeGit.IndexEntry(); - entry.path = exports.modulesFileName; - entry.mode = NodeGit.TreeEntry.FILEMODE.BLOB; - entry.id = NodeGit.Oid.fromString(sha); - entry.flags = entry.flagsExtended = 0; - yield index.add(entry); } }); From 4af7ae418d769058477c51d4a25615dbac41b6d6 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 11 May 2018 12:43:48 -0400 Subject: [PATCH 142/402] remove bogus param docs --- node/lib/util/submodule_config_util.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index ae645b311..42d6a8fd8 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -443,11 +443,6 @@ exports.getConfigLines = function (name, url) { * `name` in the config file. * * @async - * @param {NodeGit.Repository} repo - * @param {String} name - * @param {String} url - * - * @async * @param {String} repoPath * @param {String} name * @param {String} url From 10906f900ae8c1ccfa4690f958587a3a1749361f Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 1 May 2018 23:29:26 -0400 Subject: [PATCH 143/402] initial stab at better push --- node/lib/util/git_util.js | 2 +- node/lib/util/push.js | 136 ++++++++++++++++++++++++--- node/lib/util/submodule_util.js | 76 +++++++++++++++ node/package.json | 3 +- node/test/util/push.js | 153 ++++++++++++++++++++++++++++++- node/test/util/submodule_util.js | 36 ++++++++ 6 files changed, 388 insertions(+), 18 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 2f49a0902..2ae160d28 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -387,7 +387,7 @@ exports.push = co.wrap(function *(repo, remote, source, target, force, quiet) { } const execString = `\ -git -C '${repo.workdir()}' push ${forceStr} ${remote} ${source}:${target}`; +git -C '${repo.path()}' push ${forceStr} ${remote} ${source}:${target}`; try { const result = yield ChildProcess.exec(execString); if (result.error || !quiet) { diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 7fa4fbb06..1301393e2 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -46,6 +46,120 @@ const SubmoduleConfigUtil = require("./submodule_config_util"); const SyntheticBranchUtil = require("./synthetic_branch_util"); const UserError = require("./user_error"); + +/** + * For a given proposed push, return a map from submodule to sha, + * excluding any submodules that the server likely already has. + * + * Start with the set of submodules that are either open, or that + * exist in .git/modules. Populate a pushMap from submodule name to + * commit (SHA) registered in the meta-commit being pushed. + * + * If the push target exists in remotes/$remote/$branch, remove from + * pushMap, any entry where the commit for a submodule already exists + * in the target. + * + * Note that for "exists" we mean the target points to exactly the + * commit needed or a descendant of that commit. + * + * If the user is pushing a branch, B, further reduce the size of + * pushMap by removing commits referenced in the branches that B pulls + * from and/or pushes to by default, if they exist and are different + * from the target branch. + * + * @async + * @param {NodeGit.Repository} repo + * @param {String} remoteName + * @param {String} source + * @param {String} target + * @param {NodeGit.Commit} commit + */ +exports.getPushMap = co.wrap(function*(repo, remoteName, source, target, + commit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(remoteName); + assert.isString(source); + assert.isString(target); + assert.instanceOf(commit, NodeGit.Commit); + + const submoduleSet = new Set(yield SubmoduleUtil.listOpenSubmodules(repo)); + for (const s of yield SubmoduleUtil.listAbsorbedSubmodules(repo)) { + submoduleSet.add(s); + } + const submodules = [...submoduleSet]; + const pushMap = yield SubmoduleUtil.getSubmoduleShasForCommit(repo, + submodules, + commit); + const bareRepos = {}; + for (const sub of Object.keys(pushMap)) { + const subBareRepo = yield SubmoduleUtil.getBareRepo(repo, sub); + bareRepos[sub] = subBareRepo; + } + + const trackingBranches = new Set(); + trackingBranches.add(`refs/remotes/${remoteName}/${target}`); + let tracking; + try { + const sourceBranch = yield repo.getReference(source); + tracking = yield GitUtil.getTrackingInfo(repo, sourceBranch); + } catch (e) { + // we have no local branch? maybe source is a sha. + tracking = null; + } + + if (tracking !== null) { + if (tracking.remoteName !== null) { + trackingBranches.add( + `refs/remotes/${tracking.remoteName}/${target}`); + } + if (tracking.pushRemoteName !== null) { + trackingBranches.add( + `refs/remotes/${tracking.pushRemoteName}/${target}`); + } + } + + for (const branch of trackingBranches) { + let reference = null; + try { + reference = yield NodeGit.Reference.lookup(repo, branch); + } catch (e) { + // if this ref doesn't exist, OK + continue; + } + const id = reference.target(); + const trackingCommit = yield NodeGit.Commit.lookup( + repo, id); + const getter = SubmoduleUtil.getSubmoduleShasForCommit; + const submoduleShasForCommit = yield getter(repo, Object.keys(pushMap), + trackingCommit); + for (const sub of Object.keys(pushMap)) { + const sha = submoduleShasForCommit[sub]; + const subRepo = bareRepos[sub]; + if (sha === pushMap[sub]) { + delete pushMap[sub]; + continue; + } + const commitToPush = yield subRepo.getCommit(pushMap[sub]); + let trackingCommit; + try { + trackingCommit = yield subRepo.getCommit(sha); + } catch (e) { + // We haven't fetched this commit in this submodule, + // so we can't do an ancestry check + continue; + } + const descendantCheck = NodeGit.Graph.descendantOf; + const isDescendant = yield descendantCheck(subRepo, + trackingCommit, + commitToPush); + if (isDescendant) { + delete pushMap[sub]; + } + } + } + return pushMap; +}); + /** * For each open submodule that exists in the commit indicated by the specified * `source`, push a synthetic-meta-ref for the `source` commit. @@ -83,27 +197,26 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { let remoteUrl = yield GitUtil.getUrlFromRemoteName(repo, remoteName); + const annotatedCommit = yield GitUtil.resolveCommitish(repo, source); + const sha = annotatedCommit.id(); + const commit = yield repo.getCommit(sha); + // First, push the submodules. + const pushMap = yield exports.getPushMap(repo, remoteName, source, target, + commit); let errorMessage = ""; - const shas = yield SubmoduleUtil.getSubmoduleShasForCommitish(repo, - source); - const annotatedCommit = yield GitUtil.resolveCommitish(repo, source); - const commit = yield repo.getCommit(annotatedCommit.id()); + const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, commit); const pushSub = co.wrap(function *(subName) { - // If no commit for a submodule on this branch, skip it. - if (!(subName in shas)) { - return; // RETURN - } // Push to a synthetic branch; first, calculate name. - const sha = shas[subName]; + const sha = pushMap[subName]; const syntheticName = SyntheticBranchUtil.getSyntheticBranchForCommit(sha); - const subRepo = yield SubmoduleUtil.getRepo(repo, subName); + const subRepo = yield SubmoduleUtil.getBareRepo(repo, subName); // Resolve the submodule's URL against the URL of the meta-repo, // ignoring the remote that is configured in the open submodule. @@ -126,8 +239,7 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { `Failed to push submodule ${colors.yellow(subName)}: ${pushResult}`; } }); - const subRepos = yield SubmoduleUtil.listOpenSubmodules(repo); - yield DoWorkQueue.doInParallel(subRepos, pushSub); + yield DoWorkQueue.doInParallel(Object.keys(pushMap), pushSub); // Throw an error if there were any problems pushing submodules; don't push // the meta-repo. diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index a507ff170..3abf8fb4d 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -40,6 +40,7 @@ const colors = require("colors"); const NodeGit = require("nodegit"); const fs = require("fs-promise"); const path = require("path"); +const walk = require("walk"); const DoWorkQueue = require("../util/do_work_queue"); const GitUtil = require("./git_util"); @@ -201,6 +202,62 @@ exports.getCurrentSubmoduleShas = function (index, submoduleNames) { return result; }; +const gitReservedNames = new Set(["HEAD", "FETCH_HEAD", "ORIG_HEAD", + "COMMIT_EDITMSG", "index", "config", + "logs", "rr-cache", "hooks", "info", + "objects", "refs"]); +/** + * Return a list of submodules from .git/modules -- that is, + * approximately, those which we have ever opened. + */ +exports.listAbsorbedSubmodules = co.wrap(function*(repo) { + const options = { + followLinks: false + }; + + const modules_dir = path.join(repo.path(), "modules"); + const walker = walk.walk(modules_dir, options); + const out = []; + + walker.on("names", function (root, nodeNamesArray) { + if (nodeNamesArray.indexOf("HEAD") !== -1) { + // We've hit an actual git module -- don't recurse + // further. It's possible that our module contains other + // modules (e.g. if foo/bar/baz gets moved to + // foo/bar/baz/fleem). If so, really weird things could + // happen -- e.g. .git/modules/foo/bar/baz/objects could + // secretly contain another entire git repo. There are + // cases here that regular git can't handle (for instance, + // if you move a submodule to a subdirectory of itself + // named "config"). But the vast majority of the time, + // nested repos won't have name conflicts with git + // reserved dir names, so we'll just eliminate those + // reserved name, and recurse the rest if any. + + const filtered = []; + for (const name of nodeNamesArray) { + if (!gitReservedNames.has(name)) { + filtered.push(name); + } + } + nodeNamesArray.splice(0, nodeNamesArray.length, filtered); + out.push(root.substring(modules_dir.length + 1)); + } + }); + + yield new Promise(function(resolve, reject) { + try { + walker.on("end", resolve); + } catch (e) { + reject(); + } + }); + + return out; + +}); + + /** * Return true if the submodule having the specified `submoduleName` in the * specified `repo` is visible and false otherwise. @@ -239,6 +296,25 @@ exports.getRepo = function (metaRepo, name) { return NodeGit.Repository.open(submodulePath); }; +/** + * Return the `Repository` for the absorbed bare repo for th submodule + * having the specified `name` in the specified `metaRepo`. That's + * the one in meta/.git/modules/... + * + * @async + * @param {NodeGit.Repository} metaRepo + * @param {String} name + * @return {NodeGit.Repository} + */ +exports.getBareRepo = function (metaRepo, name) { + assert.instanceOf(metaRepo, NodeGit.Repository); + assert.isString(name); + + // metaRepo.path() returns the path to the gitdir. + const submodulePath = path.join(metaRepo.path(), "modules", name); + return NodeGit.Repository.openBare(submodulePath); +}; + /** * Return an array containing a list of the currently open submodules of the * specified `repo`. diff --git a/node/package.json b/node/package.json index 504805b46..70aa78399 100644 --- a/node/package.json +++ b/node/package.json @@ -37,7 +37,8 @@ "group-by": "", "nodegit": "^0.21.1", "rimraf": "", - "split": "" + "split": "", + "walk": "" }, "devDependencies": { "deepcopy": "", diff --git a/node/test/util/push.js b/node/test/util/push.js index dfec266bb..42b752759 100644 --- a/node/test/util/push.js +++ b/node/test/util/push.js @@ -30,12 +30,16 @@ */ "use strict"; +const assert = require("chai").assert; const co = require("co"); -const Push = require("../../lib/util/push"); -const RepoAST = require("../../lib/util/repo_ast"); -const RepoASTUtil = require("../../lib/util/repo_ast_util"); -const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const GitUtil = require("../../lib/util/git_util"); +const Push = require("../../lib/util/push"); +const RepoAST = require("../../lib/util/repo_ast"); +const RepoASTUtil = require("../../lib/util/repo_ast_util"); +const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const SubmoduleUtil = require("../../lib/util/submodule_util"); +const SubmoduleConfigUtil = require("../../lib/util/submodule_config_util"); /** * Return a map from name to RepoAST that is the same as the one in the @@ -190,6 +194,147 @@ describe("refMapper", function () { }); }); +describe("getPushMap", function () { + const cases = { + "empty, by sha" : { + initial: "x=S:C2-1;Bmaster=2", + source: "2", + expectedPushMap: {} + }, + "simple" : { + initial: `sub=S:C8-1;Bmaster=8|x=S:C2-1 d=Ssub:8;Bmaster=2;Od`, + source: "2", + expectedPushMap: { + d : "8" + }, + }, + "another sub in parent commit" : { + initial: `sub1=S:C8-1;Bmaster=8| + sub2=S:C7-1;Bmaster=7| + x=S:C2-1 d1=Ssub1:8;C3-2 d2=Ssub2:7;Bmaster=3;Od1;Od2`, + source: "3", + expectedPushMap: { + d1 : "8", + d2 : "7" + }, + }, + "another sub in parent commit, but origin already has it" : { + initial: `sub1=S:C8-1;Bmaster=8| + sub2=S:C7-1;Bmaster=7| + x=S:C2-1 d1=Ssub1:8;C3-2 d2=Ssub2:7;Bmaster=3; + Rorigin=foo target=2; + Od1;Od2`, + source: "3", + expectedPushMap: { + d2 : "7", + }, + }, + "origin has a child but we didn't fetch it, so we don't know that" : { + initial: `sub=S:C7-1;C8-7;Bmaster=8| + x=S:C2-1 d=Ssub:8;C3-1 d=Ssub:7;Bmaster=3; + Rorigin=foo target=2; + Od`, + source: "3", + expectedPushMap: { + d : "7", + }, + }, + "origin has a child (which we know is a child)" : { + initial: `sub=S:C7-1;C8-7;Bmaster=8| + x=S:C2-1 d=Ssub:8;C3-1 d=Ssub:7;Bmaster=3; + Rorigin=foo target=2; + Od`, + extraFetch: { + "d" : { + sub: "sub", + commits: ["8"], + }, + }, + source: "3", + expectedPushMap: {}, + }, + "origin is equal" : { + initial: `sub=S:C7-1;Bmaster=7| + x=S:C2-1 d=Ssub:7;Bmaster=2; + Rorigin=foo target=2; + Od`, + extraFetch: { + "d" : { + sub: "sub", + commits: ["7"], + }, + }, + source: "2", + expectedPushMap: {}, + }, + }; + + const testGetPushMap = function(source, expectedPushMap, extraFetch) { + return co.wrap(function *(repos, commitMap) { + const repo = repos.x; + let sha; + if (parseInt(source) > 0) { + sha = commitMap.reverseCommitMap[source]; + } else { + sha = yield repo.getReference(source).target(); + } + + const commit = yield repo.getCommit(sha); + + // Do any necessary extra fetches in submodules + for (const sub of Object.keys(extraFetch)) { + const extra = extraFetch[sub]; + const subRepo = yield SubmoduleUtil.getBareRepo(repo, sub); + for (const toFetch of extra.commits) { + const mappedCommit = commitMap.reverseCommitMap[toFetch]; + yield GitUtil.fetchSha(subRepo, + commitMap.reverseUrlMap[extra.sub], + mappedCommit); + } + } + + // We want to test two modes: one with submodules open, + // and another with them closed. We need them to be initially + // open, because this will populate .git/modules, but + // we also want to test with them closed to ensure + // that we can handle that case. + for (const closeSubs of [false, true]) { + if (closeSubs) { + const subs = yield SubmoduleUtil.listOpenSubmodules(repo); + for (const sub of subs) { + yield SubmoduleConfigUtil.deinit(repo, sub); + } + } + + const pushMap = yield Push.getPushMap(repo, "origin", source, + "target", commit); + const mappedPushMap = {}; + for (const sub of Object.keys(pushMap)) { + mappedPushMap[sub] = commitMap.commitMap[pushMap[sub]]; + } + assert.deepEqual(expectedPushMap, mappedPushMap); + } + }); + }; + + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const parts = c.initial.split(";"); + const expectedParts = parts.filter( + part => !part.trim().startsWith("O")); + const expected = expectedParts.join(";"); + const manipulator = testGetPushMap(c.source, + c.expectedPushMap, + c.extraFetch || {}); + yield RepoASTTestUtil.testMultiRepoManipulator( + c.initial, + expected, + manipulator); + })); + }); +}); + describe("push", function () { function pusher(repoName, remoteName, source, target, force) { diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index 69c71f1c5..72b504a82 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -298,6 +298,42 @@ describe("SubmoduleUtil", function () { })); }); + describe("listAbsorbedSubmodules", function () { + const cases = { + "simple": { + input: "x=S", + expected: [], + }, + "one open": { + input: "a=S|x=S:I q=Sa:1;Oq", + expected: ["q"], + }, + "two open": { + input: "a=S|x=S:I q=Sa:1,s/x=Sa:1;Oq;Os/x", + expected: ["q", "s/x"], + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const written = + yield RepoASTTestUtil.createMultiRepos(c.input); + const x = written.repos.x; + for (const closeSubs of [false, true]) { + if (closeSubs) { + const subs = yield SubmoduleUtil.listOpenSubmodules(x); + for (const sub of subs) { + yield SubmoduleConfigUtil.deinit(x, sub); + } + } + const dut = SubmoduleUtil; + const result = yield dut.listAbsorbedSubmodules(x); + assert.deepEqual(result.sort(), c.expected.sort()); + } + })); + }); + }); + describe("listOpenSubmodules", function () { // We will always inspect the repo `x`. From 61315a632d6fa7684bc668717093142066af153e Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 16 May 2018 17:07:38 -0400 Subject: [PATCH 144/402] clarify error message for remotes --- node/lib/util/repo_ast_util.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node/lib/util/repo_ast_util.js b/node/lib/util/repo_ast_util.js index 0b894e7ac..a330aca2f 100644 --- a/node/lib/util/repo_ast_util.js +++ b/node/lib/util/repo_ast_util.js @@ -213,15 +213,15 @@ expected url to be ${colorExp(expected.url)} but got ${colorAct(actual.url)}` ); } function missingActual(branch) { - result.push(`missing branch ${colorBad(branch)}`); + result.push(`missing remote branch ${colorBad(branch)}`); } function missingExpected(branch) { - result.push(`unexpected branch ${colorBad(branch)}`); + result.push(`unexpected remote branch ${colorBad(branch)}`); } function compare(branch) { if (actual.branches[branch] !== expected.branches[branch]) { result.push(`\ -for branch ${colorBad(branch)} expected \ +for remote branch ${colorBad(branch)} expected \ ${colorExp(expected.branches[branch])} but got \ ${colorAct(actual.branches[branch])}` ); From bde5a99c90ea440558ca8eec115428fdeb49fda6 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 16 May 2018 17:13:24 -0400 Subject: [PATCH 145/402] Make shadow-commit work with commits in submodules which are not staged in the meta repo --- node/lib/util/stash_util.js | 15 +++++++++++---- node/test/util/stash_util.js | 26 +++++++++++++++++++------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index a1f4eb376..cb83ded41 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -500,15 +500,20 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, includeUntracked); const head = yield repo.getHeadCommit(); const parents = []; - if (null !== head) { - parents.push(head); - } const index = yield repo.index(); const treeOid = yield index.writeTree(); const indexTree = yield repo.getTree(treeOid); const newTree = yield TreeUtil.writeTree(repo, indexTree, changes); + if (null !== head) { + parents.push(head); + const headTree = yield head.getTree(); + if (newTree.id().equal(headTree.id())) { + return head.sha(); + } + } + let sig = repo.defaultSignature(); if (incrementTimestamp && null !== head) { sig = NodeGit.Signature.create(sig.name(), @@ -581,7 +586,9 @@ exports.makeShadowCommit = co.wrap(function *(repo, // If the submodule is closed or its workdir is clean, we don't need to // do anything for it. - if (null === wd || wd.status.isClean(includeUntracked)) { + if (null === wd || ((subStatus.commit === null || + wd.status.headCommit === subStatus.commit.sha) && + wd.status.isClean(includeUntracked))) { return; // RETURN } const subRepo = yield SubmoduleUtil.getRepo(repo, name); diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index 6ec0d2e9d..3565528ed 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -742,11 +742,21 @@ meta-stash@{1}: log of 1 expected: "x=E:Cm-2 README.md=2;Bm=m", includeMeta: true, }, - "new commit in unopened submodule": { + "new, staged commit in opened submodule": { + state: "a=B:Ca-1;Ba=a|x=U:I s=Sa:a;Os", + expected: "x=E:Cm-2 s=Sa:a;Bm=m", + includeMeta: true, + }, + "new, staged commit in unopened submodule": { state: "a=B:Ca-1;Ba=a|x=U:I s=Sa:a", expected: "x=E:Cm-2 s=Sa:a;Bm=m", includeMeta: true, }, + "new, unstaged commit in opened submodule": { + state: "a=B:Ca-1;Ba=a|x=U:C3-2;Bmaster=3;Os H=a", + expected: "x=E:Cm-3 s=Sa:a;Bm=m", + includeMeta: true, + }, "new file in open submodule, untracked not included": { state: "a=B|x=U:Os W x/y/z=3", includeMeta: true, @@ -775,7 +785,7 @@ x=E:Cm-1 s=Sa:s;Bm=m;Os Cs foo=bar!Bs=s!W foo=bar` }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; - const shadower = co.wrap(function *(repos) { + const shadower = co.wrap(function *(repos, mapping) { // If a meta commit was made, map it to "m" and create a branch // named "m" pointing to it. For each submodule commit made, // map the commit to one with that submodule's name, and make a @@ -811,11 +821,13 @@ x=E:Cm-1 s=Sa:s;Bm=m;Os Cs foo=bar!Bs=s!W foo=bar` const subRepo = yield SubmoduleUtil.getRepo(repo, path); const subCommit = yield subRepo.getCommit(subSha); - commitMap[subSha] = path; - yield NodeGit.Branch.create(subRepo, - path, - subCommit, - 1); + if (!(subSha in mapping.commitMap)) { + commitMap[subSha] = path; + yield NodeGit.Branch.create(subRepo, + path, + subCommit, + 1); + } } } return { From 428080ea9cac4cafda456185a36c1b2b9262f3a1 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 16 May 2018 23:57:31 -0400 Subject: [PATCH 146/402] make stash also work on workdir changes to submodule HEAD --- node/lib/util/stash_util.js | 25 +++++++++++++++++-------- node/test/util/stash_util.js | 26 ++++++++++++++++++-------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index cb83ded41..e600e0360 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -148,21 +148,30 @@ exports.save = co.wrap(function *(repo, status, includeUntracked, message) { const wd = sub.workdir; if (null === wd || (wd.status.isClean() && - (!includeUntracked || - 0 === Object.keys(wd.status.workdir).length))) { + (sub.commit === null || + wd.status.headCommit === sub.commit.sha) && + (!includeUntracked || + 0 === Object.keys(wd.status.workdir).length))) { // Nothing to do for closed or clean subs return; // RETURN } const subRepo = yield SubmoduleUtil.getRepo(repo, name); subRepos[name] = subRepo; - const FLAGS = NodeGit.Stash.FLAGS; - const flags = includeUntracked ? - FLAGS.INCLUDE_UNTRACKED : - FLAGS.DEFAULT; - const stashId = yield NodeGit.Stash.save(subRepo, sig, "stash", flags); - subResults[name] = stashId.tostrS(); + let stashId; + if (sub.commit !== null && wd.status.headCommit === sub.commit.sha) { + const FLAGS = NodeGit.Stash.FLAGS; + const flags = includeUntracked ? + FLAGS.INCLUDE_UNTRACKED : + FLAGS.DEFAULT; + stashId = yield NodeGit.Stash.save(subRepo, sig, "stash", + flags); + } + else { + stashId = NodeGit.Oid.fromString(wd.status.headCommit); + } + subResults[name] = stashId.tostrS(); // Record the values we've created. subChanges[name] = new TreeUtil.Change( diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index 3565528ed..e18c934f9 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -220,6 +220,11 @@ x=E:Ci#i foo=bar,1=1;Cw#w foo=bar,1=1;Bi=i;Bw=w`, state: "a=B|x=S:C2-1 README.md,s=Sa:1;Bmaster=2;Os", expected: `x=E:Cstash#s-2 ;Fmeta-stash=s`, }, + "open sub on updated commit, unstaged": { + state: `a=B:Css-1;Bss=ss| + x=S:C2-1 README.md,s=Sa:1;Bmaster=2;Os H=ss`, + expected: `x=E:Cstash#s-2 s=Sa:ss;Fmeta-stash=s;Os Fsub-stash/ss=ss!H=ss`, + }, "open sub with an added file": { state: "a=B|x=S:C2-1 README.md,s=Sa:1;Bmaster=2;Os W foo=bar", expected: `x=E:Cstash#s-2 ;Fmeta-stash=s`, @@ -285,7 +290,7 @@ x=E:Fmeta-stash=s; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; const includeUntracked = c.includeUntracked || false; - const stasher = co.wrap(function *(repos) { + const stasher = co.wrap(function *(repos, mapping) { const repo = repos.x; const stashMessage = c.message || null; let expMessage = c.expectedMessage; @@ -310,23 +315,28 @@ x=E:Fmeta-stash=s; const message = entry.message(); assert.equal(message, expMessage); commitMap[stashId.target().tostrS()] = "s"; - // Look up the commits made for stashed submodules and create // the appropriate mappings. for (let subName in result) { const subSha = result[subName]; - commitMap[subSha] = `s${subName}`; + if (!(subSha in mapping.commitMap)) { + commitMap[subSha] = `s${subName}`; + } + const subRepo = yield SubmoduleUtil.getRepo(repo, subName); const subStash = yield subRepo.getCommit(subSha); - const indexCommit = yield subStash.parent(1); - commitMap[indexCommit.id().tostrS()] = `si${subName}`; - if (includeUntracked) { - const untrackedCommit = yield subStash.parent(2); - commitMap[untrackedCommit.id().tostrS()] = + if (subStash.parentcount() > 1) { + const indexCommit = yield subStash.parent(1); + commitMap[indexCommit.id().tostrS()] = `si${subName}`; + if (includeUntracked) { + const untrackedCommit = yield subStash.parent(2); + commitMap[untrackedCommit.id().tostrS()] = `su${subName}`; + } } } + return { commitMap: commitMap, }; From d09ef0bccac57f82f5993b2c091faf5f558b6425 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 18 May 2018 14:51:17 -0400 Subject: [PATCH 147/402] formatting --- node/test/util/stash_util.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index e18c934f9..6adf88eb3 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -223,7 +223,8 @@ x=E:Ci#i foo=bar,1=1;Cw#w foo=bar,1=1;Bi=i;Bw=w`, "open sub on updated commit, unstaged": { state: `a=B:Css-1;Bss=ss| x=S:C2-1 README.md,s=Sa:1;Bmaster=2;Os H=ss`, - expected: `x=E:Cstash#s-2 s=Sa:ss;Fmeta-stash=s;Os Fsub-stash/ss=ss!H=ss`, + expected: `x=E:Cstash#s-2 s=Sa:ss;Fmeta-stash=s; + Os Fsub-stash/ss=ss!H=ss`, }, "open sub with an added file": { state: "a=B|x=S:C2-1 README.md,s=Sa:1;Bmaster=2;Os W foo=bar", From 525ce8d6512778377fd0769bac408096f4f78c15 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 20 May 2018 14:51:19 -0400 Subject: [PATCH 148/402] sync sparse-checkout & index after open/close The current strategy for sparse mode was to exclude in the sparse-checkout file and index all files besides `.gitmodules`. This strategy was fine as far as `git-meta` was concerned generally, but it confused Git; since `git-meta` defers to Git for many commands (e.g., `diff`), and since we generally want to allow people to use normal Git commands and maintain our repo in a recognizable state, the current strategy is not sufficient. Instead, we need to maintain the invarian that "open" submodules are seen as visible in the sparse mode (not skipped) and "closed" submodules are seen as invisible (skipped). Severl changes were needed to make this happen: * We now consider a repo to be "inSparseMode" if the config is set and don't also check to see if `".gitmodules\n"` is the contents of the `sparse-checkout` file. It was a little pedantic to add that to the check to begin with, and now we're going to have an arbitrary set of paths. * When opening a submodule, we add its path to `sparse-checkout`. * `SubmoduleConfigUtil.deinit` now removes submodules from `sparse-checkout`. Since this would be O(N^2) (e.g. if you're closing all submodules) and in all cases we generally know up front the list of subs to remove, `deinit` now takes a list of subs to deinitialize. * Opening and closing subs now requires an update to the index, since libgit2 (and I'm guessing not even Git) will do what we need by default. To facilitate this, I updated the contracts of the open/close methods, and moved `writeMetaIndex` down into `SparseCheckoutUtil`. * Call `writeMetaIndex` from stash_util, which didn't need to update the index previously * And also from `CloseUtil`. --- node/lib/cmd/open.js | 5 ++ node/lib/util/add.js | 12 +-- node/lib/util/add_submodule.js | 3 +- node/lib/util/checkout.js | 19 ++-- node/lib/util/cherry_pick_util.js | 11 +-- node/lib/util/close_util.js | 24 +++-- node/lib/util/commit.js | 5 +- node/lib/util/git_util.js | 29 ------ node/lib/util/merge_util.js | 9 +- node/lib/util/open.js | 20 ++++- node/lib/util/reset.js | 2 +- node/lib/util/rm.js | 4 +- node/lib/util/sparse_checkout_util.js | 115 ++++++++++++++++++++---- node/lib/util/stash_util.js | 18 ++-- node/lib/util/submodule_config_util.js | 93 ++++++++++--------- node/lib/util/submodule_rebase_util.js | 3 +- node/lib/util/test_util.js | 2 +- node/test/util/push.js | 4 +- node/test/util/read_repo_ast_util.js | 12 +-- node/test/util/sparse_checkout_util.js | 67 ++++++++------ node/test/util/submodule_config_util.js | 12 +-- node/test/util/submodule_util.js | 4 +- 22 files changed, 291 insertions(+), 182 deletions(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index bee290e03..44e5e194e 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -82,6 +82,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { const DoWorkQueue = require("../util/do_work_queue"); const GitUtil = require("../util/git_util"); const Open = require("../util/open"); + const SparseCheckoutUtil = require("../util/sparse_checkout_util"); const SubmoduleConfigUtil = require("../util/submodule_config_util"); const SubmoduleFetcher = require("../util/submodule_fetcher"); const SubmoduleUtil = require("../util/submodule_util"); @@ -141,6 +142,10 @@ Opening ${colors.blue(name)} on ${colors.green(shas[index])}.`); }); yield DoWorkQueue.doInParallel(subsToOpen, opener); + // Make sure the index entries are updated in case we're in sparse mode. + + yield SparseCheckoutUtil.writeMetaIndex(repo, index); + // Run post-open-submodule hook with submodules which opened successfully. yield Hook.execHook("post-open-submodule", subsOpenSuccessfully); diff --git a/node/lib/util/add.js b/node/lib/util/add.js index f4aeff8a3..fb163d4c0 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -37,11 +37,11 @@ const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); -const GitUtil = require("./git_util"); -const RepoStatus = require("./repo_status"); -const StatusUtil = require("./status_util"); -const SubmoduleUtil = require("./submodule_util"); -const UserError = require("./user_error"); +const RepoStatus = require("./repo_status"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); +const StatusUtil = require("./status_util"); +const SubmoduleUtil = require("./submodule_util"); +const UserError = require("./user_error"); /** * Stage modified content at the specified `paths` in the specified `repo`. If @@ -116,6 +116,6 @@ exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update) { if (0 !== toAdd.length) { const index = yield repo.index(); yield toAdd.map(filename => index.addByPath(filename)); - yield GitUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.writeMetaIndex(repo, index); } }); diff --git a/node/lib/util/add_submodule.js b/node/lib/util/add_submodule.js index 2c1482a1e..8377c7c04 100644 --- a/node/lib/util/add_submodule.js +++ b/node/lib/util/add_submodule.js @@ -36,6 +36,7 @@ const colors = require("colors"); const NodeGit = require("nodegit"); const GitUtil = require("./git_util"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); const UserError = require("./user_error"); @@ -65,7 +66,7 @@ exports.addSubmodule = co.wrap(function *(repo, url, filename, importArg) { const urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); urls[filename] = url; yield SubmoduleConfigUtil.writeUrls(repo, index, urls); - yield GitUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.writeMetaIndex(repo, index); const metaUrl = yield GitUtil.getOriginUrl(repo); const templatePath = yield SubmoduleConfigUtil.getTemplatePath(repo); diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 505d27ae3..7bb06f5d6 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -38,14 +38,15 @@ const co = require("co"); const colors = require("colors"); const NodeGit = require("nodegit"); -const DoWorkQueue = require("../util/do_work_queue"); -const GitUtil = require("./git_util"); -const SubmoduleFetcher = require("./submodule_fetcher"); -const Reset = require("./reset"); -const RepoStatus = require("./repo_status"); -const StatusUtil = require("./status_util"); -const SubmoduleUtil = require("./submodule_util"); -const UserError = require("./user_error"); +const DoWorkQueue = require("../util/do_work_queue"); +const GitUtil = require("./git_util"); +const Reset = require("./reset"); +const RepoStatus = require("./repo_status"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); +const StatusUtil = require("./status_util"); +const SubmoduleFetcher = require("./submodule_fetcher"); +const SubmoduleUtil = require("./submodule_util"); +const UserError = require("./user_error"); /** * If the specified `name` matches the tracking branch for one and only one @@ -234,7 +235,7 @@ exports.checkoutCommit = co.wrap(function *(repo, commit, force) { repo.setHeadDetached(commit); }); yield DoWorkQueue.doInParallel(Object.keys(subs), doCheckout); - yield GitUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.writeMetaIndex(repo, index); }); /** diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 73a30d662..3ee5c55f2 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -45,6 +45,7 @@ const Open = require("./open"); const Reset = require("./reset"); const SequencerState = require("./sequencer_state"); const SequencerStateUtil = require("./sequencer_state_util"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); const StatusUtil = require("./status_util"); const Submodule = require("./submodule"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -428,16 +429,16 @@ Please try with normal git commands.`); */ exports.closeSubs = co.wrap(function *(opener, changes) { const repo = opener.repo; - const closeSub = co.wrap(function *(path) { + const toClose = (yield opener.getOpenedSubs()).filter(path => { const commits = changes.commits[path]; if ((undefined === commits || 0 === Object.keys(commits).length) && !(path in changes.conflicts)) { console.log(`Closing ${colors.green(path)}`); - yield SubmoduleConfigUtil.deinit(repo, path); + return true; } + return false; }); - const opened = Array.from(yield opener.getOpenedSubs()); - DoWorkQueue.doInParallel(opened, closeSub); + yield SubmoduleConfigUtil.deinit(repo, toClose); }); /** @@ -492,7 +493,7 @@ exports.rewriteCommit = co.wrap(function *(repo, commit) { errorMessage: errorMessage === "" ? null : errorMessage, newMetaCommit: null, }; - yield GitUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.writeMetaIndex(repo, index); if ("" === errorMessage && (0 !== Object.keys(changes.simpleChanges).length || 0 !== Object.keys(picks.commits).length)) { diff --git a/node/lib/util/close_util.js b/node/lib/util/close_util.js index 90210bb11..350937976 100644 --- a/node/lib/util/close_util.js +++ b/node/lib/util/close_util.js @@ -35,8 +35,8 @@ const NodeGit = require("nodegit"); const co = require("co"); const colors = require("colors"); -const DoWorkQueue = require("../util/do_work_queue"); const Hook = require("../util/hook"); +const SparseCheckoutUtil = require("../util/sparse_checkout_util"); const StatusUtil = require("../util/status_util"); const SubmoduleConfigUtil = require("../util/submodule_config_util"); const SubmoduleUtil = require("../util/submodule_util"); @@ -73,12 +73,10 @@ exports.close = co.wrap(function *(repo, cwd, paths, force) { }); const subStats = repoStatus.submodules; let errorMessage = ""; - let subsClosedSuccessfully = []; - - const closer = co.wrap(function *(name) { + const subsClosedSuccessfully = subsToClose.filter(name => { const sub = subStats[name]; if (undefined === sub || null === sub.workdir) { - return; // RETURN + return false; // RETURN } const subWorkdir = sub.workdir; const subRepo = subWorkdir.status; @@ -93,17 +91,17 @@ exports.close = co.wrap(function *(repo, cwd, paths, force) { Could not close ${colors.cyan(name)} because it is not clean. Pass ${colors.magenta("--force")} to close it anyway. `; - return; // RETURN + return false; // RETURN } } - // TODO: something smarter so that we're not doing an O(N^2) operation - // here. Probably not going to matter for now as few users will every - // have enough submodules open that it will cause a problem. - - yield SubmoduleConfigUtil.deinit(repo, name); - subsClosedSuccessfully.push(name); + return true; // RETURN }); - yield DoWorkQueue.doInParallel(subsToClose, closer); + yield SubmoduleConfigUtil.deinit(repo, subsClosedSuccessfully); + + // Write out the meta index to update SKIP_WORKTREE flags for closed + // submodules. + + yield SparseCheckoutUtil.writeMetaIndex(repo, yield repo.index()); // Run post-close-submodule hook with submodules which closed successfully. yield Hook.execHook("post-close-submodule", subsClosedSuccessfully); diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 36c9ba8e4..32e8c7982 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -50,6 +50,7 @@ const GitUtil = require("./git_util"); const Open = require("./open"); const RepoStatus = require("./repo_status"); const PrintStatusUtil = require("./print_status_util"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); const StatusUtil = require("./status_util"); const Submodule = require("./submodule"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -366,7 +367,7 @@ const stageOpenSubmodules = co.wrap(function *(repo, index, submodules) { yield index.addByPath(name); } })); - yield GitUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.writeMetaIndex(repo, index); }); /** @@ -628,7 +629,7 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message) { } } - yield GitUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.writeMetaIndex(repo, index); // Use 'TreeUtil' to create a new tree having the required paths. diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 2ae160d28..d014c15a7 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -44,8 +44,6 @@ const path = require("path"); const ConfigUtil = require("./config_util"); const DoWorkQueue = require("./do_work_queue"); -const SparseCheckoutUtil = require("./sparse_checkout_util"); -const SubmoduleConfigUtil = require("./submodule_config_util"); const UserError = require("./user_error"); /** @@ -1033,30 +1031,3 @@ exports.hashObject = co.wrap(function *(repo, data) { const res = yield db.write(data, data.length, BLOB); return res; }); - -/** - * Write out the specified `index` for the specified meta-repo, `repo`. - * - * @param {NodeGit.Repository} repo - * @param {NodeGit.Index} index - */ -exports.writeMetaIndex = co.wrap(function *(repo, index) { - assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(index, NodeGit.Index); - - // If we're in sparse mode, manually set bits to skip the worktree since - // libgit2 will not. - - if (yield SparseCheckoutUtil.inSparseMode(repo)) { - const SKIP_WORKTREE = 1 << 14; - const NORMAL = 0; - for (const e of index.entries()) { - if (NORMAL === NodeGit.Index.entryStage(e) && - SubmoduleConfigUtil.modulesFileName !== index.path) { - e.flagsExtended |= SKIP_WORKTREE; - yield index.add(e); - } - } - } - yield index.write(); -}); diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index cbc64b7a3..ac4195f49 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -45,6 +45,7 @@ const Open = require("./open"); const RepoStatus = require("./repo_status"); const SequencerState = require("./sequencer_state"); const SequencerStateUtil = require("./sequencer_state_util"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); const StatusUtil = require("./status_util"); const SubmoduleRebaseUtil = require("./submodule_rebase_util"); const SubmoduleUtil = require("./submodule_util"); @@ -424,9 +425,9 @@ ${colors.red(commitSha)}.`); errorMessage += SubmoduleRebaseUtil.subConflictErrorMessage(name); }); - // We must write the index here or the staging we've done erlier will go + // We must write the index here or the staging we've done earlier will go // away. - yield GitUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.writeMetaIndex(repo, index); if ("" !== errorMessage) { // We're about to fail due to conflict. First, record that there is a @@ -556,7 +557,7 @@ exports.continue = co.wrap(function *(repo) { }); const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo); yield DoWorkQueue.doInParallel(openSubs, continueSub); - yield GitUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.writeMetaIndex(repo, index); if ("" !== errorMessage) { throw new UserError(errorMessage); @@ -625,7 +626,7 @@ exports.abort = co.wrap(function *(repo) { }); yield DoWorkQueue.doInParallel(openSubs, abortSub); yield index.conflictCleanup(); - yield GitUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.writeMetaIndex(repo, index); yield resetMerge(repo); yield SequencerStateUtil.cleanSequencerState(repo.path()); }); diff --git a/node/lib/util/open.js b/node/lib/util/open.js index 54d2d18b1..ed1f2e631 100644 --- a/node/lib/util/open.js +++ b/node/lib/util/open.js @@ -38,6 +38,7 @@ const colors = require("colors"); const NodeGit = require("nodegit"); const GitUtil = require("./git_util"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); const SubmoduleUtil = require("./submodule_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); const SubmoduleFetcher = require("./submodule_fetcher"); @@ -49,6 +50,11 @@ const SubmoduleFetcher = require("./submodule_fetcher"); * to the `url` configured in the meta-repo. If the specified `templatePath` * is provided, use it to configure the newly-opened submodule's repository. * + * Note that after opening one or more submodules, + * `SparseCheckoutUtil.writeMetaIndex` must be called so that `SKIP_WORKTREE` + * is *unset*; since this operation is expensive, we cannot do it automatically + * each time a submodule is opened. + * * @async * @param {SubmoduleFetcher} fetcher * @param {String} submoduleName @@ -87,7 +93,7 @@ exports.openOnCommit = co.wrap(function *(fetcher, yield fetcher.fetchSha(submoduleRepo, submoduleName, submoduleSha); } catch (e) { - yield SubmoduleConfigUtil.deinit(metaRepo, submoduleName); + yield SubmoduleConfigUtil.deinit(metaRepo, [submoduleName]); throw e; } @@ -96,6 +102,13 @@ exports.openOnCommit = co.wrap(function *(fetcher, const commit = yield submoduleRepo.getCommit(submoduleSha); yield GitUtil.setHeadHard(submoduleRepo, commit); + // If we're in sparse mode, we need to add a submodule to the + // `.git/info/sparse-checkout` file so that it's "visible". + + if (yield SparseCheckoutUtil.inSparseMode(metaRepo)) { + yield SparseCheckoutUtil.addToSparseCheckoutFile(metaRepo, + submoduleName); + } return submoduleRepo; }); @@ -194,6 +207,11 @@ Opener.prototype.isOpen = co.wrap(function *(subName) { * Return the repository for the specified `submoduleName`, opening it if * necessary. * + * Note that after opening one or more submodules, + * `SparseCheckoutUtil.writeMetaIndex` must be called so that `SKIP_WORKTREE` + * is *unset*; since this operation is expensive, we cannot do it automatically + * each time a submodule is opened. + * * @param {String} subName * @return {NodeGit.Repository} */ diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index 58c078860..fd8a406cb 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -248,7 +248,7 @@ exports.reset = co.wrap(function *(repo, commit, type) { // Write the index in case we've had to stage submodule changes. - yield GitUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.writeMetaIndex(repo, index); }); /** diff --git a/node/lib/util/rm.js b/node/lib/util/rm.js index b54780076..b358daf6d 100644 --- a/node/lib/util/rm.js +++ b/node/lib/util/rm.js @@ -38,8 +38,8 @@ const groupBy = require("group-by"); const path = require("path"); const NodeGit = require("nodegit"); -const GitUtil = require("./git_util"); const CloseUtil = require("./close_util"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); const SubmoduleUtil = require("./submodule_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); const TextUtil = require("./text_util"); @@ -425,7 +425,7 @@ exports.rmPaths = co.wrap(function *(repo, paths, options) { // https://github.com/nodegit/nodegit/issues/1487 if (toRemove.length !== 0) { yield index.removeAll(toRemove); - yield GitUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.writeMetaIndex(repo, index); } // close to-be-deleted submodules diff --git a/node/lib/util/sparse_checkout_util.js b/node/lib/util/sparse_checkout_util.js index 8d74d6663..59e7947f8 100644 --- a/node/lib/util/sparse_checkout_util.js +++ b/node/lib/util/sparse_checkout_util.js @@ -57,9 +57,7 @@ exports.getSparseCheckoutPath = function (repo) { /** * Return true if the specified `repo` is in sparse mode and false otherwise. - * A repo is in sparse mode iff: `core.sparsecheckout` is true and the contents - * of `.git/info/sparse-checkout` is exactly ".gitmodules\n". We can do - * something more general purpose later if we deem it useful. + * A repo is in sparse mode iff: `core.sparsecheckout` is true. * * @param {NodeGit.Repository} repo * @return {Bool} @@ -67,23 +65,16 @@ exports.getSparseCheckoutPath = function (repo) { exports.inSparseMode = co.wrap(function *(repo) { assert.instanceOf(repo, NodeGit.Repository); - if (!(yield ConfigUtil.configIsTrue(repo, "core.sparsecheckout"))) { - return false; - } - let content; - try { - content = yield fs.readFile(exports.getSparseCheckoutPath(repo), - "utf8"); - } catch (e) { - return false; // RETURN - } - return content === ".gitmodules\n"; + return (yield ConfigUtil.configIsTrue(repo, "core.sparsecheckout")) || + false; }); /** * Configure the specified `repo` to be in sparse-checkout mode -- * specifically, our sparse checkout mode where everything but `.gitmodules` is - * excluded. + * excluded. Note that this method is just for testing; to make it work in a + * real environment you'd also need to udpate the index entries and the + * sparse-checkout file for open submodules. * * @param {NodeGit.Repository} repo */ @@ -94,3 +85,97 @@ exports.setSparseMode = co.wrap(function *(repo) { yield config.setString("core.sparsecheckout", "true"); yield fs.writeFile(exports.getSparseCheckoutPath(repo), ".gitmodules\n"); }); + +/** + * This bit is set in the `flagsExtended` field of a `NodeGit.Index.Entry` for + * paths that should be skipped due to sparse checkout. + */ +const SKIP_WORKTREE = 1 << 14; + +/** + * Return the contents of the `.git/info/sparse-checkout` file for the + * specified `repo`. + * + * @param {NodeGit.Repository} repo + * @return {String} + */ +exports.readSparseCheckout = function (repo) { + assert.instanceOf(repo, NodeGit.Repository); + const filePath = exports.getSparseCheckoutPath(repo); + try { + return fs.readFileSync(filePath, "utf8"); + } catch (e) { + if ("ENOENT" !== e.code) { + throw e; + } + return ""; + } +}; + +/** + * Add the specified `filename` to the set of files visible in the sparse + * checkout in the specified `repo`. + * + * @param {NodeGit.Repository} repo + * @param {String} filename + */ +exports.addToSparseCheckoutFile = co.wrap(function *(repo, filename) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(filename); + + const sparsePath = exports.getSparseCheckoutPath(repo); + yield fs.appendFile(sparsePath, filename + "\n"); +}); + +/** + * Remove the specified `filenames` from the set of files visible in the sparse + * checkout in the specified `repo`. + * + * @param {NodeGit.Repository} repo + * @param {String[]} filenames + */ +exports.removeFromSparseCheckoutFile = function (repo, filenames) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isArray(filenames); + const sparseFile = exports.readSparseCheckout(repo); + const toRemoveSet = new Set(filenames); + const newContent = sparseFile.split("\n").filter( + name => !toRemoveSet.has(name)); + fs.writeFileSync(exports.getSparseCheckoutPath(repo), + newContent.join("\n")); +}; + +/** + * Write out the specified `index` for the specified meta-repo, `repo`, set the + * index flags to the correct values based on the contents of + * `.git/info/sparse-checkout`, which libgit2 does not do. + * + * TODO: independent test + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Index} index + */ +exports.writeMetaIndex = co.wrap(function *(repo, index) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(index, NodeGit.Index); + + // If we're in sparse mode, manually set bits to skip the worktree since + // libgit2 will not. + + if (yield exports.inSparseMode(repo)) { + const sparseCheckout = exports.readSparseCheckout(repo); + const sparseSet = new Set(sparseCheckout.split("\n")); + const NORMAL = 0; + for (const e of index.entries()) { + if (NORMAL === NodeGit.Index.entryStage(e)) { + if (sparseSet.has(e.path)) { + e.flagsExtended &= ~SKIP_WORKTREE; + } else { + e.flagsExtended |= SKIP_WORKTREE; + } + yield index.add(e); + } + } + } + yield index.write(); +}); diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index e600e0360..a6b12f806 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -35,14 +35,15 @@ const co = require("co"); const colors = require("colors"); const NodeGit = require("nodegit"); -const GitUtil = require("./git_util"); -const Open = require("./open"); -const PrintStatusUtil = require("./print_status_util"); -const RepoStatus = require("./repo_status"); -const StatusUtil = require("./status_util"); -const SubmoduleUtil = require("./submodule_util"); -const TreeUtil = require("./tree_util"); -const UserError = require("./user_error"); +const GitUtil = require("./git_util"); +const Open = require("./open"); +const PrintStatusUtil = require("./print_status_util"); +const RepoStatus = require("./repo_status"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); +const StatusUtil = require("./status_util"); +const SubmoduleUtil = require("./submodule_util"); +const TreeUtil = require("./tree_util"); +const UserError = require("./user_error"); /** * Return the IDs of tress reflecting the current state of the index and @@ -349,6 +350,7 @@ ${colors.red(name)}`); result[name] = stashSha; } })); + yield SparseCheckoutUtil.writeMetaIndex(repo, yield repo.index()); return result; }); diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index 42d6a8fd8..fd55d431f 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -53,7 +53,8 @@ const path = require("path"); const rimraf = require("rimraf"); const url = require("url"); -const GitUtil = require("./git_util"); +const DoWorkQueue = require("./do_work_queue"); +const GitUtil = require("./git_util"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const UserError = require("./user_error"); @@ -139,61 +140,73 @@ exports.clearSubmoduleConfigEntry = }); /** - * De-initialize the repository having the specified `submoduleName` in the + * De-initialize the repositories having the specified `submoduleNames` in the * specified `repo`. * + * Note that after calling this method, `SparseCheckoutUtil.writeMetaIndex` + * must be called to update the SKIP_WORKTREE flags for closed submodules. + * * @async * @param {NodeGit.Repository} repo - * @param {String} submoduleName + * @param {String[]} submoduleNames */ -exports.deinit = co.wrap(function *(repo, submoduleName) { +exports.deinit = co.wrap(function *(repo, submoduleNames) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isArray(submoduleNames); - // This operation is a major pain, first because libgit2 does not provide - // any direct methods to do the equivalent of 'git deinit', and second - // because nodegit does not expose the method that libgit2 does provide to - // delete an entry from the config file. + const sparse = yield SparseCheckoutUtil.inSparseMode(repo); - // De-initting a submodule requires the following things: - // 1. Confirms there are no unpushed (to any remote) commits - // or uncommited changes (including new files). - // 2. Remove all files under the path of the submodule, but not the - // directory itself, which would look to Git as if we were trying - // to remove the submodule. - // 3. Remove the entry for the submodule from the '.git/config' file. - // 4. Remove the directory .git/modules/ + const deinitOne = co.wrap(function *(submoduleName) { - // We will clear out the path for the submodule. + // This operation is a major pain, first because libgit2 does not + // provide any direct methods to do the equivalent of 'git deinit', and + // second because nodegit does not expose the method that libgit2 does - const rootDir = repo.workdir(); - const submodulePath = path.join(rootDir, submoduleName); + // De-initting a submodule requires the following things: + // 1. Confirms there are no unpushed (to any remote) commits + // or uncommited changes (including new files). + // 2. Remove all files under the path of the submodule, but not the + // directory itself, which would look to Git as if we were trying + // to remove the submodule. + // 3. Remove the entry for the submodule from the '.git/config' file. + // 4. Remove the directory .git/modules/ - const sparse = yield SparseCheckoutUtil.inSparseMode(repo); - if (sparse) { - // Clear out submodule's contents. + // We will clear out the path for the submodule. - yield doRimRaf(submodulePath); + const rootDir = repo.workdir(); + const submodulePath = path.join(rootDir, submoduleName); - // Clear parent directories until they're all gone or we find one - // that's non-empty. + if (sparse) { + // Clear out submodule's contents. - let next = path.dirname(submoduleName); - try { - while ("." !== next) { - yield fs.rmdir(path.join(rootDir, next)); - next = path.dirname(next); - } - } catch (e) { - if ("ENOTEMPTY" !== e.code) { - throw e; + yield doRimRaf(submodulePath); + + // Clear parent directories until they're all gone or we find one + // that's non-empty. + + let next = path.dirname(submoduleName); + try { + while ("." !== next) { + yield fs.rmdir(path.join(rootDir, next)); + next = path.dirname(next); + } + } catch (e) { + if ("ENOTEMPTY" !== e.code) { + throw e; + } } + } else { + const files = yield fs.readdir(submodulePath); + yield files.map(co.wrap(function *(filename) { + yield doRimRaf(path.join(submodulePath, filename)); + })); } - } else { - const files = yield fs.readdir(submodulePath); - yield files.map(co.wrap(function *(filename) { - yield doRimRaf(path.join(submodulePath, filename)); - })); + yield exports.clearSubmoduleConfigEntry(repo.path(), submoduleName); + }); + yield DoWorkQueue.doInParallel(submoduleNames, deinitOne); + if (yield SparseCheckoutUtil.inSparseMode(repo)) { + SparseCheckoutUtil.removeFromSparseCheckoutFile(repo, submoduleNames); } - yield exports.clearSubmoduleConfigEntry(repo.path(), submoduleName); }); /** diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index fe9683985..4b2465f36 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -41,6 +41,7 @@ const GitUtil = require("./git_util"); const DoWorkQueue = require("./do_work_queue"); const RebaseFileUtil = require("./rebase_file_util"); const RepoStatus = require("./repo_status"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); const SubmoduleUtil = require("./submodule_util"); /** @@ -360,7 +361,7 @@ ${colors.green(rebaseInfo.onto)}.`); } }); yield DoWorkQueue.doInParallel(Object.keys(subs), continueSub); - yield GitUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.writeMetaIndex(repo, index); const result = { errorMessage: "" === errorMessage ? null : errorMessage, commits: commits, diff --git a/node/lib/util/test_util.js b/node/lib/util/test_util.js index 6f40651c7..cdb52540e 100644 --- a/node/lib/util/test_util.js +++ b/node/lib/util/test_util.js @@ -39,7 +39,7 @@ const co = require("co"); const fs = require("fs-promise"); const path = require("path"); const NodeGit = require("nodegit"); -const temp = require("temp").track(); +const temp = require("temp"); /** * Return the path to a newly-created temporary directory. diff --git a/node/test/util/push.js b/node/test/util/push.js index 42b752759..ec4645068 100644 --- a/node/test/util/push.js +++ b/node/test/util/push.js @@ -301,9 +301,7 @@ describe("getPushMap", function () { for (const closeSubs of [false, true]) { if (closeSubs) { const subs = yield SubmoduleUtil.listOpenSubmodules(repo); - for (const sub of subs) { - yield SubmoduleConfigUtil.deinit(repo, sub); - } + yield SubmoduleConfigUtil.deinit(repo, subs); } const pushMap = yield Push.getPushMap(repo, "origin", source, diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index 7d9044338..3cbf504c9 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -540,7 +540,7 @@ describe("readRAST", function () { const commit = yield TestUtil.makeCommit(repo, ["x/y", ".gitmodules"]); - yield SubmoduleConfigUtil.deinit(repo, "x/y"); + yield SubmoduleConfigUtil.deinit(repo, ["x/y"]); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ changes: { "README.md": new File("", false)}, @@ -588,7 +588,7 @@ describe("readRAST", function () { const index = yield repo.index(); yield index.addByPath(".gitmodules"); yield index.write(); - yield SubmoduleConfigUtil.deinit(repo, "x/y"); + yield SubmoduleConfigUtil.deinit(repo, ["x/y"]); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ changes: { "README.md": new File("", false)}, @@ -639,7 +639,7 @@ describe("readRAST", function () { const subRepo = yield submodule.open(); const anotherSubCommit = yield TestUtil.generateCommit(subRepo); const lastCommit = yield TestUtil.makeCommit(repo, ["x/y"]); - yield SubmoduleConfigUtil.deinit(repo, "x/y"); + yield SubmoduleConfigUtil.deinit(repo, ["x/y"]); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ @@ -800,7 +800,7 @@ describe("readRAST", function () { baseSubPath, "x/y", subHead.id().tostrS()); - yield SubmoduleConfigUtil.deinit(repo, "x/y"); + yield SubmoduleConfigUtil.deinit(repo, ["x/y"]); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ @@ -845,7 +845,7 @@ describe("readRAST", function () { const nextSubCommit = yield TestUtil.generateCommit(subRepo); const index = yield repo.index(); yield index.addAll("x/y", -1); - yield SubmoduleConfigUtil.deinit(repo, "x/y"); + yield SubmoduleConfigUtil.deinit(repo, ["x/y"]); let commits = {}; commits[headCommit.id().tostrS()] = new Commit({ @@ -1523,7 +1523,7 @@ describe("readRAST", function () { const modules = ".gitmodules"; const nextCommit = yield TestUtil.makeCommit(repo, ["a", modules]); const nextSha = nextCommit.id().tostrS(); - yield SubmoduleConfigUtil.deinit(repo, "a"); + yield SubmoduleConfigUtil.deinit(repo, ["a"]); yield fs.writeFile(path.join(repo.workdir(), modules), diff --git a/node/test/util/sparse_checkout_util.js b/node/test/util/sparse_checkout_util.js index 135f566c7..0bad2aa02 100644 --- a/node/test/util/sparse_checkout_util.js +++ b/node/test/util/sparse_checkout_util.js @@ -32,7 +32,6 @@ const assert = require("chai").assert; const co = require("co"); -const fs = require("fs-promise"); const path = require("path"); const SparseCheckoutUtil = require("../../lib/util/sparse_checkout_util"); @@ -53,45 +52,61 @@ describe("inSparseMode", function () { const result = yield SparseCheckoutUtil.inSparseMode(repo); assert.equal(result, false); })); - it("configured but no file", co.wrap(function *() { + it("configured", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); const config = yield repo.config(); yield config.setString("core.sparsecheckout", "true"); const result = yield SparseCheckoutUtil.inSparseMode(repo); - assert.equal(result, false); + assert.equal(result, true); })); - it("configured but wrong file", co.wrap(function *() { +}); +describe("setSparseMode", function () { + it("breathing test", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); - const config = yield repo.config(); - yield config.setString("core.sparsecheckout", "true"); - yield fs.writeFile(SparseCheckoutUtil.getSparseCheckoutPath(repo), - "foo\n"); - const result = yield SparseCheckoutUtil.inSparseMode(repo); - assert.equal(result, false); + yield SparseCheckoutUtil.setSparseMode(repo); + const isSet = yield SparseCheckoutUtil.inSparseMode(repo); + assert.equal(isSet, true); + const content = SparseCheckoutUtil.readSparseCheckout(repo); + assert.equal(content, ".gitmodules\n"); })); - it("configured good file", co.wrap(function *() { +}); +describe("readSparseCheckout", function () { + it("doesn't exist", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); - const config = yield repo.config(); - yield config.setString("core.sparsecheckout", "true"); - yield fs.writeFile(SparseCheckoutUtil.getSparseCheckoutPath(repo), - ".gitmodules\n"); - const result = yield SparseCheckoutUtil.inSparseMode(repo); - assert.equal(result, true); + const result = SparseCheckoutUtil.readSparseCheckout(repo); + assert.equal(result, ""); })); - it("good file, not configured", co.wrap(function *() { + it("exists", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); - yield fs.writeFile(SparseCheckoutUtil.getSparseCheckoutPath(repo), - ".gitmodules\n"); - const result = yield SparseCheckoutUtil.inSparseMode(repo); - assert.equal(result, false); + yield SparseCheckoutUtil.setSparseMode(repo); + const result = SparseCheckoutUtil.readSparseCheckout(repo); + assert.equal(result, ".gitmodules\n"); })); }); -describe("setSparseMode", function () { - it("breathing test", co.wrap(function *() { +describe("addToSparseCheckoutFile", function () { + it("breathing", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); yield SparseCheckoutUtil.setSparseMode(repo); - const isSet = yield SparseCheckoutUtil.inSparseMode(repo); - assert.equal(isSet, true); + yield SparseCheckoutUtil.addToSparseCheckoutFile(repo, "foo"); + const result = SparseCheckoutUtil.readSparseCheckout(repo); + assert.equal(result, ".gitmodules\nfoo\n"); + })); +}); +describe("removeFromSparseCheckoutFile", function () { + it("nothing to remove", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + yield SparseCheckoutUtil.setSparseMode(repo); + SparseCheckoutUtil.removeFromSparseCheckoutFile(repo, ["foo"]); + const result = SparseCheckoutUtil.readSparseCheckout(repo); + assert.equal(result, ".gitmodules\n"); + })); + it("remove one", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + yield SparseCheckoutUtil.setSparseMode(repo); + yield SparseCheckoutUtil.addToSparseCheckoutFile(repo, "foo"); + SparseCheckoutUtil.removeFromSparseCheckoutFile(repo, ["foo"]); + const result = SparseCheckoutUtil.readSparseCheckout(repo); + assert.equal(result, ".gitmodules\n"); })); }); }); diff --git a/node/test/util/submodule_config_util.js b/node/test/util/submodule_config_util.js index 50550f3bf..6782ec876 100644 --- a/node/test/util/submodule_config_util.js +++ b/node/test/util/submodule_config_util.js @@ -116,7 +116,7 @@ describe("SubmoduleConfigUtil", function () { // Then close it and recheck status. - yield SubmoduleConfigUtil.deinit(repo, "x/y"); + yield SubmoduleConfigUtil.deinit(repo, ["x/y"]); const closedStatus = yield NodeGit.Submodule.status(repo, "x/y", 0); assert(closedStatus & WD_UNINITIALIZED); @@ -151,7 +151,7 @@ describe("SubmoduleConfigUtil", function () { yield TestUtil.makeCommit(repo, ["x/y", ".gitmodules"]); yield SparseCheckoutUtil.setSparseMode(repo); - yield SubmoduleConfigUtil.deinit(repo, "x/y"); + yield SubmoduleConfigUtil.deinit(repo, ["x/y"]); // Verify that directory for sub is gone @@ -166,7 +166,7 @@ describe("SubmoduleConfigUtil", function () { // verify we clean the root when all is gone failed = false; - yield SubmoduleConfigUtil.deinit(repo, "x/z"); + yield SubmoduleConfigUtil.deinit(repo, ["x/z"]); try { yield fs.readdir(path.join(repo.workdir(), "x")); } catch (e) { @@ -665,7 +665,7 @@ foo sig, sig, "my message"); - yield SubmoduleConfigUtil.deinit(repo, subName); + yield SubmoduleConfigUtil.deinit(repo, [subName]); const repoPath = repo.workdir(); const result = yield SubmoduleConfigUtil.initSubmoduleAndRepo( originUrl, @@ -708,7 +708,7 @@ foo const sub = yield NodeGit.Submodule.lookup(repo, "foo"); const subRepo = yield sub.open(); NodeGit.Remote.setUrl(subRepo, "origin", "/bar"); - yield SubmoduleConfigUtil.deinit(repo, "foo"); + yield SubmoduleConfigUtil.deinit(repo, ["foo"]); const newSub = yield SubmoduleConfigUtil.initSubmoduleAndRepo("", repo, @@ -776,7 +776,7 @@ foo sig, sig, "my message"); - yield SubmoduleConfigUtil.deinit(repo, "foo"); + yield SubmoduleConfigUtil.deinit(repo, ["foo"]); // Remove `foo` dir, otherwise, we will not need to re-init the // repo and the template will not be executed. diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index 72b504a82..ad99952fb 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -322,9 +322,7 @@ describe("SubmoduleUtil", function () { for (const closeSubs of [false, true]) { if (closeSubs) { const subs = yield SubmoduleUtil.listOpenSubmodules(x); - for (const sub of subs) { - yield SubmoduleConfigUtil.deinit(x, sub); - } + yield SubmoduleConfigUtil.deinit(x, subs); } const dut = SubmoduleUtil; const result = yield dut.listAbsorbedSubmodules(x); From c9f0e360254c317adf98578c25361df306d5cb91 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 21 May 2018 12:58:46 -0400 Subject: [PATCH 149/402] fix two issues in push --- node/lib/util/push.js | 10 +++++++++- node/lib/util/submodule_util.js | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 1301393e2..785738120 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -139,7 +139,15 @@ exports.getPushMap = co.wrap(function*(repo, remoteName, source, target, delete pushMap[sub]; continue; } - const commitToPush = yield subRepo.getCommit(pushMap[sub]); + let commitToPush; + try { + commitToPush = yield subRepo.getCommit(pushMap[sub]); + } catch (e) { + // We haven't fetched this commit in this submodule, + // so we can't push + delete pushMap[sub]; + continue; + } let trackingCommit; try { trackingCommit = yield subRepo.getCommit(sha); diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 3abf8fb4d..7be396fa1 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -240,7 +240,7 @@ exports.listAbsorbedSubmodules = co.wrap(function*(repo) { filtered.push(name); } } - nodeNamesArray.splice(0, nodeNamesArray.length, filtered); + nodeNamesArray.splice(0, nodeNamesArray.length, ...filtered); out.push(root.substring(modules_dir.length + 1)); } }); From a651a5918e8d531cb975cce6b48314952a6e87ea Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 23 May 2018 14:17:03 -0400 Subject: [PATCH 150/402] handle no-tracking case in push --- node/lib/util/push.js | 17 +++++++++++++++++ node/test/util/push.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 785738120..219e549fc 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -118,6 +118,7 @@ exports.getPushMap = co.wrap(function*(repo, remoteName, source, target, } } + let anyTrackingBranches = false; for (const branch of trackingBranches) { let reference = null; try { @@ -126,6 +127,7 @@ exports.getPushMap = co.wrap(function*(repo, remoteName, source, target, // if this ref doesn't exist, OK continue; } + anyTrackingBranches = true; const id = reference.target(); const trackingCommit = yield NodeGit.Commit.lookup( repo, id); @@ -165,6 +167,21 @@ exports.getPushMap = co.wrap(function*(repo, remoteName, source, target, } } } + + if (!anyTrackingBranches) { + // We make sure we actually locally have the commits we want + // to push. If there were any tracking branches, this would + // have been handled above, but since there aren't, we've got + // to do it here. + for (const sub of Object.keys(pushMap)) { + const subRepo = bareRepos[sub]; + try { + yield subRepo.getCommit(pushMap[sub]); + } catch (e) { + delete pushMap[sub]; + } + } + } return pushMap; }); diff --git a/node/test/util/push.js b/node/test/util/push.js index ec4645068..1e30a4aef 100644 --- a/node/test/util/push.js +++ b/node/test/util/push.js @@ -331,6 +331,35 @@ describe("getPushMap", function () { manipulator); })); }); + + it("handles a case with no tracking branches", co.wrap(function*() { + // We can't use the usual multi-repo test for this because it + // gets confused about which commits should exist in which + // submodules. Instead, we create a meta commit in one repo + // then do a fetch into a fresh repo (which has ever opened + // the repo in question but which doesn't have the + // newly-fetched commit). + + const them = "sub=S:C8-1;Bmaster=8|x=S:C2-1 d=Ssub:8;Bmaster=2"; + const us = "sub=S:C7-1;Bmaster=7|x=S:C3-1 d=Ssub:7;Bmaster=3;Od"; + + const theirWritten = yield RepoASTTestUtil.createMultiRepos(them); + const theirRepo = theirWritten.repos.x; + const theirCommitMap = theirWritten.reverseCommitMap; + + const ourWritten = yield RepoASTTestUtil.createMultiRepos(us); + + const ourRepo = ourWritten.repos.x; + const config = yield ourRepo.config(); + yield config.setString("remote.upstream.url", theirRepo.path()); + yield GitUtil.fetch(ourRepo, "upstream"); + + const sha = theirCommitMap["2"]; + const commit = yield ourRepo.getCommit(sha); + const pushMap = yield Push.getPushMap(ourRepo, "upstream", "2", + "target", commit); + assert.deepEqual({}, pushMap); + })); }); describe("push", function () { From 93fceb4ae67897c8dcd155c378ff6e5c2387806e Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 22 May 2018 17:27:06 -0400 Subject: [PATCH 151/402] Use libgit2 to compute submodule tree changes Addresses: https://github.com/twosigma/git-meta/issues/595 Previously, I was manually computing what submodule changes and conflicts would result from a merge or cherry-pick (rebase uses cherry-pick). In the case of a merge, I used "the" merge base between the head and target commits -- however, there may be many merge bases, and the logic used to determine what to do is beyond the scope of git-meta. I started doing this instead of the obvious `Merge.merge` or `CherryPick.cherryPick` command due to the littany of problems I have using libgit2 in the workdir of the meta-repo (performance, correctness e.g., with sparse-checkouts, etc.). With this change, I go back to using libgit2 to do this computation, but I'm using the "commits" versions of these commands -- `Merge.commits` and `CherryPick.commits` -- which do not touch the worktree, but work only on commits. Now, the only manual deduction I do is to determine which submodule conflicts flagged by libgit2 are candidates for resolution (via merge/cherry-pick in the affected submodule). As can be seen from the test case added to `merge`, it does solve the problem. --- node/lib/util/cherry_pick_util.js | 225 ++++++++++++++--------------- node/lib/util/merge_util.js | 4 +- node/test/util/cherry_pick_util.js | 48 +++--- node/test/util/merge_util.js | 12 ++ 4 files changed, 146 insertions(+), 143 deletions(-) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 3ee5c55f2..9d2b7c597 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -42,15 +42,17 @@ const ConflictUtil = require("./conflict_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); const Open = require("./open"); +const RepoStatus = require("./repo_status"); const Reset = require("./reset"); const SequencerState = require("./sequencer_state"); const SequencerStateUtil = require("./sequencer_state_util"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const StatusUtil = require("./status_util"); const Submodule = require("./submodule"); +const SubmoduleChange = require("./submodule_change"); +const SubmoduleUtil = require("./submodule_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); const SubmoduleRebaseUtil = require("./submodule_rebase_util"); -const SubmoduleUtil = require("./submodule_util"); const TreeUtil = require("./tree_util"); const UserError = require("./user_error"); @@ -175,139 +177,123 @@ exports.containsUrlChanges = co.wrap(function *(repo, commit, baseCommit) { return false; }); -/** - * Return the entry for the specified `path` in the optionally specified `tree` - * or null if `null === tree` or `path` does not exist in `tree`. - * - * @param {NodeGit.Tree|null} tree - * @param {String} path - * @return {NodeGit.TreeEntry} - */ -const getTreeEntry = co.wrap(function *(tree, path) { - if (null === tree) { - return null; - } - try { - return yield tree.entryByPath(path); - } catch (e) { - // only way to tell if entry doesn't exist - } - return null; -}); - /** * Determine how to apply the submodule changes introduced in the - * specified `commit` to the commit on the head of the specified `repo`. - * Return an object describing what changes to make, including which submodules - * cannot be updated at all due to a conflicts, such as a change being - * introduced to a submodule that does not exist in HEAD. If the specified - * `fromBase` is true, comput the changes from the merge base between `commit` - * and HEAD; otherwise, compute them between `commit` and its first parent. - * Throw a `UserError` if non-submodule changes are detected. The behavior is - * undefined if there is no merge base between HEAD and `commit`. + * specified `targetCommit` to the commit on the head of the specified `repo` + * as described in the specified in-memory `index`. Return an object + * describing what changes to make, including which submodules cannot be + * updated at all due to a conflicts, such as a change being introduced to a + * submodule that does not exist in HEAD. Throw a `UserError` if non-submodule + * changes are detected. The behavior is undefined if there is no merge base + * between HEAD and `commit`. + * + * Note that this method will cause conflicts in `index` to be cleaned up. * * @param {NodeGit.Repository} repo - * @param {NodeGit.Commit} commit - * @param {Bool} fromBase + * @param {NodeGit.Index} index + * @param {NodeGit.Commit} targetCommit * @return {Object} return * @return {Object} return.changes from sub name to `SubmoduleChange` * @return {Object} return.simpleChanges from sub name to `Submodule` or null - * @return {Object} return.conflicts map from sub name to `Conflict` + * @return {Object} return.conflicts from sub name to `Conflict` */ -exports.computeChanges = co.wrap(function *(repo, commit, fromBase) { +exports.computeChanges = co.wrap(function *(repo, index, targetCommit) { assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(commit, NodeGit.Commit); - assert.isBoolean(fromBase); + assert.instanceOf(index, NodeGit.Index); + assert.instanceOf(targetCommit, NodeGit.Commit); + + const conflicts = {}; + const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit( + repo, + targetCommit); + + // Group together all parts of conflicted entries. + + const conflictEntries = new Map(); // name -> normal, ours, theirs + const entries = index.entries(); + const STAGE = RepoStatus.STAGE; + for (const entry of entries) { + const name = entry.path; + const stage = NodeGit.Index.entryStage(entry); + if (STAGE.NORMAL !== stage) { + let subEntry = conflictEntries.get(name); + if (undefined === subEntry) { + subEntry = {}; + conflictEntries.set(name, subEntry); + } + subEntry[stage] = entry; + } + } - const head = yield repo.getHeadCommit(); - const headTree = yield head.getTree(); - const mergeBase = yield GitUtil.getMergeBase(repo, head, commit); - assert.isNotNull(mergeBase); - const baseTree = yield mergeBase.getTree(); - const changeBase = fromBase ? mergeBase : null; - const changes = yield SubmoduleUtil.getSubmoduleChanges(repo, - commit, - changeBase, - false); - const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, - commit); - const result = { - changes: {}, - simpleChanges: {}, - conflicts: {}, - }; + // Now, look at `conflictEntries` and see if any are eligible for further + // work -- basically, submodule changes where there is a conflict that + // could be resolved by an internal merge, cherry-pick, etc. Otherwise, + // log and resolve conflicts. + const COMMIT = NodeGit.TreeEntry.FILEMODE.COMMIT; const ConflictEntry = ConflictUtil.ConflictEntry; const Conflict = ConflictUtil.Conflict; - const FILEMODE = NodeGit.TreeEntry.FILEMODE; - - yield Object.keys(changes).map(co.wrap(function *(sub) { - const change = changes[sub]; - const headEntry = yield getTreeEntry(headTree, sub); - - const makeConflict = co.wrap(function *() { - const baseEntry = yield getTreeEntry(baseTree, sub); - let ancestor = null; - if (null !== baseEntry) { - ancestor = new ConflictEntry(baseEntry.filemode(), - baseEntry.sha()); - } - let our = null; - if (null !== headEntry) { - our = new ConflictEntry(headEntry.filemode(), headEntry.sha()); - } - let their = null; - if (null !== change.newSha) { - their = new ConflictEntry(FILEMODE.COMMIT, change.newSha); - } - return new Conflict(ancestor, our, their); - }); - - if (null === headEntry) { - // If doesn't exist on HEAD, only valid change is an addition; - // ignore a removal. + const changes = {}; + function makeConflict(entry) { + if (undefined === entry) { + return null; + } + return new ConflictEntry(entry.mode, entry.id.tostrS()); + } + for (const [name, entries] of conflictEntries) { + const ancestor = entries[STAGE.ANCESTOR]; + const ours = entries[STAGE.OURS]; + const theirs = entries[STAGE.THEIRS]; + if (undefined !== ancestor && + undefined !== ours && + undefined !== theirs && + COMMIT === ours.mode && + COMMIT === theirs.mode) { + changes[name] = new SubmoduleChange(ancestor.id.tostrS(), + theirs.id.tostrS()); + } else if (SubmoduleConfigUtil.modulesFileName !== name) { + conflicts[name] = new Conflict(makeConflict(ancestor), + makeConflict(ours), + makeConflict(theirs)); + } + } - if (null === change.oldSha) { - result.simpleChanges[sub] = new Submodule(urls[sub], - change.newSha); - } else if (null !== change.newSha) { - result.conflicts[sub] = yield makeConflict(); - } - } else if (FILEMODE.COMMIT !== headEntry.filemode()) { - // If the path on HEAD is not a submodule, we have a conflict. - result.conflicts[sub] = yield makeConflict(); - } else if (null === change.oldSha) { - // We have an addition. We've already covered the case where this - // sub doesn't exist on head, so it's going to be a conflict unless - // the SHA on HEAD is the same. + // Now we handle the changes that Git was able to take care of by itself. + // First, we're going to need to write the index to a tree; this write + // requires that we clean the conflicts. Anything we've already diagnosed + // as either a conflict or a non-simple change will be ignored here. - if (headEntry.sha() !== change.newSha) { - result.conflicts[sub] = yield makeConflict(); - } - } else if (null === change.newSha) { - // Register a deletion unless the SHA was changed on HEAD. - - if (headEntry.sha() === change.oldSha) { - result.simpleChanges[sub] = null; - } else { - result.conflicts[sub] = yield makeConflict(); - } - } else if (change.newSha !== headEntry.sha()) { - // Finally, we have a normal update, new commits in the submodule. - // We still deem it to be a "simple" change not needing a - // cherry-pick if the old sha for the change is the same as that on - // head. - - if (change.oldSha === headEntry.sha()) { - result.simpleChanges[sub] = new Submodule(urls[sub], - change.newSha); - } else { - result.changes[sub] = change; - } + yield index.conflictCleanup(); + const simpleChanges = {}; + const treeId = yield index.writeTreeTo(repo); + const tree = yield NodeGit.Tree.lookup(repo, treeId); + const head = yield repo.getHeadCommit(); + const headTree = yield head.getTree(); + const diff = yield NodeGit.Diff.treeToTree(repo, headTree, tree, null); + const treeChanges = + yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff, false); + for (let name in treeChanges) { + // Skip changes we've already taken into account and the `.gitmodules` + // file. + + if (SubmoduleConfigUtil.modulesFileName === name || + name in changes || + name in conflicts) { + continue; // CONTINUE } - })); - return result; + const change = treeChanges[name]; + if (null === change.newSha) { + simpleChanges[name] = null; + } else { + simpleChanges[name] = new Submodule(urls[name], change.newSha); + } + } + return { + simpleChanges: simpleChanges, + changes: changes, + conflicts: conflicts, + }; }); /** @@ -463,7 +449,10 @@ exports.rewriteCommit = co.wrap(function *(repo, commit) { yield exports.ensureNoURLChanges(repo, commit); - const changes = yield exports.computeChanges(repo, commit, false); + const head = yield repo.getHeadCommit(); + const changeIndex = + yield NodeGit.Cherrypick.commit(repo, commit, head, 0, []); + const changes = yield exports.computeChanges(repo, changeIndex, commit); const index = yield repo.index(); // Perform simple changes that don't require picks -- addition, deletions, diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index ac4195f49..91e7c0252 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -395,7 +395,9 @@ ${colors.red(commitSha)}.`); const sig = repo.defaultSignature(); - const changes = yield CherryPickUtil.computeChanges(repo, commit, true); + const changeIndex = yield NodeGit.Merge.commits(repo, head, commit, []); + const changes = + yield CherryPickUtil.computeChanges(repo, changeIndex, commit); const index = yield repo.index(); const opener = new Open.Opener(repo, null); diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index 1b8f9ed6f..eb84ebeec 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -41,6 +41,7 @@ const Open = require("../../lib/util/open"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); const Submodule = require("../../lib/util/submodule"); const SubmoduleChange = require("../../lib/util/submodule_change"); +const UserError = require("../../lib/util/user_error"); /** * Return a commit map as expected from a manipulator for `RepoASTTestUtil` @@ -235,7 +236,7 @@ describe("containsUrlChanges", function () { })); }); }); -describe("computeChanges", function () { +describe("computeChangesFromIndex", function () { const Conflict = ConflictUtil.Conflict; const ConflictEntry = ConflictUtil.ConflictEntry; const FILEMODE = NodeGit.TreeEntry.FILEMODE; @@ -266,14 +267,6 @@ x=U:C3-2 s=Sa:a;Ct-2 s=Sa:b;Bt=t;Bmaster=3`, "s": new Submodule("a", "1"), }, }, - "addition in ancestor, but from base": { - input: "a=B|x=S:Ct-2 s=Sa:1;C2-1 t=Sa:1;Bt=t", - simpleChanges: { - "s": new Submodule("a", "1"), - "t": new Submodule("a", "1"), - }, - fromBase: true, - }, "double addition": { input: ` a=B:Ca-1;Cb-1;Ba=a;Bb=b| @@ -309,7 +302,7 @@ x=S:C2-1 s=Sa:a;Ct-1 s=Sa:a;Bmaster=2;Bt=t`, "change, but never on HEAD": { input: "a=B:Ca-1;Ba=a|x=U:Bmaster=1;Ct-2 s=Sa:a;Bt=t", conflicts: { - "s": new Conflict(null, + "s": new Conflict(new ConflictEntry(FILEMODE.COMMIT, "1"), null, new ConflictEntry(FILEMODE.COMMIT, "a")), }, @@ -321,6 +314,7 @@ x=S:C2-1 s=Sa:a;Ct-1 s=Sa:a;Bmaster=2;Bt=t`, new ConflictEntry(FILEMODE.BLOB, "foo"), null), }, + fails: true, }, "deletion, but was changed on HEAD": { input: ` @@ -342,11 +336,26 @@ x=U:C3-2 s=Sa:a;Ct-2 s;Bt=t;Bmaster=3`, const commitMap = w.commitMap; const reverseCommitMap = w.reverseCommitMap; const urlMap = w.urlMap; + const head = yield repo.getHeadCommit(); const target = yield repo.getCommit(reverseCommitMap.t); - const fromBase = c.fromBase || false; - const result = yield CherryPickUtil.computeChanges(repo, - target, - fromBase); + const index = + yield NodeGit.Cherrypick.commit(repo, target, head, 0, []); + let result; + let exception; + try { + result = yield CherryPickUtil.computeChanges(repo, + index, + target); + } catch (e) { + exception = e; + } + if (undefined !== exception) { + if (!c.fails || !(exception instanceof UserError)) { + throw exception; + } + return; // RETURN + } + assert.equal(c.fails || false, false); const changes = {}; for (let name in result.changes) { const change = result.changes[name]; @@ -378,7 +387,7 @@ x=U:C3-2 s=Sa:a;Ct-2 s;Bt=t;Bmaster=3`, yield repo.getBlob(NodeGit.Oid.fromString(entry.id)); return new ConflictEntry(entry.mode, data.toString()); }); - assert.deepEqual(simpleChanges, c.simpleChanges || {}); + assert.deepEqual(simpleChanges, c.simpleChanges || {}, "simple"); const conflicts = {}; for (let name in result.conflicts) { @@ -721,15 +730,6 @@ a `, errorMessage: `\ Submodule ${colors.red("s")} is conflicted. -`, - }, - "type conflict": { - input: ` -a=B:Ca-1;Ba=a| -x=U:C3-2 s=foo;C8-2 s=Sa:a;Bmaster=3;Bfoo=8`, - expected: `x=E:I *s=S:1*foo*S:a;W s=foo`, - errorMessage: `\ -Conflicting entries for submodule ${colors.red("s")} `, }, }; diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index 1455db274..b8930b05f 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -468,6 +468,18 @@ x=U:C3-2 t=Sa:1;C4-2 s;Bmaster=4;Bfoo=3`, fromCommit: "3", expected: `x=E:Cx-4,3 t=Sa:1;Bmaster=x`, }, + "change with multiple merge bases": { + initial: ` +a=B:Ca-1;Ba=a| +x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; + C3-2 s=Sa:a; + C4-2 t=Sa:a; + Cl-3,4 s,t; + Ct-3,4 a=Sa:1,t=Sa:a; + Bmaster=l;Bfoo=t`, + fromCommit: "t", + expected: "x=E:Cx-l,t a=Sa:1;Bmaster=x", + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From 13f4e046e3721d7e28d8606de86119c7c8fde8b0 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 21 May 2018 13:20:11 -0400 Subject: [PATCH 152/402] wording fixes --- node/lib/util/commit.js | 4 ++-- node/lib/util/repo_status.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 32e8c7982..079d92385 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -809,7 +809,7 @@ exports.getSubmoduleAmendStatus = co.wrap(function *(status, indexSha = commitSha; - // The read in the staged/workdir changes based on the difference in + // Then read in the staged/workdir changes based on the difference in // this submodule's open repo and the prior commit. repo = yield getRepo(); @@ -929,7 +929,7 @@ exports.getAmendStatus = co.wrap(function *(repo, options) { if (null === change.newSha) { return; // RETURN } - // This submodule was affected by the commit; record it's old sha + // This submodule was affected by the commit; record its old sha // if it wasn't added. if (null !== change.oldSha) { diff --git a/node/lib/util/repo_status.js b/node/lib/util/repo_status.js index 22bb1f23b..4abd820a6 100644 --- a/node/lib/util/repo_status.js +++ b/node/lib/util/repo_status.js @@ -365,8 +365,8 @@ class Submodule { } /** - * Return true if this submodule can be committed, either being not new, or - * having some staged changes or commits. + * Return true if this submodule can be committed, because it is both not + * new and has some staged changes or commits. * * @return {Boolean} */ From 4419469ff73f282a02bd6d02bcf10700e4f9c4df Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 21 May 2018 14:49:11 -0400 Subject: [PATCH 153/402] handle merges in amend --- node/lib/util/commit.js | 97 +++++++++++++++++++++++++++++++--------- node/test/util/commit.js | 90 +++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 20 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 079d92385..ab0cbe2ff 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -58,6 +58,8 @@ const SubmoduleUtil = require("./submodule_util"); const TreeUtil = require("./tree_util"); const UserError = require("./user_error"); +const getSubmodulesFromCommit = SubmoduleConfigUtil.getSubmodulesFromCommit; + /** * If the specified `message` does not end with '\n', return the result of * appending '\n' to 'message'; otherwise, return 'message'. @@ -841,6 +843,62 @@ exports.getSubmoduleAmendStatus = co.wrap(function *(status, }; }); +/* Read the state of the commits in the commit before the one to be + * amended, so that we can see what's been changed. + */ + +const computeAmendSubmoduleChanges = co.wrap(function*(repo, head) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(head, NodeGit.Commit); + + const headTree = yield head.getTree(); + const oldUrls = {}; + const parents = yield head.getParents(); + + // `merged` records changes that were present in at least one parent + const merged = {}; + // `changes` records what changes were actually made in this + // commit (as opposed to merged from a parent). + let changes = null; + if (parents.length === 0) { + changes = {}; + } + + for (const parent of parents) { + const parentTree = yield parent.getTree(); + const parentSubmodules = yield getSubmodulesFromCommit(repo, parent); + // TODO: this doesn't really handle URL updates + Object.assign(oldUrls, parentSubmodules); + const diff = + yield NodeGit.Diff.treeToTree(repo, parentTree, headTree, null); + const ourChanges = yield SubmoduleUtil.getSubmoduleChangesFromDiff( + diff, + true); + if (changes === null) { + changes = ourChanges; + } else { + for (const path of Object.keys(changes)) { + if (ourChanges[path] === undefined) { + merged[path] = changes[path]; + delete changes[path]; + } else { + delete ourChanges[path]; + } + } + for (const path of Object.keys(ourChanges)) { + merged[path] = ourChanges[path]; + delete changes[path]; + } + } + } + + return { + changes: changes, + merged: merged, + oldUrls: oldUrls + }; +}); + /** * Return the status object describing an amend commit to be created in the * specified `repo` and a map containing the submodules to have amend @@ -887,26 +945,14 @@ exports.getAmendStatus = co.wrap(function *(repo, options) { }); const head = yield repo.getHeadCommit(); - const headTree = yield head.getTree(); - const newUrls = - yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, head); + const newUrls = yield getSubmodulesFromCommit(repo, head); - // Read the state of the commits in the commit before the one to be - // amended. + const amendChanges = yield computeAmendSubmoduleChanges(repo, head); + const changes = amendChanges.changes; + const merged = amendChanges.merged; + const oldUrls = amendChanges.oldUrls; - let oldUrls = {}; - const parent = yield GitUtil.getParentCommit(repo, head); - let parentTree = null; - if (null !== parent) { - parentTree = yield parent.getTree(); - oldUrls = - yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, parent); - } - const diff = - yield NodeGit.Diff.treeToTree(repo, parentTree, headTree, null); - const changes = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff, - true); const submodules = baseStatus.submodules; // holds resulting sub statuses const opener = new Open.Opener(repo, null); @@ -922,7 +968,15 @@ exports.getAmendStatus = co.wrap(function *(repo, options) { const change = changes[name]; let currentSub = submodules[name]; let old = null; - if (undefined !== change) { + const isMerged = name in merged; + // TODO: This is pretty complicated -- it might be simpler to + // figure out if a commit is present in any ancestor. + if (isMerged) { + // In this case, the "old" version is actually the merged + // version + old = new Submodule(oldUrls[name], merged[name].newSha); + } + else if (undefined !== change) { // We handle deleted submodules later. TODO: this should not be a // special case when we've done the refactoring noted below. @@ -971,9 +1025,12 @@ exports.getAmendStatus = co.wrap(function *(repo, options) { old, getRepo, all); - // If no status was returned, remove this submodule. - if (null === result.status) { + // If no status was returned, or this was a merged + // submodule with no local changes, remove this submodule. + if (null === result.status || + (isMerged && result.status.isWorkdirClean() && + result.status.isIndexClean())) { delete submodules[name]; } else { diff --git a/node/test/util/commit.js b/node/test/util/commit.js index ce67ceb50..43f8da7c0 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -1284,6 +1284,96 @@ a=B:Chi#a-1;Ba=a|x=U:C3-2 s=Sa:a;Bmaster=3;Os W README.md=888`, }, }, }, + "no-op amend of a merge": { + state: `a=B:Chi#a-1;Cbranch2#b-1;Ba=a;Bb=b| + x=U:C3-1 s2=Sa:b;Cm-2,3 s2=Sa:b;Bmaster=m;Os`, + expected: { + status: new RepoStatus({ + currentBranchName: "master", + headCommit: "m", + submodules: {}, + }), + }, + toAmend: {} + }, + "amend of a merge (left)": { + state: `a=B:Chi#a-1;Cbranch2#b-1;Ba=a;Bb=b| + x=U:C3-1 s2=Sa:b;Cm-2,3 s2=Sa:b;Bmaster=m;Os I a=b`, + all: true, + expected: { + status: new RepoStatus({ + currentBranchName: "master", + headCommit: "m", + submodules: { + s : new Submodule({ + commit: new Submodule.Commit("1", "a"), + index: new Submodule.Index("1", "a", SAME), + workdir: new Submodule.Workdir(new RepoStatus({ + headCommit: "1", + staged: { + a: FILESTATUS.ADDED, + }, + }), SAME), + }), + }, + }), + }, + }, + "amend of a merge (right)": { + state: `a=B:Chi#a-1;Cbranch2#b-1;Ba=a;Bb=b| + x=U:C3-1 s2=Sa:b;Cm-2,3 s2=Sa:b;Bmaster=m;Os2 I a=b`, + all: true, + expected: { + status: new RepoStatus({ + currentBranchName: "master", + headCommit: "m", + submodules: { + s2 : new Submodule({ + commit: new Submodule.Commit("b", "a"), + index: new Submodule.Index("b", "a", SAME), + workdir: new Submodule.Workdir(new RepoStatus({ + headCommit: "b", + staged: { + a: FILESTATUS.ADDED, + }, + }), SAME), + }), + }, + }), + }, + }, + "no-op amend of a merge (changed in merge)": { + state: `a=B:Chi#a-1;Cbranch2#b-1;Cthird#c-a;Ba=a;Bb=b;Bc=c| + x=U:C3-1 s2=Sa:b;Cm-2,3 s2=Sa:c;Bmaster=m`, + all: true, + expected: { + status: new RepoStatus({ + currentBranchName: "master", + headCommit: "m", + submodules: { + s2 : new Submodule({ + index: new Submodule.Index("c", "a", null), + }), + }, + }), + }, + }, + "actual amend of a merge (changed in merge)": { + state: `a=B:Chi#a-1;Cbranch2#b-1;Cc-a;Cd-a;Ba=a;Bb=b;Bc=c;Bd=d| + x=U:C3-1 s2=Sa:b;Cm-2,3 s2=Sa:c;Bmaster=m;I s2=Sa:d`, + all: true, + expected: { + status: new RepoStatus({ + currentBranchName: "master", + headCommit: "m", + submodules: { + s2 : new Submodule({ + index: new Submodule.Index("d", "a", null), + }), + }, + }), + }, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From 7cb0c7439a0e77fb811023da7063f433c0300475 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 30 May 2018 13:18:01 -0400 Subject: [PATCH 154/402] Fix push in a sparse repo This works around a bug in git where push attempts to cd into a worktree even though itn fact it could work juts fine in a bare repo. --- node/lib/util/git_util.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index d014c15a7..e4dfb8c69 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -384,10 +384,16 @@ exports.push = co.wrap(function *(repo, remote, source, target, force, quiet) { forceStr = "-f"; } + const environ = Object.assign({}, process.env); + // Hack: set a fake work-tree because the repo's core.worktree might + // be set to a directory that, due to sparseness, doesn't exist, and + // git push has a bug which requires it to have a worktree. + environ.GIT_WORK_TREE = repo.path(); + const execString = `\ git -C '${repo.path()}' push ${forceStr} ${remote} ${source}:${target}`; try { - const result = yield ChildProcess.exec(execString); + const result = yield ChildProcess.exec(execString, {env : environ}); if (result.error || !quiet) { if (result.stdout) { console.log(result.stdout); From 0baa53bd186f904c9efa1b16a81166f9b74bbefb Mon Sep 17 00:00:00 2001 From: Zokir Tiliaev Date: Tue, 5 Jun 2018 16:04:12 -0400 Subject: [PATCH 155/402] Remove duplicates from git meta open --- node/lib/cmd/open.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 44e5e194e..cff55559d 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -93,6 +93,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { const cwd = process.cwd(); const subs = yield SubmoduleUtil.getSubmoduleNames(repo); + args.path = Array.from(new Set(args.path)); const subsToOpen = yield SubmoduleUtil.resolveSubmoduleNames(workdir, cwd, subs, From ed9cfcf286c183e937ded78032d35dc0f932c016 Mon Sep 17 00:00:00 2001 From: Zokir Tiliaev Date: Tue, 5 Jun 2018 18:12:35 -0400 Subject: [PATCH 156/402] Show appropriate msg when local ref unknown --- node/lib/util/push.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 219e549fc..94ca0bcbd 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -223,6 +223,9 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { let remoteUrl = yield GitUtil.getUrlFromRemoteName(repo, remoteName); const annotatedCommit = yield GitUtil.resolveCommitish(repo, source); + if (annotatedCommit === null) { + throw new UserError(`No such ref: ${source}`); + } const sha = annotatedCommit.id(); const commit = yield repo.getCommit(sha); From fa8c392ce81c3f300d1442b3fa1ea298379c0973 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 8 Jun 2018 15:40:12 -0400 Subject: [PATCH 157/402] rename add refs to sync refs --- node/lib/cmd/submodule.js | 37 ++++++++++---------------------- node/lib/git-meta.js | 2 ++ node/lib/util/submodule_util.js | 4 ++-- node/test/util/submodule_util.js | 8 +++---- 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/node/lib/cmd/submodule.js b/node/lib/cmd/submodule.js index 831f9db27..375fb04eb 100644 --- a/node/lib/cmd/submodule.js +++ b/node/lib/cmd/submodule.js @@ -277,31 +277,6 @@ meta-repo.`); } }); -const doAddRefs = co.wrap(function *(paths) { - const NodeGit = require("nodegit"); - - const GitUtil = require("../util/git_util"); - const SubmoduleUtil = require("../util/submodule_util"); - - const repo = yield GitUtil.getCurrentRepo(); - let subs = yield SubmoduleUtil.listOpenSubmodules(repo); - - if (0 !== paths.length) { - const workdir = repo.workdir(); - const cwd = process.cwd(); - const names = yield SubmoduleUtil.getSubmoduleNames(repo); - - const includedSubs = yield SubmoduleUtil.resolveSubmoduleNames(workdir, - cwd, - names, - paths); - const includedSet = new Set(includedSubs); - subs = subs.filter(name => includedSet.has(name)); - } - const refs = yield repo.getReferenceNames(NodeGit.Reference.TYPE.LISTALL); - yield SubmoduleUtil.addRefs(repo, refs, subs); -}); - /** * Execute the `submodule` command according to the specified `args`. * @@ -316,7 +291,17 @@ exports.executeableSubcommand = function (args) { return doStatusCommand(args.path, args.verbose); } else if ("addrefs" === args.command) { - return doAddRefs(args.path); + const SyncRefs = require("../util/syncrefs.js"); + console.error( + "This command is deprecated -- use git meta sync-refs instead"); + if (args.path.length !== 0) { + console.error(`\ +Also, the paths argument is deprecated. Exiting with no change.`); + /*jshint noyield:true*/ + const fail = co.wrap(function*() {return 1;}); + return fail(); + } + return SyncRefs.doSyncRefs(); } return doFindCommand(args.path, args.meta_committish, diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js index 0ab2f2ec0..f69ec8fa3 100755 --- a/node/lib/git-meta.js +++ b/node/lib/git-meta.js @@ -58,6 +58,7 @@ const root = require("./cmd/root"); const commitShadow = require("./cmd/commit-shadow"); const stash = require("./cmd/stash"); const status = require("./cmd/status"); +const syncrefs = require("./cmd/syncrefs"); const submodule = require("./cmd/submodule"); const UserError = require("./util/user_error"); const version = require("./cmd/version"); @@ -142,6 +143,7 @@ const commands = { "stash": stash, "submodule": submodule, "status": status, + "sync-refs": syncrefs, "version": version, }; diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 7be396fa1..54dbbf703 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -686,7 +686,7 @@ exports.resolvePaths = function (paths, indexSubNames, openSubmodules) { * @param {String[]} refs * @param {String[]} submodules */ -exports.addRefs = co.wrap(function *(repo, refs, submodules) { +exports.syncRefs = co.wrap(function *(repo, refs, submodules) { assert.instanceOf(repo, NodeGit.Repository); assert.isArray(refs); assert.isArray(submodules); @@ -728,7 +728,7 @@ exports.addRefs = co.wrap(function *(repo, refs, submodules) { name, NodeGit.Oid.fromString(sha), 1, - "addRefs"); + "syncRefs"); } })); })); diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index ad99952fb..5ebce080d 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -939,7 +939,7 @@ describe("SubmoduleUtil", function () { }); }); }); - describe("addRefs", function () { + describe("syncRefs", function () { const cases = { "trivial": { input: "x=S", @@ -972,13 +972,13 @@ describe("SubmoduleUtil", function () { Object.keys(cases).forEach(caseName => { const c = cases[caseName]; it(caseName, co.wrap(function *() { - const addRefs = co.wrap(function *(repos) { + const syncRefs = co.wrap(function *(repos) { const repo = repos.x; - yield SubmoduleUtil.addRefs(repo, c.refs, c.subs); + yield SubmoduleUtil.syncRefs(repo, c.refs, c.subs); }); yield RepoASTTestUtil.testMultiRepoManipulator(c.input, c.expected, - addRefs, + syncRefs, c.fails); })); }); From 29ca6916d19f13c3d56899302ca5716f632f9a61 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 8 Jun 2018 16:47:11 -0400 Subject: [PATCH 158/402] oops --- node/lib/cmd/syncrefs.js | 71 +++++++++++++++++++++++++++++++++++++++ node/lib/util/syncrefs.js | 48 ++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 node/lib/cmd/syncrefs.js create mode 100644 node/lib/util/syncrefs.js diff --git a/node/lib/cmd/syncrefs.js b/node/lib/cmd/syncrefs.js new file mode 100644 index 000000000..85ca08134 --- /dev/null +++ b/node/lib/cmd/syncrefs.js @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const co = require("co"); + +/** + * This module contains methods for implementing the `sync-refs` command. + */ + +/** + * help text for the `sync-refs` command + * @property {String} + */ +exports.helpText = + `Create refs in submodules which correspond to the refs in the meta repo.`; + +/** + * description of the `sync-refs` command + * @property {String} + */ +exports.description =` +For each ref in the monorepo, this command creates a ref in each open submodule +(which exists as-of that meta ref) pointing to the corresponding submodule +commit. +`; + +exports.configureParser = function (parser) { + /* This space intentionally left blank */ + return parser; +}; + +/** + * Execute the `sync-refs` command + * + * @async + * @param {Object} args + */ +exports.executeableSubcommand = co.wrap(function *() { + const SyncRefs = require("../util/syncrefs"); + + return yield SyncRefs.doSyncRefs(); +}); diff --git a/node/lib/util/syncrefs.js b/node/lib/util/syncrefs.js new file mode 100644 index 000000000..249f48b39 --- /dev/null +++ b/node/lib/util/syncrefs.js @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2017, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const co = require("co"); +const NodeGit = require("nodegit"); + +const GitUtil = require("./git_util"); +const SubmoduleUtil = require("./submodule_util"); + +/** + * Create refs in submodules which correspond to the refs in the meta repo. + */ +exports.doSyncRefs = co.wrap(function *() { + const repo = yield GitUtil.getCurrentRepo(); + let subs = yield SubmoduleUtil.listOpenSubmodules(repo); + + const refs = yield repo.getReferenceNames(NodeGit.Reference.TYPE.LISTALL); + yield SubmoduleUtil.syncRefs(repo, refs, subs); +}); From a6827565a1ff4780babd6d319cd4238a0503a75d Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 6 Jun 2018 15:39:00 -0400 Subject: [PATCH 159/402] fix message formatting --- node/lib/util/rm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/lib/util/rm.js b/node/lib/util/rm.js index b358daf6d..d46916674 100644 --- a/node/lib/util/rm.js +++ b/node/lib/util/rm.js @@ -254,8 +254,8 @@ function checkAllPathsResolved(paths, resolved) { const nextResolved = allResolvedPaths[insertionPoint]; if (insertionPoint >= allResolvedPaths.length || !nextResolved.startsWith(stripped + "/")) { - throw new UserError(`pathspec '${spec}' did not match any \ - files`); + throw new UserError(`\ +pathspec '${spec}' did not match any files`); } } } From 02156e4d4dc3c04d617c2849625d035511a9e9f2 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 6 Jun 2018 15:47:10 -0400 Subject: [PATCH 160/402] add failOnUnprefixed to resolvePaths --- node/lib/util/rm.js | 2 ++ node/lib/util/submodule_util.js | 32 ++++++++++++++++++---- node/test/util/submodule_util.js | 47 +++++++++++++++++++++++++++++--- 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/node/lib/util/rm.js b/node/lib/util/rm.js index d46916674..5a3abea6e 100644 --- a/node/lib/util/rm.js +++ b/node/lib/util/rm.js @@ -345,6 +345,8 @@ exports.rmPaths = co.wrap(function *(repo, paths, options) { const openArray = yield SubmoduleUtil.listOpenSubmodules(repo); const openSubmoduleSet = new Set(openArray); + // TODO: consider changing resolvePaths to support root-level items + // items, which would enable removal of checkAllPathsResolved const resolved = yield SubmoduleUtil.resolvePaths(paths, rootLevelItems, openArray); diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 54dbbf703..7e19a9d41 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -621,22 +621,36 @@ No submodules found from ${colors.yellow(filename)}.`); * @param {String []} paths * @param {String []} indexSubNames * @param {String []} openSubmodules + * @param {Boolean} failOnUnprefixed * @return {Object} map from submodule name to array of paths */ -exports.resolvePaths = function (paths, indexSubNames, openSubmodules) { +exports.resolvePaths = function (paths, indexSubNames, openSubmodules, + failOnUnprefixed) { assert.isArray(paths); assert.isArray(indexSubNames); assert.isArray(openSubmodules); + if (failOnUnprefixed === undefined) { + failOnUnprefixed = false; + } else { + assert.isBoolean(failOnUnprefixed); + } const result = {}; // First, populate 'result' with all the subs that are completely - // contained. + // contained, and clean the relevant specs out of paths - paths.map(path => { + const remainingPaths = []; + const add = subName => result[subName] = []; + for (const path of paths) { const subs = exports.getSubmodulesInPath(path, indexSubNames); - subs.forEach(subName => result[subName] = []); - }); + if (subs.length > 0) { + subs.forEach(add); + } else { + remainingPaths.push(path); + } + } + paths = remainingPaths; // Now check to see which paths refer to a path inside a submodule. // Checking each file against the name of each open submodule has @@ -653,9 +667,11 @@ exports.resolvePaths = function (paths, indexSubNames, openSubmodules) { for (let i = 0; i < paths.length; ++i) { const filename = paths[i]; - for (let j = 0; j < subsToCheck.length; ++j) { + let found = false; + for (let j = 0; j < subsToCheck.length; ++j) { const subName = subsToCheck[j]; if (filename.startsWith(subName + "/")) { + found = true; const pathInSub = filename.slice(subName.length + 1, filename.length); const subPaths = result[subName]; @@ -667,6 +683,10 @@ exports.resolvePaths = function (paths, indexSubNames, openSubmodules) { } } } + if (!found && failOnUnprefixed) { + throw new UserError(`\ +pathspec '${filename}' did not match any files`); + } } return result; diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index 5ebce080d..d39fa85a3 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -922,6 +922,35 @@ describe("SubmoduleUtil", function () { open: ["s"], expected: {}, }, + "path not in sub (but has a slash)": { + subNames: ["s"], + paths: ["t/README.md"], + open: ["s"], + expected: {}, + }, + "path not in sub, fail": { + subNames: ["s"], + paths: ["README.md"], + open: ["s"], + failOnUnprefixed: true, + fails: true + }, + "path of sub": { + subNames: ["s"], + paths: ["s"], + open: ["s"], + failOnUnprefixed: true, + expected: { + "s": [] + } + }, + "path not in sub (but has a slash), fail": { + subNames: ["s"], + paths: ["t/README.md"], + open: ["s"], + failOnUnprefixed: true, + fails: true + }, "filename starts with subname but not in it": { subNames: ["s"], paths: ["sam"], @@ -932,10 +961,20 @@ describe("SubmoduleUtil", function () { Object.keys(cases).forEach(caseName => { const c = cases[caseName]; it(caseName, function () { - const result = SubmoduleUtil.resolvePaths(c.paths, - c.subNames, - c.open); - assert.deepEqual(result, c.expected); + let result; + let failed; + try { + result = SubmoduleUtil.resolvePaths(c.paths, + c.subNames, + c.open, + c.failOnUnprefixed); + } catch (e) { + failed = true; + } + assert.equal(failed, c.fails); + if (!failed) { + assert.deepEqual(result, c.expected); + } }); }); }); From 616aef5884c96f7085c73cb5ea8efa09afd00125 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 7 Jun 2018 17:19:23 -0400 Subject: [PATCH 161/402] allow createShadowCommit to create a stash from indexes --- node/lib/cmd/commit-shadow.js | 3 ++- node/lib/util/stash_util.js | 28 ++++++++++++++++++++++++---- node/test/util/stash_util.js | 3 ++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/node/lib/cmd/commit-shadow.js b/node/lib/cmd/commit-shadow.js index 5fb360251..136cdfe39 100644 --- a/node/lib/cmd/commit-shadow.js +++ b/node/lib/cmd/commit-shadow.js @@ -110,7 +110,8 @@ exports.executeableSubcommand = co.wrap(function *(args) { args.message, incrementTimestamp, false, - args.include_untracked); + args.include_untracked, + false); if (null === result) { const head = yield repo.getHeadCommit(); console.log(head.id().tostrS()); diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index a6b12f806..d5c5172cc 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -499,12 +499,23 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, status, message, incrementTimestamp, - includeUntracked) { + includeUntracked, + indexOnly) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(status, RepoStatus); assert.isString(message); assert.isBoolean(includeUntracked); assert.isBoolean(incrementTimestamp); + assert.isBoolean(indexOnly); + + if (indexOnly) { + const index = yield repo.index(); + const tree = yield index.writeTree(); + const sig = repo.defaultSignature(); + const subCommit = yield repo.createCommit(null, sig, sig, + message, tree, []); + return subCommit.tostrS(); + } const changes = yield TreeUtil.listWorkdirChanges(repo, status, @@ -560,6 +571,7 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, * @param {Bool} useEpochTimestamp * @param {Bool} includeMeta * @param {Bool} includeUntracked + * @param {Bool} indexOnly include only staged changes * @return {Object|null} * @return {String} return.metaCommit * @return {Object} return.subCommits path to sha of generated subrepo commits @@ -568,12 +580,18 @@ exports.makeShadowCommit = co.wrap(function *(repo, message, useEpochTimestamp, includeMeta, - includeUntracked) { + includeUntracked, + indexOnly) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(message); assert.isBoolean(includeMeta); assert.isBoolean(includeUntracked); assert.isBoolean(useEpochTimestamp); + if (indexOnly === undefined) { + indexOnly = false; + } else { + assert.isBoolean(indexOnly); + } if (!message.endsWith("\n")) { message += "\n"; @@ -607,7 +625,8 @@ exports.makeShadowCommit = co.wrap(function *(repo, wd.status, message, useEpochTimestamp, - includeUntracked); + includeUntracked, + indexOnly); const newSubStat = new Submodule({ commit: subStatus.commit, index: subStatus.index, @@ -631,7 +650,8 @@ exports.makeShadowCommit = co.wrap(function *(repo, newStatus, message, useEpochTimestamp, - includeUntracked); + includeUntracked, + false); return { metaCommit: metaCommit, subCommits: subCommits, diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index 6adf88eb3..0084628e6 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -815,7 +815,8 @@ x=E:Cm-1 s=Sa:s;Bm=m;Os Cs foo=bar!Bs=s!W foo=bar` message, incrementTimestamp, meta, - includeUntracked); + includeUntracked, + false); const commitMap = {}; if (null !== result) { From aad4392a72b838f5ca2590e3b9101ed64c468b24 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 7 Jun 2018 17:19:38 -0400 Subject: [PATCH 162/402] checkout files --- node/lib/cmd/checkout.js | 70 +++++++++++++++++++++-- node/lib/util/checkout.js | 114 +++++++++++++++++++++++++++++++------ node/test/util/checkout.js | 96 ++++++++++++++++++++++++++++++- 3 files changed, 255 insertions(+), 25 deletions(-) diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index 4c773b681..b0a1d42f3 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -31,6 +31,7 @@ const co = require("co"); const Hook = require("../util/hook"); +const UserError = require("../../lib/util/user_error"); /** * This module contains the entrypoint for the `checkout` command. @@ -52,16 +53,22 @@ exports.configureParser = function (parser) { nargs: 1, }); - parser.addArgument(["committish"], { + parser.addArgument(["committish_or_file"], { type: "string", - help: ` -commit to check out. If this is not found, but does \ + help: `if this resolves to a commit, check out that commit. +If this is not found, but does \ match a single remote tracking branch, treat as equivalent to \ -'checkout -b -t /'`, +'checkout -b -t /'. Else, treat \ + as a file`, defaultValue: null, nargs: "?", }); + parser.addArgument(["files"], { + type: "string", + nargs: "*" + }); + parser.addArgument(["-t", "--track"], { help: "Set tracking branch.", action: "storeConst", @@ -76,6 +83,40 @@ match a single remote tracking branch, treat as equivalent to \ }); }; +/* +Argparse literally can't represent git's semantics here. Checkout +takes optional arguments [branch] and [files]. In git, anything after +"--" (hereinafter, "the separator") is part of "files". So if you say +"git checkout -- myfile", it'll assume that last argument is a file. + +There are two types of arguments that argparse recognizes: regular and +positional. Branch can't be a non-required regular argument, because +regular arguments must have names which start with "-". So it must be +a positional argument with nargs='?'. In argparse, the right side of +"--" is always positional arguments. And that means that the branch +will be read from the right side of the separator if it is not present +on the left side. + +(Incidentally, the same is true of Python's argparse, of which node's +argparse is a port.) + +Thanks, git, for using "--" for an utterly non-standard fashion. +*/ +function reanalyzeArgs(args) { + const separatorIndex = process.argv.indexOf("--"); + if (separatorIndex === -1) { + return; + } + + const countAfterSeparator = (process.argv.length - 1) - separatorIndex; + + if (args.files.length !== countAfterSeparator) { + const firstFile = args.committish_or_file; + args.committish_or_file = null; + args.files.splice(0, 0, firstFile); + } +} + /** * Execute the `checkout` command based on the supplied arguments. * @@ -84,6 +125,8 @@ match a single remote tracking branch, treat as equivalent to \ * @param {String} args.committish */ exports.executeableSubcommand = co.wrap(function *(args) { + reanalyzeArgs(args); + const colors = require("colors"); const Checkout = require("../util/checkout"); @@ -99,10 +142,25 @@ exports.executeableSubcommand = co.wrap(function *(args) { // Validate and determine what operation we're actually doing. + let committish = args.committish_or_file; + let files = args.files; + + if (files.length > 0 && newBranch) { + throw new UserError(`Cannot update paths and switch to branch + '${newBranch}' at the same time.`); + } + const op = yield Checkout.deriveCheckoutOperation(repo, - args.committish, + committish, newBranch, - args.track || false); + args.track || false, + files); + + // If we're going to check out files, just do that + if (Object.keys(op.resolvedPaths).length !== 0) { + yield Checkout.checkoutFiles(repo, op.commit, op.resolvedPaths); + return; + } // If we're already on this branch, note it and exit. diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 7bb06f5d6..47a6b031e 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -44,6 +44,7 @@ const Reset = require("./reset"); const RepoStatus = require("./repo_status"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const StatusUtil = require("./status_util"); +const StashUtil = require("./stash_util"); const SubmoduleFetcher = require("./submodule_fetcher"); const SubmoduleUtil = require("./submodule_util"); const UserError = require("./user_error"); @@ -274,6 +275,17 @@ ${colors.yellow(remoteName)}`); }; }); +const makeShadowCommitFromIndex = co.wrap(function *(repo) { + const committish = yield StashUtil.makeShadowCommit( + repo, "index", true, true, false, true); + if (committish === null) { + const head = yield repo.head(); + return yield NodeGit.Commit.lookup(repo, head.target()); + } else { + return committish; + } +}); + /** * Return an object describing what operation to perform in the specified * `repo` based on the optionally specified `committish`, the optionally @@ -287,6 +299,7 @@ ${colors.yellow(remoteName)}`); * @param {String|null} committish * @param {String|null} newBranch * @param {Boolean} track + * @param {Array|null} files * @return {Object} * @return {NodeGit.Commit} return.commit to check out * @return {Object|null} return.newBranch to create @@ -295,11 +308,13 @@ ${colors.yellow(remoteName)}`); * @return {String|null} return.newBranch.tracking.remoteName * @return {String} return.newBranch.tracking.branchName * @return {String|null} return.switchBranch to make current + * @return {Array} return.resolvedPaths to check out */ exports.deriveCheckoutOperation = co.wrap(function *(repo, committish, newBranch, - track) { + track, + files) { assert.instanceOf(repo, NodeGit.Repository); if (null !== committish) { assert.isString(committish); @@ -307,12 +322,18 @@ exports.deriveCheckoutOperation = co.wrap(function *(repo, if (null !== newBranch) { assert.isString(newBranch); } + if (null === files || undefined === files) { + files = []; + } else { + assert.isArray(files); + } assert.isBoolean(track); const result = { commit: null, newBranch: null, switchBranch: null, + resolvedPaths: null }; const ensureBranchDoesntExist = co.wrap(function *(name) { @@ -355,15 +376,15 @@ A branch named ${colors.red(name)} already exists.`); if (null !== committish) { // Now, we have a committish to resolve. - const annotated = yield GitUtil.resolveCommitish(repo, committish); + let annotated = yield GitUtil.resolveCommitish(repo, committish); if (null === annotated) { - // If we are not explicitly setting up a tracking branch and are - // not explicitly createing a new branch, we may implicitly do both + // If we are not explicitly setting up a tracking branch nor + // explicitly creating a new branch, we may implicitly do both // when `committish` is not directly resolveable, but does match a // single remote tracking branch. - if (null === newBranch) { + if (null === newBranch && files.length === 0) { const remote = yield exports.findTrackingBranch(repo, committish); if (null !== remote) { @@ -382,18 +403,27 @@ A branch named ${colors.red(name)} already exists.`); }, }; result.switchBranch = committish; + if (null === result.commit) { + throw new UserError(`\ +Could not resolve ${colors.red(committish)} as a branch or commit.`); + } } } - - // If we didn't resolve anything from `committish`, throw an error. - - if (null === result.commit) { - throw new UserError( - `Could not resolve ${colors.red(committish)} as a branch or commit.`); + if (null === annotated) { + // If we didn't resolve anything from `committish`, try it + // as a file. + files.splice(0, 0, committish); + // We would now like to resolve from the index, but + // libgit2 doesn't support the ":0" shorthand, and our + // remaining code all assumes an actual commit, so + // we're just going to make one. The cache-tree ought to + // make this relatively cheap. + + annotated = yield makeShadowCommitFromIndex(repo); } + result.commit = annotated; } else { - const commit = yield repo.getCommit(annotated.id()); result.commit = commit; @@ -415,15 +445,32 @@ A branch named ${colors.red(name)} already exists.`); } } else { - // If we're implicitly using HEAD, see if it's on a branch and record - // that branch's name. - - const head = yield repo.head(); - if (head.isBranch()) { - committishBranch = head; + if (files.length !== 0) { + //When checking out files, we're implicitly using the index + result.commit = yield makeShadowCommitFromIndex(repo); + } else { + // If we're implicitly using HEAD, see if it's on a branch + // and record that branch's name. + + const head = yield repo.head(); + if (head.isBranch()) { + committishBranch = head; + } } } + if (files.length !== 0) { + const indexSubNames = yield SubmoduleUtil.getSubmoduleNames( + repo); + const openSubmodules = yield SubmoduleUtil.listOpenSubmodules( + repo); + result.resolvedPaths = SubmoduleUtil.resolvePaths( + files, indexSubNames, openSubmodules); + + return result; + } + + if (null !== newBranch) { // If we have a `newBranch`, we need to make sure it doesn't already // exist. @@ -541,3 +588,34 @@ exports.executeCheckout = co.wrap(function *(repo, yield repo.setHead(`refs/heads/${switchBranch}`); } }); + + +/** + * Checkout files in submodules. + * + * @param {NodeGit.repository} repo + * @param {NodeGit.Commit} commit + * @param {Object} resolvedPaths -- keys are submodules, values are + * files + */ +exports.checkoutFiles = co.wrap(function*(repo, commit, resolvedPaths) { + + const getShas = SubmoduleUtil.getSubmoduleShasForCommit; + const subCommits = yield getShas(repo, Object.keys(resolvedPaths), commit); + const checkoutSub = co.wrap(function*(subName) { + const subRepo = yield SubmoduleUtil.getRepo(repo, subName); + const subCommit = subCommits[subName]; + + const paths = resolvedPaths[subName]; + const resolvedSubCommit = yield NodeGit.Commit.lookup(subRepo, + subCommit); + const opts = new NodeGit.CheckoutOptions(); + opts.checkoutStrategy = NodeGit.Checkout.STRATEGY.FORCE; + if (paths.length > 0) { + opts.paths = paths; + } + yield NodeGit.Checkout.tree(subRepo, resolvedSubCommit, opts); + }); + + yield DoWorkQueue.doInParallel(Object.keys(resolvedPaths), checkoutSub); +}); diff --git a/node/test/util/checkout.js b/node/test/util/checkout.js index 574157643..681187e00 100644 --- a/node/test/util/checkout.js +++ b/node/test/util/checkout.js @@ -32,10 +32,13 @@ const assert = require("chai").assert; const co = require("co"); +const NodeGit = require("nodegit"); const Checkout = require("../../lib/util/checkout"); const GitUtil = require("../../lib/util/git_util"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const StashUtil = require("../../lib/util/stash_util"); +const SubmoduleUtil = require("../../lib/util/submodule_util"); const UserError = require("../../lib/util/user_error"); describe("Checkout", function () { @@ -315,7 +318,11 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, state: "x=S", committish: "bar", track: false, - fails: true, + expectedSha: "1", // here, there are no index changes, so the + // shadow-commit will be a no-op + expectedFiles: "bar", + expectedNewBranch: null, + expectedSwitchBranch: null, }, "commit, no new branch, nameless": { state: "x=S", @@ -592,4 +599,91 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, })); }); }); + describe("checkoutFiles", function () { + const cases = { + //TODO: bad pathspecs + "index: one file": { + input: "a=S|x=S:I s=Sa:1;Os I foo=bar!W foo=baz", + paths: ["s/foo"], + commit: ":0", + expected: "x=S:I s=Sa:1;Os I foo=bar" + }, + "index: two files, one spec": { + input: `a=S| + x=S:I s=Sa:1;Os I foo=bar,foo2=bar2!W foo=baz,foo2=baz2`, + paths: ["s/foo"], + commit: ":0", + expected: "x=S:I s=Sa:1;Os I foo=bar,foo2=bar2!W foo2=baz2" + }, + "index: two files, two specs": { + input: `a=S| + x=S:I s=Sa:1;Os I foo=bar,foo2=bar2!W foo=baz,foo2=baz2`, + paths: ["s/foo", "s/foo2"], + commit: ":0", + expected: "x=S:I s=Sa:1;Os I foo=bar,foo2=bar2" + }, + "index: two files, wide spec": { + input: `a=S| + x=S:I s=Sa:1;Os I foo=bar,foo2=bar2!W foo=baz,foo2=baz2`, + paths: ["s"], + commit: ":0", + expected: "x=S:I s=Sa:1;Os I foo=bar,foo2=bar2" + }, + "index: two files, two submodules, two specs": { + input: `a=S|b=S:C2-1;Bmaster=2| + x=S:I a=Sa:1,b=Sb:2;Oa I foo=bar!W foo=baz; + Ob I foo=bar!W foo=baz`, + paths: ["a/foo", "b/foo"], + commit: ":0", + expected: `x=S:I a=Sa:1,b=Sb:2;Oa I foo=bar; + Ob I foo=bar` + }, + "some commit: one file": { + input: `a=S:C2-1 foo=c2;C3-2 foo=c3;Bmaster=3| + x=S:C4-1 a=Sa:2;C5-4 a=Sa:3;Bmaster=5;Oa`, + paths: ["a/foo"], + commit: "4", + expected: `x=S:C4-1 a=Sa:2;C5-4 a=Sa:3;Bmaster=5;Oa I foo=c2` + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const manipulator = co.wrap(function*(repos, maps) { + const repo = repos.x; + const index = yield SubmoduleUtil.getSubmoduleNames( + repo); + const open = yield SubmoduleUtil.listOpenSubmodules( + repo); + const resolvedFiles = SubmoduleUtil.resolvePaths(c.paths, + index, + open, + true); + let annotated; + if (c.commit === ":0") { + let shadow = yield StashUtil.makeShadowCommit( + repo, "index", true, true, false, true); + if (shadow === null) { + const head = yield repo.head(); + shadow = yield head.target(); + } + annotated = yield NodeGit.Commit.lookup(repo, + shadow.metaCommit); + assert (annotated !== null); + } else { + const mapped = maps.reverseCommitMap[c.commit]; + annotated = yield NodeGit.Commit.lookup(repo, mapped); + } + + yield Checkout.checkoutFiles(repo, annotated, + resolvedFiles); + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.input, + c.expected, + manipulator, + c.fails); + + })); + }); + }); }); From fd7c42fc0517fc929fda2c53a47dc325f00b32ff Mon Sep 17 00:00:00 2001 From: Zokir Tiliaev Date: Thu, 21 Jun 2018 10:58:36 -0400 Subject: [PATCH 163/402] Added submodule unique check in open/close after path resolution --- node/lib/cmd/open.js | 9 +++++---- node/lib/util/close_util.js | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index cff55559d..d43aac589 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -94,10 +94,11 @@ exports.executeableSubcommand = co.wrap(function *(args) { const subs = yield SubmoduleUtil.getSubmoduleNames(repo); args.path = Array.from(new Set(args.path)); - const subsToOpen = yield SubmoduleUtil.resolveSubmoduleNames(workdir, - cwd, - subs, - args.path); + let subsToOpen = yield SubmoduleUtil.resolveSubmoduleNames(workdir, + cwd, + subs, + args.path); + subsToOpen = Array.from(new Set(subsToOpen)); const index = yield repo.index(); const shas = yield SubmoduleUtil.getCurrentSubmoduleShas(index, subsToOpen); diff --git a/node/lib/util/close_util.js b/node/lib/util/close_util.js index 350937976..e41002143 100644 --- a/node/lib/util/close_util.js +++ b/node/lib/util/close_util.js @@ -63,10 +63,11 @@ exports.close = co.wrap(function *(repo, cwd, paths, force) { const workdir = repo.workdir(); const subs = yield SubmoduleUtil.getSubmoduleNames(repo); - const subsToClose = yield SubmoduleUtil.resolveSubmoduleNames(workdir, - cwd, - subs, - paths); + let subsToClose = yield SubmoduleUtil.resolveSubmoduleNames(workdir, + cwd, + subs, + paths); + subsToClose = Array.from(new Set(subsToClose)); const repoStatus = yield StatusUtil.getRepoStatus(repo, { paths: subsToClose, From 1cab620f8e60208f85c3f8562f9e7da76181ebbd Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 20 Jun 2018 00:17:19 -0400 Subject: [PATCH 164/402] Work around libgit2 merging bug --- node/lib/util/cherry_pick_util.js | 65 +++++++++++++++++++++++++++++- node/lib/util/git_util.js | 18 +++++++++ node/test/util/cherry_pick_util.js | 23 +++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 9d2b7c597..3c94171a9 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -58,6 +58,7 @@ const UserError = require("./user_error"); const CommitAndRef = SequencerState.CommitAndRef; const CHERRY_PICK = SequencerState.TYPE.CHERRY_PICK; +const STAGE = RepoStatus.STAGE; /** * Throw a `UserError` if the specfied `seq` is null or does not indicate a @@ -177,6 +178,60 @@ exports.containsUrlChanges = co.wrap(function *(repo, commit, baseCommit) { return false; }); +const populateLibgit2MergeBugData = co.wrap(function*(repo, data) { + if (data.mergeBases === undefined) { + const head = data.head; + const targetCommit = data.targetCommit; + const mergeBases = yield GitUtil.mergeBases(repo, head, targetCommit); + data.mergeBases = []; + for (const base of mergeBases) { + const commit = yield NodeGit.Commit.lookup(repo, base); + data.mergeBases.push(commit); + } + } + + return data.mergeBases; +}); + +const workAroundLibgit2MergeBug = co.wrap(function *(data, repo, name, + entries) { + let ancestor = entries[STAGE.ANCESTOR]; + const ours = entries[STAGE.OURS]; + const theirs = entries[STAGE.THEIRS]; + if (undefined === ancestor && + undefined !== ours && + undefined !== theirs) { + // This might be a normal conflict that libgit2 is falsely + // telling us is an add-add conflict. I don't yet have a + // libgit2 bug report for this because the only repro is a + // complex case in Two Sigma's proprietary monorepo. + + // We work around this by looking at all merge-bases, and checking + // if any of them have an entry for this name, and if so, filling + // in the ancestor with it + const mergeBases = yield populateLibgit2MergeBugData(repo, data); + for (const base of mergeBases) { + const shas = yield SubmoduleUtil.getSubmoduleShasForCommit( + repo, [ours.path], base); + if (shas[name] !== undefined) { + + ancestor = new NodeGit.IndexEntry(); + ancestor.id = NodeGit.Oid.fromString(shas[name]); + ancestor.mode = NodeGit.TreeEntry.FILEMODE.COMMIT; + ancestor.path = ours.path; + ancestor.flags = ours.flags; + ancestor.gid = ours.gid; + ancestor.uid = ours.uid; + ancestor.fileSize = 0; + ancestor.ino = 0; + ancestor.dev = 0; + entries[STAGE.ANCESTOR] = ancestor; + break; + } + } + } +}); + /** * Determine how to apply the submodule changes introduced in the * specified `targetCommit` to the commit on the head of the specified `repo` @@ -202,6 +257,7 @@ exports.computeChanges = co.wrap(function *(repo, index, targetCommit) { assert.instanceOf(index, NodeGit.Index); assert.instanceOf(targetCommit, NodeGit.Commit); + const head = yield repo.getHeadCommit(); const conflicts = {}; const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit( repo, @@ -211,7 +267,6 @@ exports.computeChanges = co.wrap(function *(repo, index, targetCommit) { const conflictEntries = new Map(); // name -> normal, ours, theirs const entries = index.entries(); - const STAGE = RepoStatus.STAGE; for (const entry of entries) { const name = entry.path; const stage = NodeGit.Index.entryStage(entry); @@ -240,7 +295,14 @@ exports.computeChanges = co.wrap(function *(repo, index, targetCommit) { } return new ConflictEntry(entry.mode, entry.id.tostrS()); } + + const libgit2MergeBugData = { + head: head, + targetCommit: targetCommit + }; for (const [name, entries] of conflictEntries) { + yield workAroundLibgit2MergeBug(libgit2MergeBugData, repo, name, + entries); const ancestor = entries[STAGE.ANCESTOR]; const ours = entries[STAGE.OURS]; const theirs = entries[STAGE.THEIRS]; @@ -268,7 +330,6 @@ exports.computeChanges = co.wrap(function *(repo, index, targetCommit) { const simpleChanges = {}; const treeId = yield index.writeTreeTo(repo); const tree = yield NodeGit.Tree.lookup(repo, treeId); - const head = yield repo.getHeadCommit(); const headTree = yield head.getTree(); const diff = yield NodeGit.Diff.treeToTree(repo, headTree, tree, null); const treeChanges = diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index e4dfb8c69..61be7fc4d 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -1037,3 +1037,21 @@ exports.hashObject = co.wrap(function *(repo, data) { const res = yield db.write(data, data.length, BLOB); return res; }); + +/** + * You would like to use NodeGit.Merge.bases, but unfortunately, + * https://github.com/nodegit/nodegit/issues/1231 + * @async + */ +exports.mergeBases = co.wrap(function *(repo, commit1, commit2) { + const id1 = commit1.id().tostrS(); + const id2 = commit2.id().tostrS(); + const execString = `\ +git -C '${repo.path()}' merge-base ${id1} ${id2}`; + const result = yield ChildProcess.exec(execString); + if (result.error) { + throw new UserError("Couldn't run git merge-base: " + + result.stderr); + } + return result.stdout.split("\n").filter(x => x); +}); diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index eb84ebeec..d7faba1ea 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -400,7 +400,30 @@ x=U:C3-2 s=Sa:a;Ct-2 s;Bt=t;Bmaster=3`, assert.deepEqual(conflicts, c.conflicts || {}); })); }); + it("works around a libgit2 bug", co.wrap(function*() { + const w = yield RepoASTTestUtil.createMultiRepos(` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=S:C2-1 s=Sa:1;C3-2 s=Sa:a;Ct-2 s=Sa:b;Bmaster=3;Bt=t` + ); + const repos = w.repos; + const repo = repos.x; + const reverseCommitMap = w.reverseCommitMap; + const head = yield repo.getHeadCommit(); + const target = yield repo.getCommit(reverseCommitMap.t); + const index = + yield NodeGit.Cherrypick.commit(repo, target, head, 0, []); + yield index.remove("s", 1); + const result = yield CherryPickUtil.computeChanges(repo, + index, + target); + + const change = result.changes.s; + const expect = new SubmoduleChange(reverseCommitMap["1"], + reverseCommitMap.b); + assert.deepEqual(expect, change); + })); }); + describe("pickSubs", function () { // Most of the logic is done via `RebaseUtil.rewriteCommits`. We need to // validate that we invoke that method correctly and that we fetch commits From 58b5bdf94bcb21374e13b38df66f04de639eca1d Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 22 Jun 2018 10:05:19 -0400 Subject: [PATCH 165/402] Handle a corrupt repository in the synth ref checker --- node/lib/util/synthetic_branch_util.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 93dc55d70..9231cf381 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -185,7 +185,15 @@ function* checkSubmodules(repo, commit) { const result = changeSet.map(function *(path) { const entry = yield commit.getEntry(path); const submodulePath = entry.path(); - const url = submodules[submodulePath].url; + const submodule = submodules[submodulePath]; + if (!submodule) { + console.error( + "A submodule exists in the tree but not the .gitmodules."); + console.error( + `The commit ${commit.id().tostrS()} is corrupt`); + return false; + } + const url = submodule.url; return yield *checkSubmodule(repo, cfg, commit, entry, url); }); return (yield result).every(identity); From e5cb773c516278cd6783917221f19b4a151c7b64 Mon Sep 17 00:00:00 2001 From: Zokir Tiliaev Date: Wed, 27 Jun 2018 17:45:19 -0400 Subject: [PATCH 166/402] fixed issue #615, add by path --- node/lib/util/status_util.js | 15 +++++++++++---- node/test/util/add.js | 13 +++++++++++++ node/test/util/status_util.js | 4 ++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index f3595965d..79032c864 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -296,13 +296,18 @@ exports.getSubmoduleStatus = co.wrap(function *(repo, }); /** - * Return the conflicts listed in the specified `index`. + * Return the conflicts listed in the specified `index` and + * the specified `paths`. You can specify an empty Array if + * you want to list conflicts for the whole index. * * @param {NodeGit.Index} index + * @param {String []} paths * @return {Object} map from entry name to `RepoStatus.Conflict` */ -exports.readConflicts = function (index) { +exports.readConflicts = function (index, paths) { assert.instanceOf(index, NodeGit.Index); + assert.isArray(paths); + paths = new Set(paths); const conflicted = {}; function getConflict(path) { let obj = conflicted[path]; @@ -338,7 +343,9 @@ exports.readConflicts = function (index) { const COMMIT = NodeGit.TreeEntry.FILEMODE.COMMIT; for (let name in conflicted) { const c = conflicted[name]; - + if (paths.size !== 0 && !paths.has(name)) { + continue; + } // Ignore the conflict if it's just between submodule SHAs if (COMMIT !== c.our || COMMIT !== c.their) { @@ -478,7 +485,7 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { const openSet = new Set(openArray); const index = yield repo.index(); - Object.assign(args.staged, exports.readConflicts(index)); + Object.assign(args.staged, exports.readConflicts(index, options.paths)); const headTree = yield headCommit.getTree(); const diff = yield NodeGit.Diff.treeToIndex(repo, headTree, index); diff --git a/node/test/util/add.js b/node/test/util/add.js index f3b906a8c..8985581aa 100644 --- a/node/test/util/add.js +++ b/node/test/util/add.js @@ -156,6 +156,19 @@ a=B|x=S:C2-1 a/b=Sa:1;Oa/b W x/y/z=a,x/r/z=b;Bmaster=2`, paths: ["s"], expected: "x=E:Os I README.md=foo", }, + "sub with conflict multiple files": { + initial: `a=B|x=U:Os I *README.md=a*b*c, + *lol.txt=a*b*c!W README.md=foo,lol.txt=bar`, + paths: ["s"], + expected: "x=E:Os I README.md=foo, lol.txt=bar", + }, + "sub with conflict multiple by path": { + initial: `a=B|x=U:Os I *README.md=a*b*c, + *lol.txt=a*b*c!W README.md=foo,lol.txt=bar`, + paths: ["s/README.md"], + expected: `x=E:Os I README.md=foo, + *lol.txt=a*b*c!W lol.txt=bar`, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; diff --git a/node/test/util/status_util.js b/node/test/util/status_util.js index 8125fd61c..f12fec2ba 100644 --- a/node/test/util/status_util.js +++ b/node/test/util/status_util.js @@ -58,7 +58,7 @@ describe("StatusUtil", function () { const Commit = Submodule.Commit; const Index = Submodule.Index; const Workdir = Submodule.Workdir; - + describe("remapSubmodule", function () { const cases = { "all": { @@ -548,7 +548,7 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, const written = yield RepoASTTestUtil.createRepo(c.state); const repo = written.repo; const index = yield repo.index(); - const result = StatusUtil.readConflicts(index); + const result = StatusUtil.readConflicts(index, []); assert.deepEqual(result, c.expected); })); }); From ad0893669590ca5bc06ad90845297e1aaebcf210 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 28 Jun 2018 14:18:05 -0400 Subject: [PATCH 167/402] update docstring --- node/lib/util/diff_util.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/node/lib/util/diff_util.js b/node/lib/util/diff_util.js index e89afa9cd..6192e148b 100644 --- a/node/lib/util/diff_util.js +++ b/node/lib/util/diff_util.js @@ -93,6 +93,9 @@ function readDiff(diff) { } /** + * Do not use this on the meta repo because it uses libgit2 operations + * with bad performance and without the ability to handle sparse checkouts. + * * Return differences for the specified `paths` in the specified `repo` between * the current index and working directory, and the specified `tree`, if * not null. If the specified `allUntracked` is true, include all untracked From 56afef4fad39449adf3a2e26b361b01c731286ef Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 28 Jun 2018 14:18:28 -0400 Subject: [PATCH 168/402] handle non-files checkout --- node/lib/cmd/checkout.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index b0a1d42f3..06b2b0f66 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -157,8 +157,9 @@ exports.executeableSubcommand = co.wrap(function *(args) { files); // If we're going to check out files, just do that - if (Object.keys(op.resolvedPaths).length !== 0) { - yield Checkout.checkoutFiles(repo, op.commit, op.resolvedPaths); + if (op.resolvedPaths !== null && + Object.keys(op.resolvedPaths).length !== 0) { + yield Checkout.checkoutFiles(repo, op); return; } From cc4f9ed92ea979e326959a8b736e2d8892228b3d Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 28 Jun 2018 14:43:48 -0400 Subject: [PATCH 169/402] spelling --- node/lib/util/git_util.js | 2 +- node/lib/util/status_util.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 61be7fc4d..aad97688d 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -787,7 +787,7 @@ exports.parseRefspec = function(str) { /** * Resolve the specified `filename` against the specified `cwd` and return the * relative value for that resulting path to the specified `workdir`. Throw a - * `UserError` if the path lies outsied `workdir` or does not refer to a file + * `UserError` if the path lies outside `workdir` or does not refer to a file * in `workdir`. Note that if `filename` resolves to `workdir`, the result is * `""`. * diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 79032c864..7cd352f54 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -363,7 +363,7 @@ exports.readConflicts = function (index, paths) { * (default []), list the status only of the files contained in `paths`. If * the optionally specified `options.showMetaChanges` is provided (default * true), return the status of changes in `repo`; otherwise, show only changes - * in submobules. If the optionally specified `ignoreIndex` is specified, + * in submodules. If the optionally specified `ignoreIndex` is specified, * calculate the status matching the workdir to the underlying commit rather * than against the index. If the specified `options.cwd` is provided, resolve * paths in the context of that directory. From 389d507a62c6966905c07f6b2021e88532cb4a3b Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 2 Jul 2018 09:42:57 -0400 Subject: [PATCH 170/402] fix checkout to not use slow broken libgit2 diff functions --- node/lib/util/checkout.js | 131 ++++++++++++++++++++++++++++--------- node/test/util/checkout.js | 21 +++--- 2 files changed, 109 insertions(+), 43 deletions(-) diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 47a6b031e..f5a7781d4 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -44,7 +44,6 @@ const Reset = require("./reset"); const RepoStatus = require("./repo_status"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const StatusUtil = require("./status_util"); -const StashUtil = require("./stash_util"); const SubmoduleFetcher = require("./submodule_fetcher"); const SubmoduleUtil = require("./submodule_util"); const UserError = require("./user_error"); @@ -275,17 +274,6 @@ ${colors.yellow(remoteName)}`); }; }); -const makeShadowCommitFromIndex = co.wrap(function *(repo) { - const committish = yield StashUtil.makeShadowCommit( - repo, "index", true, true, false, true); - if (committish === null) { - const head = yield repo.head(); - return yield NodeGit.Commit.lookup(repo, head.target()); - } else { - return committish; - } -}); - /** * Return an object describing what operation to perform in the specified * `repo` based on the optionally specified `committish`, the optionally @@ -333,7 +321,8 @@ exports.deriveCheckoutOperation = co.wrap(function *(repo, commit: null, newBranch: null, switchBranch: null, - resolvedPaths: null + resolvedPaths: null, + checkoutFromIndex: false }; const ensureBranchDoesntExist = co.wrap(function *(name) { @@ -419,9 +408,10 @@ Could not resolve ${colors.red(committish)} as a branch or commit.`); // we're just going to make one. The cache-tree ought to // make this relatively cheap. - annotated = yield makeShadowCommitFromIndex(repo); + result.checkoutFromIndex = true; + } else { + result.commit = annotated; } - result.commit = annotated; } else { const commit = yield repo.getCommit(annotated.id()); @@ -447,7 +437,7 @@ Could not resolve ${colors.red(committish)} as a branch or commit.`); else { if (files.length !== 0) { //When checking out files, we're implicitly using the index - result.commit = yield makeShadowCommitFromIndex(repo); + result.checkoutFromIndex = true; } else { // If we're implicitly using HEAD, see if it's on a branch // and record that branch's name. @@ -464,8 +454,16 @@ Could not resolve ${colors.red(committish)} as a branch or commit.`); repo); const openSubmodules = yield SubmoduleUtil.listOpenSubmodules( repo); + + const workdir = repo.workdir(); + const cwd = process.cwd(); + + const absfiles = files.map(filename => + GitUtil.resolveRelativePath(workdir, cwd, + filename)); + result.resolvedPaths = SubmoduleUtil.resolvePaths( - files, indexSubNames, openSubmodules); + absfiles, indexSubNames, openSubmodules); return result; } @@ -491,7 +489,7 @@ Cannot setup tracking information; starting point is not a branch.`); } // If the branch is remote, set up remote tracking information, - // otherwise leve the remote name 'null'; + // otherwise leave the remote name 'null'; if (committishBranch.isRemote()) { const parts = committishBranch.shorthand().split(/\/(.+)/); @@ -589,33 +587,106 @@ exports.executeCheckout = co.wrap(function *(repo, } }); +function noSuchFileMessage(subName, path) { + const fn = `${subName}/${path}`; + return `error: pathspec '${fn}' did not match any file(s) known to git.`; +} /** * Checkout files in submodules. * * @param {NodeGit.repository} repo - * @param {NodeGit.Commit} commit - * @param {Object} resolvedPaths -- keys are submodules, values are - * files + * @param {Object} options + * @param {Object} options.resolvedPaths -- keys are submodules, + * values are files + * @param {NodeGit.boolean} options.checkoutFromIndex Check out from the + index + * @param {NodeGit.Commit} options.commit If not null, checking out + * from a commit + * @param {NodeGit.Stage} options.stage If not null, index stage else 0 */ -exports.checkoutFiles = co.wrap(function*(repo, commit, resolvedPaths) { +exports.checkoutFiles = co.wrap(function*(repo, options) { + assert.instanceOf(repo, NodeGit.Repository); + const resolvedPaths = options.resolvedPaths; + const subNames = Object.keys(resolvedPaths); + + let subCommits; + let stage = 0; + if (undefined !== options.stage) { + assert(options.checkoutFromIndex); + stage = options.stage; + assert.isNumber(stage); + } - const getShas = SubmoduleUtil.getSubmoduleShasForCommit; - const subCommits = yield getShas(repo, Object.keys(resolvedPaths), commit); - const checkoutSub = co.wrap(function*(subName) { + if (options.commit) { + assert.instanceOf(options.commit, NodeGit.Commit); + assert(!options.checkoutFromIndex); + const getShas = SubmoduleUtil.getSubmoduleShasForCommit; + subCommits = yield getShas(repo, subNames, options.commit); + } else { + assert(options.checkoutFromIndex); + } + + const errors = []; + const submoduleInfo = {}; + const prepSub = co.wrap(function*(subName) { + const info = {}; const subRepo = yield SubmoduleUtil.getRepo(repo, subName); - const subCommit = subCommits[subName]; + info.repo = subRepo; + + const paths = resolvedPaths[subName]; + if (options.checkoutFromIndex) { + const index = yield subRepo.index(); + info.index = index; + // libgit2 doesn't care if requested paths don't exist, + // but we do. + for (const path of paths) { + if (undefined === index.getByPath(path, stage)) { + errors.push(noSuchFileMessage(subName, path)); + } + } + } else { + const subCommit = subCommits[subName]; + const resolvedSubCommit = yield NodeGit.Commit.lookup(subRepo, + subCommit); + info.resolvedSubCommit = resolvedSubCommit; + // libgit2 doesn't care if requested paths don't exist, + // but we do. + const treeId = resolvedSubCommit.treeId(); + const tree = yield NodeGit.Tree.lookup(subRepo, treeId); + + for (const path of paths) { + const entry = yield tree.entryByPath(path); + if (null === entry) { + errors.push(noSuchFileMessage(subName, path)); + } + } + } + submoduleInfo[subName] = info; + }); + + yield DoWorkQueue.doInParallel(subNames, prepSub); + if (errors.length !== 0) { + throw new UserError(errors.join("\n")); + } + + const checkoutSub = co.wrap(function*(subName) { + const info = submoduleInfo[subName]; + const subRepo = info.repo; const paths = resolvedPaths[subName]; - const resolvedSubCommit = yield NodeGit.Commit.lookup(subRepo, - subCommit); + const opts = new NodeGit.CheckoutOptions(); opts.checkoutStrategy = NodeGit.Checkout.STRATEGY.FORCE; if (paths.length > 0) { opts.paths = paths; } - yield NodeGit.Checkout.tree(subRepo, resolvedSubCommit, opts); + if (options.checkoutFromIndex) { + yield NodeGit.Checkout.index(subRepo, info.index, opts); + } else { + yield NodeGit.Checkout.tree(subRepo, info.resolvedSubCommit, opts); + } }); - yield DoWorkQueue.doInParallel(Object.keys(resolvedPaths), checkoutSub); + yield DoWorkQueue.doInParallel(subNames, checkoutSub); }); diff --git a/node/test/util/checkout.js b/node/test/util/checkout.js index 681187e00..6cd492c74 100644 --- a/node/test/util/checkout.js +++ b/node/test/util/checkout.js @@ -37,7 +37,6 @@ const NodeGit = require("nodegit"); const Checkout = require("../../lib/util/checkout"); const GitUtil = require("../../lib/util/git_util"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); -const StashUtil = require("../../lib/util/stash_util"); const SubmoduleUtil = require("../../lib/util/submodule_util"); const UserError = require("../../lib/util/user_error"); @@ -655,28 +654,24 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, repo); const open = yield SubmoduleUtil.listOpenSubmodules( repo); - const resolvedFiles = SubmoduleUtil.resolvePaths(c.paths, + const resolvedPaths = SubmoduleUtil.resolvePaths(c.paths, index, open, true); + let checkoutFromIndex; let annotated; if (c.commit === ":0") { - let shadow = yield StashUtil.makeShadowCommit( - repo, "index", true, true, false, true); - if (shadow === null) { - const head = yield repo.head(); - shadow = yield head.target(); - } - annotated = yield NodeGit.Commit.lookup(repo, - shadow.metaCommit); - assert (annotated !== null); + checkoutFromIndex = true; } else { const mapped = maps.reverseCommitMap[c.commit]; annotated = yield NodeGit.Commit.lookup(repo, mapped); } - yield Checkout.checkoutFiles(repo, annotated, - resolvedFiles); + yield Checkout.checkoutFiles(repo, { + commit: annotated, + resolvedPaths: resolvedPaths, + checkoutFromIndex: checkoutFromIndex + }); }); yield RepoASTTestUtil.testMultiRepoManipulator(c.input, c.expected, From fb7593fa3b8a6aa967d56914971b1955b5a5dcdb Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 2 Jul 2018 12:42:50 -0400 Subject: [PATCH 171/402] fix checkout tests --- node/lib/util/checkout.js | 15 ++------------- node/test/util/checkout.js | 10 +++++++--- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index f5a7781d4..43fcb9805 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -398,19 +398,11 @@ Could not resolve ${colors.red(committish)} as a branch or commit.`); } } } - if (null === annotated) { + if (null === annotated && !result.newBranch) { // If we didn't resolve anything from `committish`, try it // as a file. files.splice(0, 0, committish); - // We would now like to resolve from the index, but - // libgit2 doesn't support the ":0" shorthand, and our - // remaining code all assumes an actual commit, so - // we're just going to make one. The cache-tree ought to - // make this relatively cheap. - result.checkoutFromIndex = true; - } else { - result.commit = annotated; } } else { @@ -435,10 +427,7 @@ Could not resolve ${colors.red(committish)} as a branch or commit.`); } } else { - if (files.length !== 0) { - //When checking out files, we're implicitly using the index - result.checkoutFromIndex = true; - } else { + if (files.length === 0) { // If we're implicitly using HEAD, see if it's on a branch // and record that branch's name. diff --git a/node/test/util/checkout.js b/node/test/util/checkout.js index 6cd492c74..87fd314c4 100644 --- a/node/test/util/checkout.js +++ b/node/test/util/checkout.js @@ -317,8 +317,7 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, state: "x=S", committish: "bar", track: false, - expectedSha: "1", // here, there are no index changes, so the - // shadow-commit will be a no-op + expectedCheckoutFromIndex: true, expectedFiles: "bar", expectedNewBranch: null, expectedSwitchBranch: null, @@ -480,6 +479,7 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, const newBranch = c.newBranch || null; const track = c.track || false; let result; + process.chdir(repo.workdir()); try { result = yield Checkout.deriveCheckoutOperation(repo, committish, @@ -495,7 +495,7 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, assert(!c.fails, "was supposed to fail"); const expectedSha = c.expectedSha; const commit = result.commit; - if (null !== expectedSha) { + if (!!expectedSha) { assert.isNotNull(commit); const commitId = commit.id().tostrS(); const sha = written.commitMap[commitId]; @@ -504,6 +504,10 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, else { assert.isNull(commit); } + if (undefined !== c.expectedCheckoutFromIndex) { + assert.equal(c.expectedCheckoutFromIndex, + result.checkoutFromIndex); + } assert.deepEqual(result.newBranch, c.expectedNewBranch); assert.equal(result.switchBranch, c.expectedSwitchBranch); })); From 992b273d923a8faf6ea3ada99aaf5185cccfe3f0 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 6 Jul 2018 17:35:05 -0400 Subject: [PATCH 172/402] do not screw up escaping on shell commands --- node/lib/cmd/forward.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/node/lib/cmd/forward.js b/node/lib/cmd/forward.js index 0f6b44a40..f161fef30 100644 --- a/node/lib/cmd/forward.js +++ b/node/lib/cmd/forward.js @@ -79,16 +79,17 @@ exports.execute = co.wrap(function *(name, args) { const GitUtil = require("../util/git_util"); + if (name === "diff") { + args.splice(0, 0, "--submodule=diff"); + } const gitArgs = [ "-C", GitUtil.getRootGitDirectory(), name, - name === "diff" ? "--submodule=diff" : "" ].concat(args); try { yield ChildProcess.spawn("git", gitArgs, { stdio: "inherit", - shell: true, }); } catch (e) { From 6c728e1f7f765863c3e0d6cd958d9ad031951864 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 6 Jul 2018 15:23:34 -0400 Subject: [PATCH 173/402] always strip commit messages --- node/lib/util/commit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index ab0cbe2ff..4ec1f64e1 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -2058,9 +2058,9 @@ exports.doCommitCommand = co.wrap(function *(repo, // If no message on the command line, prompt for one. const initialMessage = exports.formatEditorPrompt(repoStatus, cwd); - const rawMessage = yield editMessage(repo, initialMessage); - message = GitUtil.stripMessage(rawMessage); + message = yield editMessage(repo, initialMessage); } + message = GitUtil.stripMessage(message); if ("" === message) { abortForNoMessage(); From 96286c209b789c70fb306bf230c6a87ec754e526 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 9 Jul 2018 13:47:44 -0400 Subject: [PATCH 174/402] make execHook take a repo arg --- node/lib/cmd/add_submodule.js | 2 +- node/lib/cmd/checkout.js | 3 ++- node/lib/cmd/cherry_pick.js | 2 +- node/lib/cmd/merge.js | 2 +- node/lib/cmd/open.js | 2 +- node/lib/cmd/reset.js | 2 +- node/lib/util/close_util.js | 2 +- node/lib/util/hook.js | 9 ++++----- node/lib/util/rebase_util.js | 2 +- node/test/util/hook.js | 5 +++-- 10 files changed, 16 insertions(+), 15 deletions(-) diff --git a/node/lib/cmd/add_submodule.js b/node/lib/cmd/add_submodule.js index b3921a401..fe0a867b7 100644 --- a/node/lib/cmd/add_submodule.js +++ b/node/lib/cmd/add_submodule.js @@ -127,5 +127,5 @@ you will not be able to use 'git meta commit' until you do so.`); } //Run post-add-submodule hook with submodule names which added successfully. - yield Hook.execHook("post-add-submodule", [args.path]); + yield Hook.execHook(repo, "post-add-submodule", [args.path]); }); diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index 06b2b0f66..9081ab453 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -206,7 +206,8 @@ exports.executeableSubcommand = co.wrap(function *(args) { const headId = yield repo.getHeadCommit(); const oldHead = headId.id().tostrS(); const newHead = op.commit; - yield Hook.execHook("post-checkout", [oldHead, newHead, branchCheckout]); + yield Hook.execHook(repo, "post-checkout", + [oldHead, newHead, branchCheckout]); // If we made a new branch, let the user know about it. const newB = op.newBranch; diff --git a/node/lib/cmd/cherry_pick.js b/node/lib/cmd/cherry_pick.js index 8b18a6634..c9acc245c 100644 --- a/node/lib/cmd/cherry_pick.js +++ b/node/lib/cmd/cherry_pick.js @@ -137,5 +137,5 @@ Could not resolve ${colors.red(commitish)} to a commit.`); } // Run post-commit hook as regular git. - yield Hook.execHook("post-commit"); + yield Hook.execHook(repo, "post-commit"); }); diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index 1b5bf36cc..ac5fe28bb 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -177,5 +177,5 @@ Merge of '${commitName}' // Fixme: --squash is not supported yet, once supported, need to parse 0/1 // as arg into the post-merge hook, 1 means it is a squash merge, 0 means // not. - yield Hook.execHook("post-merge", ["0"]); + yield Hook.execHook(repo, "post-merge", ["0"]); }); diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index d43aac589..9777621a3 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -149,7 +149,7 @@ Opening ${colors.blue(name)} on ${colors.green(shas[index])}.`); yield SparseCheckoutUtil.writeMetaIndex(repo, index); // Run post-open-submodule hook with submodules which opened successfully. - yield Hook.execHook("post-open-submodule", subsOpenSuccessfully); + yield Hook.execHook(repo, "post-open-submodule", subsOpenSuccessfully); if (failed) { process.exit(1); diff --git a/node/lib/cmd/reset.js b/node/lib/cmd/reset.js index 9dcefb9c1..4f63ead52 100644 --- a/node/lib/cmd/reset.js +++ b/node/lib/cmd/reset.js @@ -209,5 +209,5 @@ exports.executeableSubcommand = co.wrap(function *(args) { process.stdout.write(statusText); // Run post-reset hook. - yield Hook.execHook("post-reset"); + yield Hook.execHook(repo, "post-reset"); }); diff --git a/node/lib/util/close_util.js b/node/lib/util/close_util.js index e41002143..97d43b622 100644 --- a/node/lib/util/close_util.js +++ b/node/lib/util/close_util.js @@ -105,7 +105,7 @@ Pass ${colors.magenta("--force")} to close it anyway. yield SparseCheckoutUtil.writeMetaIndex(repo, yield repo.index()); // Run post-close-submodule hook with submodules which closed successfully. - yield Hook.execHook("post-close-submodule", subsClosedSuccessfully); + yield Hook.execHook(repo, "post-close-submodule", subsClosedSuccessfully); if ("" !== errorMessage) { throw new UserError(errorMessage); } diff --git a/node/lib/util/hook.js b/node/lib/util/hook.js index 038c05860..37dfbe584 100644 --- a/node/lib/util/hook.js +++ b/node/lib/util/hook.js @@ -34,7 +34,6 @@ const assert = require("chai").assert; const ChildProcess = require("child-process-promise"); const co = require("co"); const path = require("path"); -const GitUtil = require("../util/git_util"); const process = require("process"); /** @@ -44,15 +43,15 @@ const process = require("process"); * @param {String[]} args * @return {String} */ -exports.execHook = co.wrap(function *(name, args=[]) { +exports.execHook = co.wrap(function *(repo, name, args=[]) { assert.isString(name); - const rootDirectory = GitUtil.getRootGitDirectory(); - const hookPath = path.join(rootDirectory, ".git/hooks"); + const rootDirectory = repo.path(); + const hookPath = path.join(rootDirectory, "hooks"); const absPath = path.resolve(hookPath, name); try { - process.chdir(rootDirectory); + process.chdir(repo.workdir()); const result = yield ChildProcess.execFile(absPath, args); console.log(result.stdout); return result.stdout; diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index d7fbbc236..84ec5f29a 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -147,7 +147,7 @@ exports.runRebase = co.wrap(function *(repo, seq) { yield restoreHeadBranch(repo, seq); yield SequencerStateUtil.cleanSequencerState(repo.path()); console.log("Finished rebase."); - yield Hook.execHook("post-rewrite", ["rebase"]); + yield Hook.execHook(repo, "post-rewrite", ["rebase"]); return result; }); diff --git a/node/test/util/hook.js b/node/test/util/hook.js index 461be1dbb..a89c0d0fd 100644 --- a/node/test/util/hook.js +++ b/node/test/util/hook.js @@ -41,12 +41,13 @@ describe("Hook", function () { describe("execHook", function () { // 1. Hook does not exist, no error throws. it("hook_does_not_exist", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); const hookName = "fake_hook"; assert.doesNotThrow( function () { //Nothing happened, no error throws. }, - yield Hook.execHook(hookName) + yield Hook.execHook(repo, hookName) ); })); @@ -61,7 +62,7 @@ describe("Hook", function () { yield fs.writeFile(hookFile, "#!/bin/bash \necho 'it is a test hook'\n"); yield fs.chmod(hookFile, "755"); - const result = yield Hook.execHook(hookName); + const result = yield Hook.execHook(repo, hookName); assert.equal(result, "it is a test hook\n"); })); }); From 1f35f05eb1dc186e324df053ba3e157566f7a73d Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 9 Jul 2018 13:48:02 -0400 Subject: [PATCH 175/402] execute post-checkout hook after setting HEAD (e.g. after opening a repo) --- node/lib/util/git_util.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index aad97688d..4ef21e1a9 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -44,6 +44,7 @@ const path = require("path"); const ConfigUtil = require("./config_util"); const DoWorkQueue = require("./do_work_queue"); +const Hook = require("./hook"); const UserError = require("./user_error"); /** @@ -707,10 +708,20 @@ exports.setHeadHard = co.wrap(function *(repo, commit) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); + const headId = yield repo.getHeadCommit(); + let oldHead; + if (headId === null) { + oldHead = "0000000000000000000000000000000000000000"; + } else { + oldHead = headId.id().tostrS(); + } + const newHead = commit.sha(); + yield NodeGit.Checkout.tree(repo, commit, { checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, }); repo.setHeadDetached(commit); + yield Hook.execHook(repo, "post-checkout", [oldHead, newHead, "1"]); }); /** From dd7491df0072f005fb930a2aa3643a201a858ea4 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 10 Jul 2018 13:33:24 -0400 Subject: [PATCH 176/402] run post-checkout hook after ff rebase --- node/lib/util/rebase_util.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index 84ec5f29a..5b525dd7c 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -203,6 +203,7 @@ running rebase.`); if (canFF) { yield Reset.reset(repo, onto, Reset.TYPE.HARD); console.log(`Fast-forwarded to ${GitUtil.shortSha(ontoSha)}`); + yield Hook.execHook(repo, "post-checkout", [headSha, ontoSha, "1"]); return result; // RETURN } From 65457ba08515df4f8e1947bbee44c8ca5bc8d294 Mon Sep 17 00:00:00 2001 From: David Baird Date: Sun, 15 Jul 2018 17:54:00 -0400 Subject: [PATCH 177/402] Move 'deeper' to dependencies in package.json. --- node/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/package.json b/node/package.json index 70aa78399..07621329b 100644 --- a/node/package.json +++ b/node/package.json @@ -33,6 +33,7 @@ "child-process-promise": "", "co": "", "colors": "", + "deeper": "", "fs-promise": "", "group-by": "", "nodegit": "^0.21.1", @@ -42,7 +43,6 @@ }, "devDependencies": { "deepcopy": "", - "deeper": "", "istanbul": "", "mkdirp": "", "mocha": "^3.1.2", From cc59627e63384e299f8078e02e5da41e892833a0 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 18 Jul 2018 17:23:58 -0400 Subject: [PATCH 178/402] fix checkout of nonexistent ref --- node/lib/cmd/checkout.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index 9081ab453..95c423cb8 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -156,6 +156,11 @@ exports.executeableSubcommand = co.wrap(function *(args) { args.track || false, files); + if (op.commit === null) { + throw new UserError(`pathspec '${committish}' did not match any + file(s) known to git.`); + } + // If we're going to check out files, just do that if (op.resolvedPaths !== null && Object.keys(op.resolvedPaths).length !== 0) { From 90318ef1650f9bb353db8ed89fd1dcca754ce1fd Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 18 Jul 2018 17:59:09 -0400 Subject: [PATCH 179/402] fix "undefined" for detached head --- node/lib/cmd/checkout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index 95c423cb8..cbc0fb606 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -198,7 +198,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { // we checked out. branchCheckout = "0"; - console.log(`Checked out ${colors.green(args.committish)}.`); + console.log(`Checked out ${colors.green(committish)}.`); } // Run post-checkout hook. From a16c5c5a1a2ba9ca60d9f0fe496557420c6e0ba3 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 23 Jul 2018 10:32:46 -0400 Subject: [PATCH 180/402] Fix calls to post-checkout hook. Make post-checkout hook run as "branch checkout" in detached head state. Make it run at all in file checkout mode. Per the git docs, "branch checkout" (passing "1" to the post-checkout hook) does not mean "branch, as opposed to detached". It means "branch, as opposed to files". --- node/lib/cmd/checkout.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index cbc0fb606..4b5632956 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -132,13 +132,14 @@ exports.executeableSubcommand = co.wrap(function *(args) { const Checkout = require("../util/checkout"); const GitUtil = require("../util/git_util"); let newBranch = null; - let branchCheckout = "1"; const newBranchNameArr = args["new branch name"]; if (newBranchNameArr) { newBranch = newBranchNameArr[0]; } const repo = yield GitUtil.getCurrentRepo(); + const headId = yield repo.getHeadCommit(); + const oldHead = headId.id().tostrS(); // Validate and determine what operation we're actually doing. @@ -161,10 +162,14 @@ exports.executeableSubcommand = co.wrap(function *(args) { file(s) known to git.`); } + const newHead = op.commit; + // If we're going to check out files, just do that if (op.resolvedPaths !== null && Object.keys(op.resolvedPaths).length !== 0) { yield Checkout.checkoutFiles(repo, op); + yield Hook.execHook(repo, "post-checkout", + [oldHead, newHead, "0"]); return; } @@ -196,8 +201,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { if (null !== op.commit && null === op.switchBranch) { // In this case, we're not making a branch; just let the user know what // we checked out. - - branchCheckout = "0"; + console.log(`Checked out ${colors.green(committish)}.`); } @@ -208,11 +212,8 @@ exports.executeableSubcommand = co.wrap(function *(args) { // branches, flag = "1"), or a file checkout (retrieving a file from the // index, flag = "0"). - const headId = yield repo.getHeadCommit(); - const oldHead = headId.id().tostrS(); - const newHead = op.commit; yield Hook.execHook(repo, "post-checkout", - [oldHead, newHead, branchCheckout]); + [oldHead, newHead, "1"]); // If we made a new branch, let the user know about it. const newB = op.newBranch; From 22815e8e429f2406ec27502441025111435bad75 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 23 Jul 2018 16:42:00 -0400 Subject: [PATCH 181/402] make git meta reset work for newly-added submodules --- node/lib/util/checkout.js | 2 +- node/lib/util/reset.js | 36 +++++++++++++++++++++++++++----- node/test/util/reset.js | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 43fcb9805..8c669ee9d 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -219,7 +219,7 @@ exports.checkoutCommit = co.wrap(function *(repo, commit, force) { } const index = yield repo.index(); - yield Reset.resetMetaRepo(repo, index, commit, changes); + yield Reset.resetMetaRepo(repo, index, commit, changes, false); repo.setHeadDetached(commit); const doCheckout = co.wrap(function *(name) { diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index fd8a406cb..f2b621ddb 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -87,8 +87,10 @@ function getType(type) { * @param {NodeGit.Index} index * @param {NodeGit.Commit} commit * @param {Object} changes from path to `SubmoduleChange` + * @param {Boolean} mixed do not change the working tree */ -exports.resetMetaRepo = co.wrap(function *(repo, index, commit, changes) { +exports.resetMetaRepo = co.wrap(function *(repo, index, commit, changes, + mixed) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(index, NodeGit.Index); assert.instanceOf(commit, NodeGit.Commit); @@ -97,6 +99,10 @@ exports.resetMetaRepo = co.wrap(function *(repo, index, commit, changes) { const tree = yield commit.getTree(); yield index.readTree(tree); + if (mixed) { + return; + } + // Render modules file const modulesFileName = SubmoduleConfigUtil.modulesFileName; @@ -184,14 +190,13 @@ exports.reset = co.wrap(function *(repo, commit, type) { // not going to touch the index or the `.gitmodules` file. if (TYPE.SOFT !== type) { - yield exports.resetMetaRepo(repo, index, commit, changedSubs); + yield exports.resetMetaRepo(repo, index, commit, changedSubs, + TYPE.MIXED === type); } const resetType = getType(type); - // Make a list of submodules to reset, including all that have been changed - // between HEAD and 'commit', and all that are open. - + const removedSubmodules = []; const resetSubmodule = co.wrap(function *(name) { const change = changedSubs[name]; @@ -222,6 +227,14 @@ exports.reset = co.wrap(function *(repo, commit, type) { // If there's no change, use what's been configured in the index. const entry = index.getByPath(name); + // It is possible that this submodule exists in + // .gitmodules but not in the index. Probably this is + // because it is newly-created, but not yet git-added. + if (undefined === entry) { + removedSubmodules.push(name); + return; + } + subCommitSha = entry.id.tostrS(); } else { subCommitSha = change.newSha; @@ -241,11 +254,24 @@ exports.reset = co.wrap(function *(repo, commit, type) { yield index.addByPath(name); }); + + // Make a list of submodules to reset, including all that have been changed + // between HEAD and 'commit', and all that are open. const openSubs = Array.from(openSubsSet); const changedSubNames = Object.keys(changedSubs); const subsToTry = Array.from(new Set(changedSubNames.concat(openSubs))); yield DoWorkQueue.doInParallel(subsToTry, resetSubmodule); + // remove added submodules from .gitmodules + const modules = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, + index); + for (const file of removedSubmodules) { + delete modules[file]; + } + + yield SubmoduleConfigUtil.writeUrls(repo, index, modules, + type === TYPE.MIXED); + // Write the index in case we've had to stage submodule changes. yield SparseCheckoutUtil.writeMetaIndex(repo, index); diff --git a/node/test/util/reset.js b/node/test/util/reset.js index ea1d06b12..2630a3051 100644 --- a/node/test/util/reset.js +++ b/node/test/util/reset.js @@ -103,6 +103,50 @@ describe("reset", function () { })); }); + it("does a mixed and non-mixed reset", co.wrap(function *() { + const input = "a=B|x=S:C2-1 s=Sa:1;Bmaster=2"; + const maps = yield RepoASTTestUtil.createMultiRepos(input); + const repo = maps.repos.x; + const index = yield repo.index(); + + let fromWorkdir = + yield SubmoduleConfigUtil.getSubmodulesFromWorkdir(repo); + assert.equal(1, Object.keys(fromWorkdir).length); + let fromIndex = + yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + assert.equal(1, Object.keys(fromIndex).length); + + const rev = maps.reverseCommitMap; + const commit = yield repo.getCommit(rev["1"]); + const head = yield repo.getHeadCommit(); + const headTree = yield head.getTree(); + const commitTree = yield commit.getTree(); + const diff = yield NodeGit.Diff.treeToTree(repo, + headTree, + commitTree, + null); + const changes = + yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff, true); + + yield Reset.resetMetaRepo(repo, index, commit, changes, true); + + fromWorkdir = + yield SubmoduleConfigUtil.getSubmodulesFromWorkdir(repo); + assert.equal(1, Object.keys(fromWorkdir).length); + fromIndex = + yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + assert.equal(0, Object.keys(fromIndex).length); + + yield Reset.resetMetaRepo(repo, index, commit, changes, false); + + fromWorkdir = + yield SubmoduleConfigUtil.getSubmodulesFromWorkdir(repo); + assert.equal(0, Object.keys(fromWorkdir).length); + fromIndex = + yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + assert.equal(0, Object.keys(fromIndex).length); + + })); it("submodule directory is cleaned up", co.wrap(function *() { const written = yield RepoASTTestUtil.createRepo( "U:C3-2 s;Bfoo=3"); From 36d1e6b45a8123c79f550b99482e7caea080cf57 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Mon, 23 Jul 2018 19:51:49 -0400 Subject: [PATCH 182/402] Fix inaccuracies in description of `git-repo` tool Addresses: https://github.com/twosigma/git-meta/issues/634 --- doc/architecture.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/doc/architecture.md b/doc/architecture.md index bff419889..eafe15f44 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -108,7 +108,7 @@ products that take a similar approach: - [Gitslave](http://gitslave.sourceforge.net) - [myrepos](https://myrepos.branchable.com) -- [Android Repo](https://source.android.com/source/using-repo.html) +- [git-repo](https://gerrit.googlesource.com/git-repo/) - [gclient](http://dev.chromium.org/developers/how-tos/depottools#TOC-gclient) - [Git subtrees](https://git-scm.com/book/en/v1/Git-Tools-Subtree-Merging) - [Git submodules](https://git-scm.com/docs/git-submodule) @@ -116,16 +116,22 @@ products that take a similar approach: All of these tools overlap with the problems git-meta is trying to solve, but none of them are sufficient: -- most don't provide a way to reference the state of all repositories - (Gitslave, Android Repo, Myrepos) -- some require a custom server (Android Repo) -- many are strongly focused on supporting a specific software platform (Android - Repo, gclient) +- Most don't provide a way to reference the state of all repositories (Gitslave + and Myrepos). git-repo does the ability to reference the state of all repos, + but not in a way that can be used easily with normal Git commands (the state + is tracked in an XML file in a separate repository). +- or are strongly focused on supporting a specific piece of software (gclient) - doesn't fully solve the scaling issue (Git subtrees) - prohibitively difficult to use (Git submodules) - lack scalable collaboration (e.g., pull request) strategies -Git submodules come the closest: they do provide the technical ability to solve +The git-repo project uses an approach that is structurally similar to the one +used by git-meta: a (remote) meta-repo tracks the state of the sub-repos in an +XML file. It does not generally try to provide a full suite of +cross-repository operations (such as `rebase`, `cherry-pick`, etc.) and assumes +the use of the Gerrit code review tool. + +Git submodules come the close: they do provide the technical ability to solve the problem, but are very difficult to use and lack some of the desired features. With git-meta, we build on top of Git submodules to provide the desired functionality by leveraging only existing Git commands. From b474671de59559963af36cad5e0a17c100948d5d Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 24 Jul 2018 13:22:47 -0400 Subject: [PATCH 183/402] fix checkout -b --- node/lib/cmd/checkout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index cbc0fb606..b36571a5b 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -156,7 +156,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { args.track || false, files); - if (op.commit === null) { + if (null === op.commit && null === op.newBranch) { throw new UserError(`pathspec '${committish}' did not match any file(s) known to git.`); } From 9c5843879021c6c250bd1e017a292e3f043708eb Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 24 Jul 2018 13:23:10 -0400 Subject: [PATCH 184/402] fix meta checkout -- filename --- node/lib/util/checkout.js | 5 +++++ node/test/util/checkout.js | 1 + 2 files changed, 6 insertions(+) diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 8c669ee9d..aac1627f1 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -454,6 +454,11 @@ Could not resolve ${colors.red(committish)} as a branch or commit.`); result.resolvedPaths = SubmoduleUtil.resolvePaths( absfiles, indexSubNames, openSubmodules); + if (null === result.commit) { + let annotated = yield GitUtil.resolveCommitish(repo, "HEAD"); + result.commit = yield repo.getCommit(annotated.id()); + } + return result; } diff --git a/node/test/util/checkout.js b/node/test/util/checkout.js index 87fd314c4..2038d613d 100644 --- a/node/test/util/checkout.js +++ b/node/test/util/checkout.js @@ -317,6 +317,7 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, state: "x=S", committish: "bar", track: false, + expectedSha: "1", expectedCheckoutFromIndex: true, expectedFiles: "bar", expectedNewBranch: null, From 1b740d1c643bb21cc94389a44a6eab6cfc2b336c Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 23 Jul 2018 10:58:38 -0400 Subject: [PATCH 185/402] fix formatting --- node/lib/cmd/checkout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index b36571a5b..0a19f7dd2 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -158,7 +158,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { if (null === op.commit && null === op.newBranch) { throw new UserError(`pathspec '${committish}' did not match any - file(s) known to git.`); +file(s) known to git.`); } // If we're going to check out files, just do that From a1d26f05b3de65199620a0fa50e709f163965e6e Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 25 Jul 2018 09:38:01 -0400 Subject: [PATCH 186/402] fixed typos in architecture.md --- doc/architecture.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/architecture.md b/doc/architecture.md index eafe15f44..f92cfbcd9 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -117,9 +117,9 @@ All of these tools overlap with the problems git-meta is trying to solve, but none of them are sufficient: - Most don't provide a way to reference the state of all repositories (Gitslave - and Myrepos). git-repo does the ability to reference the state of all repos, + and Myrepos). git-repo has the ability to reference the state of all repos, but not in a way that can be used easily with normal Git commands (the state - is tracked in an XML file in a separate repository). + is tracked in an XML file in a separate repository). - or are strongly focused on supporting a specific piece of software (gclient) - doesn't fully solve the scaling issue (Git subtrees) - prohibitively difficult to use (Git submodules) From 60a0c27fa8c7754863b0285b10b0c596633dffe6 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 26 Jul 2018 12:21:15 -0400 Subject: [PATCH 187/402] Fix checkout of nonexistent branch --- node/lib/util/checkout.js | 2 +- node/lib/util/submodule_util.js | 5 ++++- node/test/util/checkout.js | 20 ++++++++++++++++---- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index aac1627f1..743d8a0e2 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -452,7 +452,7 @@ Could not resolve ${colors.red(committish)} as a branch or commit.`); filename)); result.resolvedPaths = SubmoduleUtil.resolvePaths( - absfiles, indexSubNames, openSubmodules); + absfiles, indexSubNames, openSubmodules, true); if (null === result.commit) { let annotated = yield GitUtil.resolveCommitish(repo, "HEAD"); diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 7e19a9d41..66622ebcf 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -670,7 +670,10 @@ exports.resolvePaths = function (paths, indexSubNames, openSubmodules, let found = false; for (let j = 0; j < subsToCheck.length; ++j) { const subName = subsToCheck[j]; - if (filename.startsWith(subName + "/")) { + if (filename === subName) { + found = true; + result[subName] = []; + } else if (filename.startsWith(subName + "/")) { found = true; const pathInSub = filename.slice(subName.length + 1, filename.length); diff --git a/node/test/util/checkout.js b/node/test/util/checkout.js index 2038d613d..ecd1c7e9b 100644 --- a/node/test/util/checkout.js +++ b/node/test/util/checkout.js @@ -313,13 +313,25 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, }, expectedSwitchBranch: "bar", }, - "no match to committish, no new branch": { + "no match to committish, nor to file, no new branch": { state: "x=S", committish: "bar", track: false, - expectedSha: "1", - expectedCheckoutFromIndex: true, - expectedFiles: "bar", + fails: true, + }, + "no match to committish, no new branch, but ok, a submodule": { + state: "a=B|x=S:C2-1 s=Sa:1;Bmaster=2;Os", + committish: "s", + track: false, + expectedSha: "2", + expectedNewBranch: null, + expectedSwitchBranch: null, + }, + "no match to committish, no new branch, but ok, some files": { + state: "a=B|x=S:C2-1 s=Sa:1;Bmaster=2;Os", + committish: "s/no-such-file-but-we-will-detect-that-later", + track: false, + expectedSha: "2", expectedNewBranch: null, expectedSwitchBranch: null, }, From d9916f94d79f8d76953b1fec22e509bbbc886035 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 30 Jul 2018 15:06:33 -0400 Subject: [PATCH 188/402] fix checkout -- --- node/lib/cmd/checkout.js | 4 ++-- node/lib/util/checkout.js | 3 +-- node/test/util/checkout.js | 17 ++++++++++++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index 097bccd98..66db59e88 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -157,8 +157,8 @@ exports.executeableSubcommand = co.wrap(function *(args) { args.track || false, files); - if (null === op.commit && null === op.newBranch) { - throw new UserError(`pathspec '${committish}' did not match any + if (null === op.commit && !op.checkoutFromIndex && null === op.newBranch) { + throw new UserError(`pathspec '${committish}' did not match any \ file(s) known to git.`); } diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 743d8a0e2..a7c62f7c7 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -455,8 +455,7 @@ Could not resolve ${colors.red(committish)} as a branch or commit.`); absfiles, indexSubNames, openSubmodules, true); if (null === result.commit) { - let annotated = yield GitUtil.resolveCommitish(repo, "HEAD"); - result.commit = yield repo.getCommit(annotated.id()); + result.checkoutFromIndex = true; } return result; diff --git a/node/test/util/checkout.js b/node/test/util/checkout.js index ecd1c7e9b..f597244b2 100644 --- a/node/test/util/checkout.js +++ b/node/test/util/checkout.js @@ -323,7 +323,7 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, state: "a=B|x=S:C2-1 s=Sa:1;Bmaster=2;Os", committish: "s", track: false, - expectedSha: "2", + expectedCheckoutFromIndex: true, expectedNewBranch: null, expectedSwitchBranch: null, }, @@ -331,9 +331,18 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, state: "a=B|x=S:C2-1 s=Sa:1;Bmaster=2;Os", committish: "s/no-such-file-but-we-will-detect-that-later", track: false, - expectedSha: "2", + expectedCheckoutFromIndex: true, + expectedNewBranch: null, + expectedSwitchBranch: null, + }, + "some files after --": { + state: "a=B|x=S:C2-1 s=Sa:1;Bmaster=2;Os", + committish: null, + track: false, + expectedCheckoutFromIndex: true, expectedNewBranch: null, expectedSwitchBranch: null, + files: ["s/no-such-file-but-we-will-detect-that-later"] }, "commit, no new branch, nameless": { state: "x=S", @@ -491,13 +500,15 @@ a=B|x=S:C2-1 s=Sa:1;C3-2 r=Sa:1,t=Sa:1;Os;Bmaster=3;Bfoo=2;H=2`, } const newBranch = c.newBranch || null; const track = c.track || false; + let files = c.files; let result; process.chdir(repo.workdir()); try { result = yield Checkout.deriveCheckoutOperation(repo, committish, newBranch, - track); + track, + files); } catch (e) { if (!c.fails || !(e instanceof UserError)) { From 82def60ebef09ae34dace9c318c25e6e3d02e672 Mon Sep 17 00:00:00 2001 From: Ben Levy Date: Mon, 6 Aug 2018 11:42:24 -0400 Subject: [PATCH 189/402] =?UTF-8?q?Add=20flag=20to=20Commit-Shadow=20that?= =?UTF-8?q?=20allows=20specifying=20the=20set=20of=20sub-repos=E2=80=A6=20?= =?UTF-8?q?(#646)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add flag to Commit-Shadow that allows specifying the set of sub-repos to include in the resulting commit, add tests to stash_util.shadow * Update contract for makeShadowCommit * Error out if included-subrepos flag is passed but no values are associatedwith it * use nargs to enforce at least one value for include-subrepos flag --- node/lib/cmd/commit-shadow.js | 22 ++++++++++++++++------ node/lib/util/shorthand_parser_util.js | 2 +- node/lib/util/stash_util.js | 8 +++++++- node/test/util/stash_util.js | 15 +++++++++++++++ 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/node/lib/cmd/commit-shadow.js b/node/lib/cmd/commit-shadow.js index 136cdfe39..7e53a7825 100644 --- a/node/lib/cmd/commit-shadow.js +++ b/node/lib/cmd/commit-shadow.js @@ -50,12 +50,14 @@ exports.helpText = * @property {String} */ exports.description = `Create a "shadow" commit containing all local -modifications (including untracked files if '--include-untracked' is specified) -to all sub-repos and then print the SHA of the created commit. If there are no -local modifications, print the SHA of HEAD. Do not modify the index or update -HEAD to point to the created commit. Note that this command ignores -non-submodule changes to the meta-repo. Note also that this command is meant -for programmatic use and its output format is stable.`; +modifications (including untracked files if '--include-untracked' is specified, +include only files in the specified directories following '--include-subrepos' +if it is specified, if unspecified all paths are considered) to all sub-repos +and then print the SHA of the created commit. If there are no local +modifications, print the SHA of HEAD. Do not modify the index or update HEAD +to point to the created commit. Note that this command ignores non-submodule +changes to the meta-repo. Note also that this command is meant for programmatic +use and its output format is stable.`; /** * Configure the specified `parser` for the `commit` command. @@ -90,6 +92,12 @@ exports.configureParser = function (parser) { defaultValue: false, help: "use timestamp of HEAD + 1 instead of current time", }); + parser.addArgument(["-s", "--include-subrepos"], { + type: "string", + required: false, + nargs: "+", + help: "only include specified sub-repos", + }); }; /** @@ -106,11 +114,13 @@ exports.executeableSubcommand = co.wrap(function *(args) { const repo = yield GitUtil.getCurrentRepo(); const incrementTimestamp = args.increment_timestamp || args.epoch_timestamp; + const includedSubrepos = args.include_subrepos || []; const result = yield StashUtil.makeShadowCommit(repo, args.message, incrementTimestamp, false, args.include_untracked, + includedSubrepos, false); if (null === result) { const head = yield repo.getHeadCommit(); diff --git a/node/lib/util/shorthand_parser_util.js b/node/lib/util/shorthand_parser_util.js index 9a2ce761b..935bb27f9 100644 --- a/node/lib/util/shorthand_parser_util.js +++ b/node/lib/util/shorthand_parser_util.js @@ -101,7 +101,7 @@ const SequencerState = RepoAST.SequencerState; * "s" with a url of "a" on commit `1` * - N -- for "null", a completely empty repo * - * Whitespace is skipped at the beginning of shorthanad, and also after some, + * Whitespace is skipped at the beginning of shorthand, and also after some, * but not all delimitors. We can't skip it in places where the space * character is itself a separator, such as after the parent commit ids. It is * skipped after separators for: diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index d5c5172cc..af5ff9d43 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -558,7 +558,9 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, /** * Generate a shadow commit in the specified 'repo' with the specified * 'message' and return an object describing the created commits. Ignore - * untracked files unless the specified 'includeUntracked' is true. If the + * untracked files unless the specified 'includeUntracked' is true. + * When 'includedSubrepos' is non-empty only consider files contained within the + * paths specified in includedSubrepos. If the * repository is clean, return null. Note that this command does not affect * the state of 'repo' other than to generate commits. * @@ -571,6 +573,7 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, * @param {Bool} useEpochTimestamp * @param {Bool} includeMeta * @param {Bool} includeUntracked + * @param {Object} includedSubrepos * @param {Bool} indexOnly include only staged changes * @return {Object|null} * @return {String} return.metaCommit @@ -581,11 +584,13 @@ exports.makeShadowCommit = co.wrap(function *(repo, useEpochTimestamp, includeMeta, includeUntracked, + includedSubrepos, indexOnly) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(message); assert.isBoolean(includeMeta); assert.isBoolean(includeUntracked); + assert.isArray(includedSubrepos); assert.isBoolean(useEpochTimestamp); if (indexOnly === undefined) { indexOnly = false; @@ -601,6 +606,7 @@ exports.makeShadowCommit = co.wrap(function *(repo, showMetaChanges: includeMeta, showAllUntracked: true, ignoreIndex: false, + paths: includedSubrepos, }); if (status.isDeepClean(includeUntracked)) { return null; // RETURN diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index 0084628e6..e700f58b9 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -721,11 +721,22 @@ meta-stash@{1}: log of 1 expected: "x=E:Cm-1 foo/bar=2;Bm=m", includeMeta: true, }, + "a new file, included in subrepo list": { + state: "x=S:W foo/bar=2", + includedSubrepos: ["foo/bar"], + expected: "x=E:Cm-1 foo/bar=2;Bm=m", + includeMeta: true, + }, "a new file, untracked not included": { state: "x=S:W foo/bar=2", includeMeta: true, includeUntracked: false, }, + "a new file, tracked but not included in subrepo list": { + state: "x=S:W foo/bar=2", + includedSubrepos: ["bar/"], + includeMeta: true, + }, "with a message": { state: "x=S:W foo/bar=2", expected: "x=E:Cfoo\n#m-1 foo/bar=2;Bm=m", @@ -810,12 +821,16 @@ x=E:Cm-1 s=Sa:s;Bm=m;Os Cs foo=bar!Bs=s!W foo=bar` const incrementTimestamp = (undefined === c.incrementTimestamp) ? false : c.incrementTimestamp; + const includedSubrepos = + (undefined === c.includedSubrepos) ? + [] : c.includedSubrepos; const result = yield StashUtil.makeShadowCommit( repo, message, incrementTimestamp, meta, includeUntracked, + includedSubrepos, false); const commitMap = {}; From ec8ed297b5d9a1f29367da890fde731033561ffc Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Fri, 10 Aug 2018 15:39:19 -0400 Subject: [PATCH 190/402] Optimize amend when many open repos Don't grab the repo for a submodule unless we're sure we need it. --- node/lib/util/commit.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 4ec1f64e1..a224ca3c3 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -1163,27 +1163,36 @@ exports.amendMetaRepo = co.wrap(function *(repo, } const subStatus = subs[subName]; - // If the sub-repo doesn't have an open status, and there are no amend - // changes, there's nothing to do. + // We're working on only open repos (they would have been opened + // earlier if necessary). if (null === subStatus.workdir) { return; // RETURN } + const doAmend = amendSubSet.has(subName); const repoStatus = subStatus.workdir.status; + const staged = repoStatus.staged; + const numStaged = Object.keys(staged).length; + + // If we're not amending and nothing staged, exit now. + + if (!doAmend && 0 === numStaged) { + return; // RETURN + } + const subRepo = yield SubmoduleUtil.getRepo(repo, subName); // If the submodule is to be amended, we don't do the normal commit // process. - if (amendSubSet.has(subName)) { + if (doAmend) { // First, we check to see if this submodule needs to have its last // commit stripped. That will be the case if we have no files // staged indicated as staged. assert.isNotNull(repoStatus); - const staged = repoStatus.staged; - if (0 === Object.keys(staged).length) { + if (0 === numStaged) { const head = yield subRepo.getHeadCommit(); const parent = yield GitUtil.getParentCommit(subRepo, head); const TYPE = NodeGit.Reset.TYPE; @@ -1220,7 +1229,7 @@ exports.amendMetaRepo = co.wrap(function *(repo, } const commit = yield commitRepo(subRepo, - repoStatus.staged, + staged, all, subMessage, false, From 4b0da3ebe6e6ee58e501e540359d960b77db80a5 Mon Sep 17 00:00:00 2001 From: Zokir Tiliaev Date: Fri, 3 Aug 2018 10:59:43 -0400 Subject: [PATCH 191/402] Added stderr and disabled buffering in hooks --- node/lib/util/hook.js | 17 +++++++++++------ node/test/util/hook.js | 9 ++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/node/lib/util/hook.js b/node/lib/util/hook.js index 37dfbe584..68aea17c3 100644 --- a/node/lib/util/hook.js +++ b/node/lib/util/hook.js @@ -31,30 +31,35 @@ "use strict"; const assert = require("chai").assert; -const ChildProcess = require("child-process-promise"); +const spawn = require("child-process-promise").spawn; const co = require("co"); const path = require("path"); const process = require("process"); +const fs = require("fs"); /** * Run git-meta hook with given hook name. + * Return 0 on success, non-zero status otherwise. * @async * @param {String} name * @param {String[]} args - * @return {String} + * @return {int} */ -exports.execHook = co.wrap(function *(repo, name, args=[]) { +exports.execHook = co.wrap(function*(repo, name, args=[]) { assert.isString(name); const rootDirectory = repo.path(); const hookPath = path.join(rootDirectory, "hooks"); const absPath = path.resolve(hookPath, name); + if (!fs.existsSync(absPath)) { + return -1; + } + try { process.chdir(repo.workdir()); - const result = yield ChildProcess.execFile(absPath, args); - console.log(result.stdout); - return result.stdout; + const result = yield spawn(absPath, args, { stdio: "inherit" }); + return result.status; } catch (e) { if (e.code === "EACCES") { console.log("EACCES: Cannot execute: " + absPath); diff --git a/node/test/util/hook.js b/node/test/util/hook.js index a89c0d0fd..f14363fcd 100644 --- a/node/test/util/hook.js +++ b/node/test/util/hook.js @@ -59,11 +59,14 @@ describe("Hook", function () { process.chdir(workDir); const hooksDir = path.join(workDir, ".git/hooks"); const hookFile = path.join(hooksDir, hookName); + const hookOutputFile = path.join(hooksDir, "hook_test"); yield fs.writeFile(hookFile, - "#!/bin/bash \necho 'it is a test hook'\n"); + "#!/bin/bash \necho 'it is a test hook' >" + hookOutputFile); yield fs.chmod(hookFile, "755"); - const result = yield Hook.execHook(repo, hookName); - assert.equal(result, "it is a test hook\n"); + yield Hook.execHook(repo, hookName); + assert.ok(fs.existsSync(hookOutputFile), "File does not exists"); + assert.equal(fs.readFileSync(hookOutputFile, "utf8"), + "it is a test hook\n"); })); }); }); From 2195235af2d0d616c656767944fa093d3dc299a3 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 15 Aug 2018 14:27:37 -0400 Subject: [PATCH 192/402] Better error when merge invoked with no args ... and there is no tracking info for the current branch. --- node/lib/cmd/merge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index ac5fe28bb..b694995d1 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -147,7 +147,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { commitName = yield GitUtil.getCurrentTrackingBranchName(repo); } if (null === commitName) { - throw new UserError("Commit required."); + throw new UserError("No remote for the current branch."); } const commitish = yield GitUtil.resolveCommitish(repo, commitName); if (null === commitish) { From d013a75944bef804e09faf30bf048b93207dc6de Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 16 Aug 2018 15:06:24 -0400 Subject: [PATCH 193/402] irrelevant non-commitable changes should not cause commits to fail --- node/lib/util/commit.js | 13 +++++-------- node/test/util/commit.js | 29 ++++++++++++----------------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index a224ca3c3..4fbeaed1c 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -1417,19 +1417,16 @@ exports.calculatePathCommitStatus = function (current, requested) { Object.keys(currentSubs).forEach(subName => { const currentSub = currentSubs[subName]; const requestedSub = requestedSubs[subName]; + // If this submodule was not requested, don't include it. + if (undefined === requestedSub) { + return; + } const curWd = currentSub.workdir; if (null !== curWd) { const curStatus = curWd.status; - // If this submodule was not requested (i.e., - // `undefined === requestedSubs`, default to an empty repo status; - // this will cause all current status files to be moved to the - // workdir. + const reqStatus = requestedSub.workdir.status; - let reqStatus = new RepoStatus(); - if (undefined !== requestedSub) { - reqStatus = requestedSub.workdir.status; - } const newSubFiles = calculateOneRepo(curStatus.staged, curStatus.workdir, reqStatus.staged, diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 43f8da7c0..71a9e4df2 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -1879,22 +1879,7 @@ my message }, }), req: new RepoStatus(), - exp: new RepoStatus({ - submodules: { - xxx: new Submodule({ - commit: new Submodule.Commit("1", "/a"), - index: new Submodule.Index("1", - "/a", - RELATION.SAME), - workdir: new Submodule.Workdir(new RepoStatus({ - headCommit: "1", - workdir: { - xxx: FILESTATUS.MODIFIED, - }, - }), RELATION.SAME), - }), - }, - }), + exp: new RepoStatus(), }, }; Object.keys(cases).forEach(caseName => { @@ -3085,9 +3070,19 @@ x=S:Cfoo\n#x-2 s=Sa:s;Os Cfoo\n#s-1 a/b=b!I b=d!H=s;Bmaster=x`, "not path-compatible": { initial: "x=S:I s=S.:1,a=b", message: "foo", - paths: ["a"], + paths: ["s"], fails: true, }, + "path-compatible (submodule)": { + initial: "x=S:C2-1 g=S I s=S.:1,a=b;Bmaster=2", + message: "foo", + paths: ["s"], + }, + "path-compatible": { + initial: "x=S:I s=S.:1,a=b", + message: "foo", + paths: ["a"], + }, "interactive": { initial: "a=B|x=U:Os I a=b", interactive: true, From 67b16fdeefaa4d41fe25eae6501dcf0d238a9597 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 16 Aug 2018 16:14:26 -0400 Subject: [PATCH 194/402] remove deleted files from submodule index --- node/lib/util/commit.js | 1 + node/test/util/commit.js | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 4fbeaed1c..bf0e79e43 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -597,6 +597,7 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message) { for (let filename in staged) { const stat = staged[filename]; if (FILESTATUS.REMOVED === stat) { + yield index.removeByPath(filename); changes[filename] = null; } else { diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 71a9e4df2..599675a9c 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -1938,6 +1938,11 @@ my message fileChanges: { "q": FILESTATUS.ADDED }, expected: "x=S:Cx-1 q=3;Bmaster=x", }, + "removed file": { + state: "x=S:C2-1 foo=bar;C3-2;W foo;Bmaster=3", + fileChanges: { "foo": FILESTATUS.REMOVED }, + expected: "x=S:Cx-3 foo;Bmaster=x", + }, "added two files, but mentioned only one": { state: "x=S:W q=3,r=4", fileChanges: { "q": FILESTATUS.ADDED }, From 4883e1548ccfa70237c7f44cab4c82fbbb9bd9eb Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 20 Aug 2018 20:49:36 -0400 Subject: [PATCH 195/402] dumb fix for color output to pipes --- node/lib/cmd/add_submodule.js | 2 +- node/lib/cmd/checkout.js | 2 +- node/lib/cmd/cherry_pick.js | 2 +- node/lib/cmd/merge.js | 2 +- node/lib/cmd/open.js | 2 +- node/lib/cmd/rebase.js | 2 +- node/lib/cmd/stash.js | 2 +- node/lib/cmd/submodule.js | 2 +- node/lib/util/add.js | 2 +- node/lib/util/add_submodule.js | 2 +- node/lib/util/checkout.js | 2 +- node/lib/util/cherry_pick_util.js | 2 +- node/lib/util/close_util.js | 2 +- node/lib/util/colors.js | 63 +++++++++++++++++++++++++ node/lib/util/commit.js | 2 +- node/lib/util/git_util.js | 2 +- node/lib/util/merge_util.js | 2 +- node/lib/util/open.js | 2 +- node/lib/util/pull.js | 2 +- node/lib/util/push.js | 2 +- node/lib/util/rebase_util.js | 2 +- node/lib/util/repo_ast_util.js | 2 +- node/lib/util/stash_util.js | 2 +- node/lib/util/submodule_rebase_util.js | 2 +- node/lib/util/submodule_util.js | 2 +- node/test/util/cherry_pick_util.js | 2 +- node/test/util/merge_util.js | 2 +- node/test/util/print_status_util.js | 2 +- node/test/util/rebase_util.js | 2 +- node/test/util/submodule_rebase_util.js | 2 +- 30 files changed, 92 insertions(+), 29 deletions(-) create mode 100644 node/lib/util/colors.js diff --git a/node/lib/cmd/add_submodule.js b/node/lib/cmd/add_submodule.js index fe0a867b7..717b586b4 100644 --- a/node/lib/cmd/add_submodule.js +++ b/node/lib/cmd/add_submodule.js @@ -75,7 +75,7 @@ Branch in repo from which we are importing to checkout as HEAD.`, * @param {String[]} args.paths */ exports.executeableSubcommand = co.wrap(function *(args) { - const colors = require("colors"); + const colors = require("../util/colors"); const fs = require("fs-promise"); const path = require("path"); diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index 66db59e88..3e48cd3aa 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -127,7 +127,7 @@ function reanalyzeArgs(args) { exports.executeableSubcommand = co.wrap(function *(args) { reanalyzeArgs(args); - const colors = require("colors"); + const colors = require("../util/colors"); const Checkout = require("../util/checkout"); const GitUtil = require("../util/git_util"); diff --git a/node/lib/cmd/cherry_pick.js b/node/lib/cmd/cherry_pick.js index c9acc245c..b6b5766f3 100644 --- a/node/lib/cmd/cherry_pick.js +++ b/node/lib/cmd/cherry_pick.js @@ -85,7 +85,7 @@ abort the cherry-pick and return to previous state, throwing away all changes`, * @param {String[]} args.commit */ exports.executeableSubcommand = co.wrap(function *(args) { - const colors = require("colors"); + const colors = require("../util/colors"); const CherryPickUtil = require("../util/cherry_pick_util"); const GitUtil = require("../util/git_util"); diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index b694995d1..3167bdda9 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -106,7 +106,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { // TODO: For now, we will always create a merge commit. We should be able // to control whether FF is allowed/required for meta and sub repos. - const colors = require("colors"); + const colors = require("../util/colors"); const MergeUtil = require("../util/merge_util"); const GitUtil = require("../util/git_util"); diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 9777621a3..2ab563b15 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -77,7 +77,7 @@ exports.configureParser = function (parser) { * @param {String} args.path */ exports.executeableSubcommand = co.wrap(function *(args) { - const colors = require("colors"); + const colors = require("../util/colors"); const DoWorkQueue = require("../util/do_work_queue"); const GitUtil = require("../util/git_util"); diff --git a/node/lib/cmd/rebase.js b/node/lib/cmd/rebase.js index dced3fb76..366adc4c8 100644 --- a/node/lib/cmd/rebase.js +++ b/node/lib/cmd/rebase.js @@ -81,7 +81,7 @@ exports.configureParser = function (parser) { exports.executeableSubcommand = co.wrap(function *(args) { // TODO: add applicable `git rebase` options. - const colors = require("colors"); + const colors = require("../util/colors"); const RebaseUtil = require("../util/rebase_util"); const GitUtil = require("../util/git_util"); diff --git a/node/lib/cmd/stash.js b/node/lib/cmd/stash.js index 2d601f6ee..2e4bbba18 100644 --- a/node/lib/cmd/stash.js +++ b/node/lib/cmd/stash.js @@ -165,7 +165,7 @@ const doDrop = co.wrap(function *(args) { * @param {Object} args */ exports.executeableSubcommand = function (args) { - const colors = require("colors"); + const colors = require("../util/colors"); switch(args.type) { case "pop" : return doPop(args); diff --git a/node/lib/cmd/submodule.js b/node/lib/cmd/submodule.js index 375fb04eb..95905b29d 100644 --- a/node/lib/cmd/submodule.js +++ b/node/lib/cmd/submodule.js @@ -183,7 +183,7 @@ const doStatusCommand = co.wrap(function *(paths, verbose) { }); const doFindCommand = co.wrap(function *(path, metaCommittish, subCommittish) { - const colors = require("colors"); + const colors = require("../util/colors"); const GitUtil = require("../util/git_util"); const LogUtil = require("../util/log_util"); diff --git a/node/lib/util/add.js b/node/lib/util/add.js index fb163d4c0..933630056 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); diff --git a/node/lib/util/add_submodule.js b/node/lib/util/add_submodule.js index 8377c7c04..f32d82561 100644 --- a/node/lib/util/add_submodule.js +++ b/node/lib/util/add_submodule.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const NodeGit = require("nodegit"); const GitUtil = require("./git_util"); diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index a7c62f7c7..9cab7a572 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -35,7 +35,7 @@ */ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const NodeGit = require("nodegit"); const DoWorkQueue = require("../util/do_work_queue"); diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 3c94171a9..1c107625c 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const mkdirp = require("mkdirp"); const NodeGit = require("nodegit"); const path = require("path"); diff --git a/node/lib/util/close_util.js b/node/lib/util/close_util.js index 97d43b622..4ed98f308 100644 --- a/node/lib/util/close_util.js +++ b/node/lib/util/close_util.js @@ -33,7 +33,7 @@ const assert = require("chai").assert; const NodeGit = require("nodegit"); const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const Hook = require("../util/hook"); const SparseCheckoutUtil = require("../util/sparse_checkout_util"); diff --git a/node/lib/util/colors.js b/node/lib/util/colors.js new file mode 100644 index 000000000..98d9b7cee --- /dev/null +++ b/node/lib/util/colors.js @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +/** + * This is a wrapper around the 'colors' module that checks if stdout + * is a tty before writing any colored text. Of course, this is sort + * of bogus: maybe stdout is a TTY but we're writing to stderr which + * isn't (or, less seriously but more likely, visa-versa). But it is + * the least-intrusive thing we can do to make colors even sort of + * respect non-TTY output, so it'll do for now. + */ + + +const colors = require("colors"); + +function identity(str) { + return str; +} + +function wrap(fn) { + if (process.stdout.isTTY) { + return fn; + } else { + return identity; + } +} + +for (let fn in colors) { + exports[fn] = wrap(colors[fn]); +} +for (let fn in colors.styles) { + exports[fn] = wrap(colors[fn]); +} + diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index bf0e79e43..496d1c38c 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -39,7 +39,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 4ef21e1a9..4e67333da 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -37,7 +37,7 @@ const assert = require("chai").assert; const ChildProcess = require("child-process-promise"); const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 91e7c0252..00ca3c215 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -33,7 +33,7 @@ const assert = require("chai").assert; const ChildProcess = require("child-process-promise"); const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const NodeGit = require("nodegit"); const Checkout = require("./checkout"); diff --git a/node/lib/util/open.js b/node/lib/util/open.js index ed1f2e631..951cdbdbf 100644 --- a/node/lib/util/open.js +++ b/node/lib/util/open.js @@ -34,7 +34,7 @@ */ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const NodeGit = require("nodegit"); const GitUtil = require("./git_util"); diff --git a/node/lib/util/pull.js b/node/lib/util/pull.js index 5b3d4993f..ea91c0a34 100644 --- a/node/lib/util/pull.js +++ b/node/lib/util/pull.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const NodeGit = require("nodegit"); const ConfigUtil = require("./config_util"); diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 94ca0bcbd..b24259d43 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -36,7 +36,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const NodeGit = require("nodegit"); const DoWorkQueue = require("./do_work_queue"); diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index 5b525dd7c..28d441cd2 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const NodeGit = require("nodegit"); const Checkout = require("./checkout"); diff --git a/node/lib/util/repo_ast_util.js b/node/lib/util/repo_ast_util.js index a330aca2f..c56f0a675 100644 --- a/node/lib/util/repo_ast_util.js +++ b/node/lib/util/repo_ast_util.js @@ -36,7 +36,7 @@ */ const assert = require("chai").assert; -const colors = require("colors"); +const colors = require("../util/colors"); const deeper = require("deeper"); const RepoAST = require("../util/repo_ast"); diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index af5ff9d43..06be1b75b 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const NodeGit = require("nodegit"); const GitUtil = require("./git_util"); diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index 4b2465f36..9390fe01a 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const NodeGit = require("nodegit"); const path = require("path"); const rimraf = require("rimraf"); diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 66622ebcf..3f198d2d7 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -36,7 +36,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../util/colors"); const NodeGit = require("nodegit"); const fs = require("fs-promise"); const path = require("path"); diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index d7faba1ea..949843fad 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../../lib/util/colors"); const NodeGit = require("nodegit"); const ConflictUtil = require("../../lib/util/conflict_util"); diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index b8930b05f..481f81e71 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../../lib/util/colors"); const MergeUtil = require("../../lib//util/merge_util"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js index 03326d3bb..3c2c0f9b3 100644 --- a/node/test/util/print_status_util.js +++ b/node/test/util/print_status_util.js @@ -31,7 +31,7 @@ "use strict"; const assert = require("chai").assert; -const colors = require("colors"); +const colors = require("../../lib/util/colors"); const NodeGit = require("nodegit"); const RepoStatus = require("../../lib/util/repo_status"); diff --git a/node/test/util/rebase_util.js b/node/test/util/rebase_util.js index 568f07f81..84c416e63 100644 --- a/node/test/util/rebase_util.js +++ b/node/test/util/rebase_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../../lib/util/colors"); const RebaseUtil = require("../../lib/util/rebase_util"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); diff --git a/node/test/util/submodule_rebase_util.js b/node/test/util/submodule_rebase_util.js index e187bb395..c676dc53b 100644 --- a/node/test/util/submodule_rebase_util.js +++ b/node/test/util/submodule_rebase_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); +const colors = require("../../lib/util/colors"); const NodeGit = require("nodegit"); const RepoAST = require("../../lib/util/repo_ast"); From a83bce306bc60927191f8429de6c093410d77660 Mon Sep 17 00:00:00 2001 From: Ben Levy Date: Wed, 22 Aug 2018 10:49:16 -0400 Subject: [PATCH 196/402] Turn off GC in submodules on open --- node/lib/util/config_util.js | 19 +++++++++++++++++-- node/lib/util/open.js | 5 +++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/node/lib/util/config_util.js b/node/lib/util/config_util.js index 2157370c3..0652191b7 100644 --- a/node/lib/util/config_util.js +++ b/node/lib/util/config_util.js @@ -59,16 +59,31 @@ exports.getConfigString = co.wrap(function *(config, key) { return null; }); + +/** + * Attempt to set the key, value pair in the specified git configuration. Returns 0 or an error code + * @param {NodeGit.Config} config + * @param {String} key + * @param {Integer} value + * + */ +exports.setConfigInt = co.wrap(function *(config, key, val) { + assert.instanceOf(config, NodeGit.Config); + assert.isString(key); + assert.isNumber(val); + return config.setInt64(key, val); +}); + /** * Returns whether a config variable is, according to git's reckoning, * true. That is, it's set to 'true', 'yes', or 'on'. If the variable is not - * se at all, return null. + * set at all, return null. * @async * @param {NodeGit.Repository} repo * @param {NodeGit.Commit} configVar * @return {Bool|null} * @throws if the configuration variable doesn't exist -*/ + */ exports.configIsTrue = co.wrap(function*(repo, configVar) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(configVar); diff --git a/node/lib/util/open.js b/node/lib/util/open.js index 951cdbdbf..c775e2ad4 100644 --- a/node/lib/util/open.js +++ b/node/lib/util/open.js @@ -37,6 +37,7 @@ const co = require("co"); const colors = require("../util/colors"); const NodeGit = require("nodegit"); +const ConfigUtil = require("./config_util"); const GitUtil = require("./git_util"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const SubmoduleUtil = require("./submodule_util"); @@ -85,6 +86,10 @@ exports.openOnCommit = co.wrap(function *(fetcher, submoduleUrl, templatePath); + // Turn off GC for the submodule + const config = yield submoduleRepo.config(); + yield ConfigUtil.setConfigInt(config, "gc.auto", 0); + // Fetch the needed sha. Close if the fetch fails; otherwise, the // repository ends up in a state where it things the submodule is open, but // it's actually not. From a633dc33bc01ef91301914253cb269bd2f167448 Mon Sep 17 00:00:00 2001 From: blevz Date: Wed, 22 Aug 2018 11:28:50 -0400 Subject: [PATCH 197/402] Remove unnecessary function call, add tests for gc config change --- node/lib/util/config_util.js | 15 --------------- node/lib/util/open.js | 3 +-- node/test/util/open.js | 11 ++++++++++- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/node/lib/util/config_util.js b/node/lib/util/config_util.js index 0652191b7..6077f2987 100644 --- a/node/lib/util/config_util.js +++ b/node/lib/util/config_util.js @@ -59,21 +59,6 @@ exports.getConfigString = co.wrap(function *(config, key) { return null; }); - -/** - * Attempt to set the key, value pair in the specified git configuration. Returns 0 or an error code - * @param {NodeGit.Config} config - * @param {String} key - * @param {Integer} value - * - */ -exports.setConfigInt = co.wrap(function *(config, key, val) { - assert.instanceOf(config, NodeGit.Config); - assert.isString(key); - assert.isNumber(val); - return config.setInt64(key, val); -}); - /** * Returns whether a config variable is, according to git's reckoning, * true. That is, it's set to 'true', 'yes', or 'on'. If the variable is not diff --git a/node/lib/util/open.js b/node/lib/util/open.js index c775e2ad4..48a6b3010 100644 --- a/node/lib/util/open.js +++ b/node/lib/util/open.js @@ -37,7 +37,6 @@ const co = require("co"); const colors = require("../util/colors"); const NodeGit = require("nodegit"); -const ConfigUtil = require("./config_util"); const GitUtil = require("./git_util"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const SubmoduleUtil = require("./submodule_util"); @@ -88,7 +87,7 @@ exports.openOnCommit = co.wrap(function *(fetcher, // Turn off GC for the submodule const config = yield submoduleRepo.config(); - yield ConfigUtil.setConfigInt(config, "gc.auto", 0); + config.setInt64("gc.auto", 0); // Fetch the needed sha. Close if the fetch fails; otherwise, the // repository ends up in a state where it things the submodule is open, but diff --git a/node/test/util/open.js b/node/test/util/open.js index 132fe9baf..cb84ecdb8 100644 --- a/node/test/util/open.js +++ b/node/test/util/open.js @@ -34,6 +34,7 @@ const assert = require("chai").assert; const co = require("co"); const NodeGit = require("nodegit"); +const ConfigUtil = require("../../lib/util/config_util"); const Open = require("../../lib/util/open"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); const SubmoduleFetcher = require("../../lib/util/submodule_fetcher"); @@ -117,6 +118,10 @@ describe("openOnCommit", function () { const base = yield SubmoduleUtil.getRepo(repo, "s"); assert.equal(s1, s2, "not re-opened"); assert.equal(s1.workdir(), base.workdir(), "right path"); + const config = yield s1.config(); + const gcConfig = yield ConfigUtil.getConfigString(config, + "gc.auto"); + assert.equal("0", gcConfig); })); it("different commit", co.wrap(function *() { const state = "a=B:Ca-1;Ba=a|x=U:C3-2 s=Sa:a;Bfoo=3"; @@ -203,9 +208,13 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos(state); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - yield opener.getSubrepo("s"); + const s = yield opener.getSubrepo("s"); const result = yield opener.isOpen("s"); assert.equal(true, result); + const config = yield s.config(); + const gcConfig = yield ConfigUtil.getConfigString(config, + "gc.auto"); + assert.equal("0", gcConfig); })); it("getOpenSubs, true immediately", co.wrap(function *() { const state = "a=B|x=U:Os"; From 98ce4049e1efc0154f64e91f278c3e9d71ad05e0 Mon Sep 17 00:00:00 2001 From: "David \"novalis\" Turner" Date: Tue, 21 Aug 2018 13:49:17 -0400 Subject: [PATCH 198/402] Revert "dumb fix for color output to pipes" This reverts commit 4883e1548ccfa70237c7f44cab4c82fbbb9bd9eb. --- node/lib/cmd/add_submodule.js | 2 +- node/lib/cmd/checkout.js | 2 +- node/lib/cmd/cherry_pick.js | 2 +- node/lib/cmd/merge.js | 2 +- node/lib/cmd/open.js | 2 +- node/lib/cmd/rebase.js | 2 +- node/lib/cmd/stash.js | 2 +- node/lib/cmd/submodule.js | 2 +- node/lib/util/add.js | 2 +- node/lib/util/add_submodule.js | 2 +- node/lib/util/checkout.js | 2 +- node/lib/util/cherry_pick_util.js | 2 +- node/lib/util/close_util.js | 2 +- node/lib/util/colors.js | 63 ------------------------- node/lib/util/commit.js | 2 +- node/lib/util/git_util.js | 2 +- node/lib/util/merge_util.js | 2 +- node/lib/util/open.js | 2 +- node/lib/util/pull.js | 2 +- node/lib/util/push.js | 2 +- node/lib/util/rebase_util.js | 2 +- node/lib/util/repo_ast_util.js | 2 +- node/lib/util/stash_util.js | 2 +- node/lib/util/submodule_rebase_util.js | 2 +- node/lib/util/submodule_util.js | 2 +- node/test/util/cherry_pick_util.js | 2 +- node/test/util/merge_util.js | 2 +- node/test/util/print_status_util.js | 2 +- node/test/util/rebase_util.js | 2 +- node/test/util/submodule_rebase_util.js | 2 +- 30 files changed, 29 insertions(+), 92 deletions(-) delete mode 100644 node/lib/util/colors.js diff --git a/node/lib/cmd/add_submodule.js b/node/lib/cmd/add_submodule.js index 717b586b4..fe0a867b7 100644 --- a/node/lib/cmd/add_submodule.js +++ b/node/lib/cmd/add_submodule.js @@ -75,7 +75,7 @@ Branch in repo from which we are importing to checkout as HEAD.`, * @param {String[]} args.paths */ exports.executeableSubcommand = co.wrap(function *(args) { - const colors = require("../util/colors"); + const colors = require("colors"); const fs = require("fs-promise"); const path = require("path"); diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index 3e48cd3aa..66db59e88 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -127,7 +127,7 @@ function reanalyzeArgs(args) { exports.executeableSubcommand = co.wrap(function *(args) { reanalyzeArgs(args); - const colors = require("../util/colors"); + const colors = require("colors"); const Checkout = require("../util/checkout"); const GitUtil = require("../util/git_util"); diff --git a/node/lib/cmd/cherry_pick.js b/node/lib/cmd/cherry_pick.js index b6b5766f3..c9acc245c 100644 --- a/node/lib/cmd/cherry_pick.js +++ b/node/lib/cmd/cherry_pick.js @@ -85,7 +85,7 @@ abort the cherry-pick and return to previous state, throwing away all changes`, * @param {String[]} args.commit */ exports.executeableSubcommand = co.wrap(function *(args) { - const colors = require("../util/colors"); + const colors = require("colors"); const CherryPickUtil = require("../util/cherry_pick_util"); const GitUtil = require("../util/git_util"); diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index 3167bdda9..b694995d1 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -106,7 +106,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { // TODO: For now, we will always create a merge commit. We should be able // to control whether FF is allowed/required for meta and sub repos. - const colors = require("../util/colors"); + const colors = require("colors"); const MergeUtil = require("../util/merge_util"); const GitUtil = require("../util/git_util"); diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 2ab563b15..9777621a3 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -77,7 +77,7 @@ exports.configureParser = function (parser) { * @param {String} args.path */ exports.executeableSubcommand = co.wrap(function *(args) { - const colors = require("../util/colors"); + const colors = require("colors"); const DoWorkQueue = require("../util/do_work_queue"); const GitUtil = require("../util/git_util"); diff --git a/node/lib/cmd/rebase.js b/node/lib/cmd/rebase.js index 366adc4c8..dced3fb76 100644 --- a/node/lib/cmd/rebase.js +++ b/node/lib/cmd/rebase.js @@ -81,7 +81,7 @@ exports.configureParser = function (parser) { exports.executeableSubcommand = co.wrap(function *(args) { // TODO: add applicable `git rebase` options. - const colors = require("../util/colors"); + const colors = require("colors"); const RebaseUtil = require("../util/rebase_util"); const GitUtil = require("../util/git_util"); diff --git a/node/lib/cmd/stash.js b/node/lib/cmd/stash.js index 2e4bbba18..2d601f6ee 100644 --- a/node/lib/cmd/stash.js +++ b/node/lib/cmd/stash.js @@ -165,7 +165,7 @@ const doDrop = co.wrap(function *(args) { * @param {Object} args */ exports.executeableSubcommand = function (args) { - const colors = require("../util/colors"); + const colors = require("colors"); switch(args.type) { case "pop" : return doPop(args); diff --git a/node/lib/cmd/submodule.js b/node/lib/cmd/submodule.js index 95905b29d..375fb04eb 100644 --- a/node/lib/cmd/submodule.js +++ b/node/lib/cmd/submodule.js @@ -183,7 +183,7 @@ const doStatusCommand = co.wrap(function *(paths, verbose) { }); const doFindCommand = co.wrap(function *(path, metaCommittish, subCommittish) { - const colors = require("../util/colors"); + const colors = require("colors"); const GitUtil = require("../util/git_util"); const LogUtil = require("../util/log_util"); diff --git a/node/lib/util/add.js b/node/lib/util/add.js index 933630056..fb163d4c0 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); diff --git a/node/lib/util/add_submodule.js b/node/lib/util/add_submodule.js index f32d82561..8377c7c04 100644 --- a/node/lib/util/add_submodule.js +++ b/node/lib/util/add_submodule.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const NodeGit = require("nodegit"); const GitUtil = require("./git_util"); diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 9cab7a572..a7c62f7c7 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -35,7 +35,7 @@ */ const assert = require("chai").assert; const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const NodeGit = require("nodegit"); const DoWorkQueue = require("../util/do_work_queue"); diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 1c107625c..3c94171a9 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const mkdirp = require("mkdirp"); const NodeGit = require("nodegit"); const path = require("path"); diff --git a/node/lib/util/close_util.js b/node/lib/util/close_util.js index 4ed98f308..97d43b622 100644 --- a/node/lib/util/close_util.js +++ b/node/lib/util/close_util.js @@ -33,7 +33,7 @@ const assert = require("chai").assert; const NodeGit = require("nodegit"); const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const Hook = require("../util/hook"); const SparseCheckoutUtil = require("../util/sparse_checkout_util"); diff --git a/node/lib/util/colors.js b/node/lib/util/colors.js deleted file mode 100644 index 98d9b7cee..000000000 --- a/node/lib/util/colors.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2018, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -/** - * This is a wrapper around the 'colors' module that checks if stdout - * is a tty before writing any colored text. Of course, this is sort - * of bogus: maybe stdout is a TTY but we're writing to stderr which - * isn't (or, less seriously but more likely, visa-versa). But it is - * the least-intrusive thing we can do to make colors even sort of - * respect non-TTY output, so it'll do for now. - */ - - -const colors = require("colors"); - -function identity(str) { - return str; -} - -function wrap(fn) { - if (process.stdout.isTTY) { - return fn; - } else { - return identity; - } -} - -for (let fn in colors) { - exports[fn] = wrap(colors[fn]); -} -for (let fn in colors.styles) { - exports[fn] = wrap(colors[fn]); -} - diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 496d1c38c..bf0e79e43 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -39,7 +39,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 4e67333da..4ef21e1a9 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -37,7 +37,7 @@ const assert = require("chai").assert; const ChildProcess = require("child-process-promise"); const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 00ca3c215..91e7c0252 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -33,7 +33,7 @@ const assert = require("chai").assert; const ChildProcess = require("child-process-promise"); const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const NodeGit = require("nodegit"); const Checkout = require("./checkout"); diff --git a/node/lib/util/open.js b/node/lib/util/open.js index 48a6b3010..14f35f82d 100644 --- a/node/lib/util/open.js +++ b/node/lib/util/open.js @@ -34,7 +34,7 @@ */ const assert = require("chai").assert; const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const NodeGit = require("nodegit"); const GitUtil = require("./git_util"); diff --git a/node/lib/util/pull.js b/node/lib/util/pull.js index ea91c0a34..5b3d4993f 100644 --- a/node/lib/util/pull.js +++ b/node/lib/util/pull.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const NodeGit = require("nodegit"); const ConfigUtil = require("./config_util"); diff --git a/node/lib/util/push.js b/node/lib/util/push.js index b24259d43..94ca0bcbd 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -36,7 +36,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const NodeGit = require("nodegit"); const DoWorkQueue = require("./do_work_queue"); diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index 28d441cd2..5b525dd7c 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const NodeGit = require("nodegit"); const Checkout = require("./checkout"); diff --git a/node/lib/util/repo_ast_util.js b/node/lib/util/repo_ast_util.js index c56f0a675..a330aca2f 100644 --- a/node/lib/util/repo_ast_util.js +++ b/node/lib/util/repo_ast_util.js @@ -36,7 +36,7 @@ */ const assert = require("chai").assert; -const colors = require("../util/colors"); +const colors = require("colors"); const deeper = require("deeper"); const RepoAST = require("../util/repo_ast"); diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 06be1b75b..af5ff9d43 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const NodeGit = require("nodegit"); const GitUtil = require("./git_util"); diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index 9390fe01a..4b2465f36 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const NodeGit = require("nodegit"); const path = require("path"); const rimraf = require("rimraf"); diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 3f198d2d7..66622ebcf 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -36,7 +36,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../util/colors"); +const colors = require("colors"); const NodeGit = require("nodegit"); const fs = require("fs-promise"); const path = require("path"); diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index 949843fad..d7faba1ea 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../../lib/util/colors"); +const colors = require("colors"); const NodeGit = require("nodegit"); const ConflictUtil = require("../../lib/util/conflict_util"); diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index 481f81e71..b8930b05f 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../../lib/util/colors"); +const colors = require("colors"); const MergeUtil = require("../../lib//util/merge_util"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js index 3c2c0f9b3..03326d3bb 100644 --- a/node/test/util/print_status_util.js +++ b/node/test/util/print_status_util.js @@ -31,7 +31,7 @@ "use strict"; const assert = require("chai").assert; -const colors = require("../../lib/util/colors"); +const colors = require("colors"); const NodeGit = require("nodegit"); const RepoStatus = require("../../lib/util/repo_status"); diff --git a/node/test/util/rebase_util.js b/node/test/util/rebase_util.js index 84c416e63..568f07f81 100644 --- a/node/test/util/rebase_util.js +++ b/node/test/util/rebase_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../../lib/util/colors"); +const colors = require("colors"); const RebaseUtil = require("../../lib/util/rebase_util"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); diff --git a/node/test/util/submodule_rebase_util.js b/node/test/util/submodule_rebase_util.js index c676dc53b..e187bb395 100644 --- a/node/test/util/submodule_rebase_util.js +++ b/node/test/util/submodule_rebase_util.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("../../lib/util/colors"); +const colors = require("colors"); const NodeGit = require("nodegit"); const RepoAST = require("../../lib/util/repo_ast"); From aafeb85296a308c06619eb289005aed493712406 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 23 Aug 2018 10:17:15 -0400 Subject: [PATCH 199/402] replace NodeGit.Repository.defaultSignature with our own function NodeGit's occasionally returns unknown@example.com, for unknown reasons, and this is bad. --- node/lib/util/commit.js | 7 ++++--- node/lib/util/config_util.js | 24 ++++++++++++++++++++++++ node/lib/util/git_util.js | 2 +- node/lib/util/merge_util.js | 9 +++++---- node/lib/util/stash_util.js | 9 +++++---- node/lib/util/submodule_rebase_util.js | 5 +++-- node/lib/util/test_util.js | 8 +++++--- node/lib/util/write_repo_ast_util.js | 5 +++-- 8 files changed, 50 insertions(+), 19 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index bf0e79e43..748c12bc7 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -44,6 +44,7 @@ const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); +const ConfigUtil = require("./config_util"); const DoWorkQueue = require("../util/do_work_queue"); const DiffUtil = require("./diff_util"); const GitUtil = require("./git_util"); @@ -641,7 +642,7 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message) { // Create a commit with this tree. - const sig = repo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(repo); const parents = [headCommit]; const commitId = yield NodeGit.Commit.create( repo, @@ -2044,7 +2045,7 @@ exports.doCommitCommand = co.wrap(function *(repo, // If 'interactive' mode is requested, ask the user to specify which // repos are committed and with what commit messages. - const sig = repo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(repo); const prompt = exports.formatSplitCommitEditorPrompt(repoStatus, sig, null, @@ -2139,7 +2140,7 @@ exports.doAmendCommand = co.wrap(function *(repo, const subsToAmend = amendStatus.subsToAmend; const head = yield repo.getHeadCommit(); - const defaultSig = repo.defaultSignature(); + const defaultSig = yield ConfigUtil.defaultSignature(repo); const headMeta = exports.getCommitMetaData(head); let subMessages = null; if (interactive) { diff --git a/node/lib/util/config_util.js b/node/lib/util/config_util.js index 6077f2987..41e8b860d 100644 --- a/node/lib/util/config_util.js +++ b/node/lib/util/config_util.js @@ -33,6 +33,7 @@ const assert = require("chai").assert; const co = require("co"); const NodeGit = require("nodegit"); +const UserError = require("../../lib/util/user_error"); /** * This module contains methods for interacting with git configuration entries. @@ -83,3 +84,26 @@ exports.configIsTrue = co.wrap(function*(repo, configVar) { }); +/** + * Returns the default Signature for a repo. Replaces repo.defaultSignature, + * which occasionally returns unknown@example.com for unknown reasons. + * @async + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} configVar + * @return {Bool|null} + * @throws if the configuration variable doesn't exist +*/ +exports.defaultSignature = co.wrap(function*(repo) { + assert.instanceOf(repo, NodeGit.Repository); + + const config = yield repo.config(); + const email = yield exports.getConfigString(config, "user.email"); + const name = yield exports.getConfigString(config, "user.name"); + if (name && email) { + const now = new Date(); + const tz = now.getTimezoneOffset(); + return NodeGit.Signature.create(name, email, now.getTime() / 1000, tz); + } + throw new UserError("Git config vars user.email and user.name are unset"); +}); + diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 4ef21e1a9..5ad13ae07 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -88,7 +88,7 @@ exports.createBranchFromHead = co.wrap(function *(repo, branchName) { return yield repo.createBranch(branchName, head, 0, - repo.defaultSignature(), + yield ConfigUtil.defaultSignature(repo), "git-meta branch"); }); diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 91e7c0252..cad8ee590 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -39,6 +39,7 @@ const NodeGit = require("nodegit"); const Checkout = require("./checkout"); const CherryPickUtil = require("./cherry_pick_util"); const Commit = require("./commit"); +const ConfigUtil = require("./config_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); const Open = require("./open"); @@ -137,7 +138,7 @@ exports.fastForwardMerge = co.wrap(function *(repo, mode, commit, message) { // Then, generate a new commit that has the previous HEAD and commit to // merge as children. - const sig = repo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(repo); const tree = yield commit.getTree(); const id = yield NodeGit.Commit.create( repo, @@ -199,7 +200,7 @@ const mergeSubmodules = co.wrap(function *(repo, conflicts: {}, commits: {}, }; - const sig = repo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(repo); const fetcher = yield opener.fetcher(); const mergeSubmodule = co.wrap(function *(name) { const subRepo = yield opener.getSubrepo(name); @@ -393,7 +394,7 @@ ${colors.red(commitSha)}.`); return result; } - const sig = repo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(repo); const changeIndex = yield NodeGit.Merge.commits(repo, head, commit, []); const changes = @@ -566,7 +567,7 @@ exports.continue = co.wrap(function *(repo) { } const treeId = yield index.writeTreeTo(repo); - const sig = repo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(repo); const head = yield repo.getHeadCommit(); const mergeHead = yield repo.getCommit(seq.target.sha); const metaCommit = yield repo.createCommit("HEAD", diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index af5ff9d43..764c9b936 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -35,6 +35,7 @@ const co = require("co"); const colors = require("colors"); const NodeGit = require("nodegit"); +const ConfigUtil = require("./config_util"); const GitUtil = require("./git_util"); const Open = require("./open"); const PrintStatusUtil = require("./print_status_util"); @@ -134,7 +135,7 @@ exports.save = co.wrap(function *(repo, status, includeUntracked, message) { const subChanges = {}; // name to TreeUtil.Change const subRepos = {}; // name to submodule open repo - const sig = repo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(repo); // First, we process the submodules. If a submodule is open and dirty, // we'll create the stash commits in its repo, populate `subResults` with @@ -242,7 +243,7 @@ exports.createReflogIfNeeded = co.wrap(function *(repo, const log = yield NodeGit.Reflog.read(repo, reference); if (0 === log.entrycount()) { const id = NodeGit.Oid.fromString(sha); - log.append(id, repo.defaultSignature(), message); + log.append(id, yield ConfigUtil.defaultSignature(repo), message); yield log.write(); } }); @@ -511,7 +512,7 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, if (indexOnly) { const index = yield repo.index(); const tree = yield index.writeTree(); - const sig = repo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(repo); const subCommit = yield repo.createCommit(null, sig, sig, message, tree, []); return subCommit.tostrS(); @@ -536,7 +537,7 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, } } - let sig = repo.defaultSignature(); + let sig = yield ConfigUtil.defaultSignature(repo); if (incrementTimestamp && null !== head) { sig = NodeGit.Signature.create(sig.name(), sig.email(), diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index 4b2465f36..20c082888 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -37,6 +37,7 @@ const NodeGit = require("nodegit"); const path = require("path"); const rimraf = require("rimraf"); +const ConfigUtil = require("./config_util"); const GitUtil = require("./git_util"); const DoWorkQueue = require("./do_work_queue"); const RebaseFileUtil = require("./rebase_file_util"); @@ -92,7 +93,7 @@ exports.makeCommit = co.wrap(function *(repo, commit) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); - const defaultSig = repo.defaultSignature(); + const defaultSig = yield ConfigUtil.defaultSignature(repo); const metaCommit = yield repo.createCommitOnHead([], commit.author(), defaultSig, @@ -167,7 +168,7 @@ exports.processRebase = co.wrap(function *(repo, rebase, op) { commits: {}, conflictedCommit: null, }; - const signature = repo.defaultSignature(); + const signature = yield ConfigUtil.defaultSignature(repo); while (null !== op) { const index = yield repo.index(); if (index.hasConflicts()) { diff --git a/node/lib/util/test_util.js b/node/lib/util/test_util.js index cdb52540e..882e3e437 100644 --- a/node/lib/util/test_util.js +++ b/node/lib/util/test_util.js @@ -34,6 +34,8 @@ * This module contains methods used in testing other git-meta components. */ +const ConfigUtil = require("./config_util"); + const assert = require("chai").assert; const co = require("co"); const fs = require("fs-promise"); @@ -106,7 +108,7 @@ exports.createSimpleRepository = co.wrap(function *(repoPath) { const fileName = "README.md"; const filePath = path.join(repoPath, fileName); yield fs.writeFile(filePath, ""); - const sig = repo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(repo); yield repo.createCommitOnHead([fileName], sig, sig, "first commit"); return repo; }); @@ -130,7 +132,7 @@ exports.createSimpleRepositoryOnBranch = co.wrap(function *(branchName) { const repo = yield exports.createSimpleRepository(); const commit = yield repo.getHeadCommit(); - const sig = repo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(repo); const publicBranch = yield repo.createBranch(branchName, commit, 0, sig); yield repo.setHead(publicBranch.name()); @@ -211,7 +213,7 @@ exports.makeCommit = co.wrap(function *(repo, files) { assert.isArray(files); files.forEach((name, i) => assert.isString(name, i)); - const sig = repo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(repo); const commitId = yield repo.createCommitOnHead(files, sig, sig, diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index 99ea61021..efbc3f445 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -45,6 +45,7 @@ const mkdirp = require("mkdirp"); const NodeGit = require("nodegit"); const path = require("path"); +const ConfigUtil = require("./config_util"); const ConflictUtil = require("./conflict_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); @@ -210,7 +211,7 @@ exports.writeCommits = co.wrap(function *(oldCommitMap, let newCommitMap = {}; // from new to old sha - const sig = repo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(repo); const commitObjs = {}; // map from new id to `Commit` object @@ -411,7 +412,7 @@ git -C '${repo.workdir()}' checkout ${toCheckout} } const notes = ast.notes; - const sig = repo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(repo); for (let notesRef in notes) { const commits = notes[notesRef]; for (let commit in commits) { From ba0971746639a15636a643aeb8b831f78aa71470 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 23 Aug 2018 10:56:28 -0400 Subject: [PATCH 200/402] test --- node/test/util/config_util.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/node/test/util/config_util.js b/node/test/util/config_util.js index 80019b3c6..ed41be18d 100644 --- a/node/test/util/config_util.js +++ b/node/test/util/config_util.js @@ -93,4 +93,13 @@ describe("configIsTrue", function () { })); }); }); + +describe("defaultSignature", function () { + it("works", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const actual = yield ConfigUtil.defaultSignature(repo); + assert.equal(actual.toString(), + repo.defaultSignature().toString()); + })); +}); }); From d59c25bbf73e7d3808bfa4ca1dfbc705205c4060 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 23 Aug 2018 11:51:03 -0400 Subject: [PATCH 201/402] missed defaultSignature calls --- node/lib/util/commit.js | 2 +- node/lib/util/merge_util.js | 2 +- node/lib/util/test_util.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 748c12bc7..dec53fe53 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -481,7 +481,7 @@ exports.commit = co.wrap(function *(metaRepo, assert(null !== message || undefined !== subMessages, "if no meta message, sub messages must be specified"); - const signature = metaRepo.defaultSignature(); + const signature = yield ConfigUtil.defaultSignature(metaRepo); const submodules = metaStatus.submodules; // Commit submodules. If any changes, remember this so we know to generate diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index cad8ee590..137f5c040 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -522,7 +522,7 @@ exports.continue = co.wrap(function *(repo) { `Submodule ${colors.red(subPath)} has conflicts.\n`; return; // RETURN } - const sig = subRepo.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(subRepo); const subSeq = yield getSequencerIfMerge(subRepo.path()); if (null === subSeq) { // There is no merge in this submodule, but if there are staged diff --git a/node/lib/util/test_util.js b/node/lib/util/test_util.js index 882e3e437..a28da600e 100644 --- a/node/lib/util/test_util.js +++ b/node/lib/util/test_util.js @@ -263,7 +263,7 @@ exports.makeBareCopy = co.wrap(function *(repo, path) { // Then create all the branches that weren't copied initially. const refs = yield repo.getReferences(NodeGit.Reference.TYPE.LISTALL); - const sig = bare.defaultSignature(); + const sig = yield ConfigUtil.defaultSignature(bare); for (let i = 0; i < refs.length; ++i) { const ref = refs[i]; const shorthand = ref.shorthand(); From 9cd3d400389dabfca4f5058436c156dd3f55172b Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Sun, 16 Sep 2018 17:56:26 -0400 Subject: [PATCH 202/402] Whoops, restore tracking of temp files so they qre automatically cleaned up --- node/lib/util/test_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/test_util.js b/node/lib/util/test_util.js index a28da600e..4d47c5ba5 100644 --- a/node/lib/util/test_util.js +++ b/node/lib/util/test_util.js @@ -41,7 +41,7 @@ const co = require("co"); const fs = require("fs-promise"); const path = require("path"); const NodeGit = require("nodegit"); -const temp = require("temp"); +const temp = require("temp").track(); /** * Return the path to a newly-created temporary directory. From 71198735f1f34a19d52b57813713587edf36c4ec Mon Sep 17 00:00:00 2001 From: blevz Date: Mon, 17 Sep 2018 19:11:52 -0400 Subject: [PATCH 203/402] Modify stash to use default behavior for pop and reinstateIndex when --index is passed --- node/lib/cmd/stash.js | 10 +++- node/lib/util/stash_util.js | 15 +++-- node/test/util/stash_util.js | 107 +++++++++++++++++++---------------- 3 files changed, 78 insertions(+), 54 deletions(-) diff --git a/node/lib/cmd/stash.js b/node/lib/cmd/stash.js index 2d601f6ee..70c4892de 100644 --- a/node/lib/cmd/stash.js +++ b/node/lib/cmd/stash.js @@ -83,6 +83,13 @@ default`, action: "storeConst", constant: true, }); + + parser.addArgument("--index", { + help: `Reinstate not only the working tree's changes, but also \ +index's ones`, + action: "storeConst", + constant: true, + }); }; const doPop = co.wrap(function *(args) { @@ -91,7 +98,8 @@ const doPop = co.wrap(function *(args) { const repo = yield GitUtil.getCurrentRepo(); const index = (null === args.stash) ? 0 : args.stash; - yield StashUtil.pop(repo, index); + const reinstateIndex = args.index || false; + yield StashUtil.pop(repo, index, reinstateIndex); }); function cleanSubs(status, includeUntracked) { diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 764c9b936..71fc5a0b9 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -287,9 +287,10 @@ exports.setStashHead = co.wrap(function *(repo, sha) { * * @param {NodeGit.Repository} repo * @param {String} id - * @return {Boolean} + * @param {Boolean} reinstateIndex + * @return {Object} submodule name to stashed commit */ -exports.apply = co.wrap(function *(repo, id) { +exports.apply = co.wrap(function *(repo, id, reinstateIndex) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(id); @@ -338,10 +339,12 @@ ${colors.red(name)}`); // And then apply it. const APPLY_FLAGS = NodeGit.Stash.APPLY_FLAGS; + const flag = reinstateIndex ? + APPLY_FLAGS.APPLY_REINSTATE_INDEX : APPLY_FLAGS.APPLY_DEFAULT; try { yield NodeGit.Stash.pop(subRepo, 0, { - flags: APPLY_FLAGS.APPLY_REINSTATE_INDEX, + flags: flag, }); } catch (e) { @@ -434,13 +437,15 @@ Dropped ${colors.green(refText)} ${colors.blue(stashSha)}`); * remove `refs/meta-stash`. * * @param {NodeGit.Repository} repo + * @param {int} index + * @param {Boolean} reinstateIndex */ -exports.pop = co.wrap(function *(repo, index) { +exports.pop = co.wrap(function *(repo, index, reinstateIndex) { assert.instanceOf(repo, NodeGit.Repository); assert.isNumber(index); const stashSha = yield getStashSha(repo, index); - const applyResult = yield exports.apply(repo, stashSha); + const applyResult = yield exports.apply(repo, stashSha, reinstateIndex); const status = yield StatusUtil.getRepoStatus(repo); process.stdout.write(PrintStatusUtil.printRepoStatus(status, "")); diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index e700f58b9..f7e2c735d 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -481,6 +481,7 @@ x=U:Fmeta-stash=s;Cstash#s-2 s=Sa:ss; result: { s: "ss", }, + reinstateIndex: true, expected: ` x=E:Os Bss=ss! C*#ss-1,sis README.md=bar! @@ -492,29 +493,34 @@ x=E:Os Bss=ss! }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; - const applier = co.wrap(function *(repos, mapping) { - const repo = repos.x; - assert.property(mapping.reverseCommitMap, c.sha); - const sha = mapping.reverseCommitMap[c.sha]; - const result = yield StashUtil.apply(repo, sha); - if (null === c.result) { - assert.isNull(result); - } - else { - const expected = {}; - Object.keys(c.result).forEach(name => { - const sha = c.result[name]; - expected[name] = mapping.reverseCommitMap[sha]; - }); - assert.deepEqual(result, expected); - } + const reinstateIndexValues = (undefined === c.reinstateIndex) ? + [false, true] : [c.reinstateIndex]; + reinstateIndexValues.forEach(function(reinstateIndex) { + const applier = co.wrap(function* (repos, mapping) { + const repo = repos.x; + assert.property(mapping.reverseCommitMap, c.sha); + const sha = mapping.reverseCommitMap[c.sha]; + const result = yield StashUtil.apply(repo, sha, + reinstateIndex); + if (null === c.result) { + assert.isNull(result); + } + else { + const expected = {}; + Object.keys(c.result).forEach(name => { + const sha = c.result[name]; + expected[name] = mapping.reverseCommitMap[sha]; + }); + assert.deepEqual(result, expected); + } + }); + it(caseName, co.wrap(function* () { + yield RepoASTTestUtil.testMultiRepoManipulator(c.state, + c.expected, + applier, + c.fails); + })); }); - it(caseName, co.wrap(function *() { - yield RepoASTTestUtil.testMultiRepoManipulator(c.state, - c.expected, - applier, - c.fails); - })); }); }); describe("removeStash", function () { @@ -643,35 +649,40 @@ x=E:Fmeta-stash=2; }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; - const popper = co.wrap(function *(repos, mapping) { - const repo = repos.x; - const revMap = mapping.reverseCommitMap; - yield writeLog(repo, revMap, c.log || []); + const reinstateIndexValues = (undefined === c.reinstateIndex) ? + [false, true] : [c.reinstateIndex]; + reinstateIndexValues.forEach(function(reinstateIndex){ + const popper = co.wrap(function *(repos, mapping) { + const repo = repos.x; + const revMap = mapping.reverseCommitMap; + yield writeLog(repo, revMap, c.log || []); - // set up stash refs in submodules, if requested + // set up stash refs in submodules, if requested - const subStash = c.subStash || {}; - for (let subName in subStash) { - const sha = revMap[subStash[subName]]; - const subRepo = yield SubmoduleUtil.getRepo(repo, subName); - const refName = `refs/sub-stash/${sha}`; - NodeGit.Reference.create(subRepo, - refName, - NodeGit.Oid.fromString(sha), - 1, - "test stash"); - } - const index = (undefined === c.index) ? 0 : c.index; - yield StashUtil.pop(repo, index); - }); - it(caseName, co.wrap(function *() { - yield RepoASTTestUtil.testMultiRepoManipulator(c.init, - c.expected, - popper, - c.fails, { - expectedTransformer: refMapper, + const subStash = c.subStash || {}; + for (let subName in subStash) { + const sha = revMap[subStash[subName]]; + const subRepo = yield SubmoduleUtil.getRepo(repo, + subName); + const refName = `refs/sub-stash/${sha}`; + NodeGit.Reference.create(subRepo, + refName, + NodeGit.Oid.fromString(sha), + 1, + "test stash"); + } + const index = (undefined === c.index) ? 0 : c.index; + yield StashUtil.pop(repo, index, reinstateIndex); }); - })); + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.init, + c.expected, + popper, + c.fails, { + expectedTransformer: refMapper, + }); + })); + }); }); }); From af3651f36af37c332bbfdc50156b292419a0dbb0 Mon Sep 17 00:00:00 2001 From: blevz Date: Tue, 18 Sep 2018 11:33:35 -0400 Subject: [PATCH 204/402] Add stash apply --- node/lib/cmd/stash.js | 13 ++++++++++++- node/lib/util/stash_util.js | 18 ++++++++++-------- node/test/util/stash_util.js | 2 +- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/node/lib/cmd/stash.js b/node/lib/cmd/stash.js index 70c4892de..3edd90da5 100644 --- a/node/lib/cmd/stash.js +++ b/node/lib/cmd/stash.js @@ -99,7 +99,17 @@ const doPop = co.wrap(function *(args) { const repo = yield GitUtil.getCurrentRepo(); const index = (null === args.stash) ? 0 : args.stash; const reinstateIndex = args.index || false; - yield StashUtil.pop(repo, index, reinstateIndex); + yield StashUtil.pop(repo, index, reinstateIndex, true); +}); + +const doApply = co.wrap(function *(args){ + const GitUtil = require("../../lib/util/git_util"); + const StashUtil = require("../../lib/util/stash_util"); + + const repo = yield GitUtil.getCurrentRepo(); + const index = (null === args.stash) ? 0 : args.stash; + const reinstateIndex = args.index || false; + yield StashUtil.pop(repo, index, reinstateIndex, false); }); function cleanSubs(status, includeUntracked) { @@ -177,6 +187,7 @@ exports.executeableSubcommand = function (args) { switch(args.type) { case "pop" : return doPop(args); + case "apply": return doApply(args); case "save": return doSave(args); case "list": return doList(args); case "drop": return doDrop(args); diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 71fc5a0b9..0343674b3 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -440,7 +440,7 @@ Dropped ${colors.green(refText)} ${colors.blue(stashSha)}`); * @param {int} index * @param {Boolean} reinstateIndex */ -exports.pop = co.wrap(function *(repo, index, reinstateIndex) { +exports.pop = co.wrap(function *(repo, index, reinstateIndex, shouldDrop) { assert.instanceOf(repo, NodeGit.Repository); assert.isNumber(index); @@ -453,15 +453,17 @@ exports.pop = co.wrap(function *(repo, index, reinstateIndex) { // If the application succeeded, remove it. if (null !== applyResult) { - yield exports.removeStash(repo, index); + if (shouldDrop) { + yield exports.removeStash(repo, index); - // Clean up sub-repo meta-refs + // Clean up sub-repo meta-refs - Object.keys(applyResult).forEach(co.wrap(function *(subName) { - const subRepo = yield SubmoduleUtil.getRepo(repo, subName); - const refName = makeSubRefName(applyResult[subName]); - NodeGit.Reference.remove(subRepo, refName); - })); + Object.keys(applyResult).forEach(co.wrap(function* (subName) { + const subRepo = yield SubmoduleUtil.getRepo(repo, subName); + const refName = makeSubRefName(applyResult[subName]); + NodeGit.Reference.remove(subRepo, refName); + })); + } } else { throw new UserError(`\ diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index f7e2c735d..d0978e921 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -672,7 +672,7 @@ x=E:Fmeta-stash=2; "test stash"); } const index = (undefined === c.index) ? 0 : c.index; - yield StashUtil.pop(repo, index, reinstateIndex); + yield StashUtil.pop(repo, index, reinstateIndex, true); }); it(caseName, co.wrap(function *() { yield RepoASTTestUtil.testMultiRepoManipulator(c.init, From 1f6da2ebbb2fb2853b859e6e0dd34381bcff0fb9 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 4 Oct 2017 11:37:14 -0400 Subject: [PATCH 205/402] Initial implementation of tool to stitch repos standalone script in `lib/stitch.js` --- node/lib/stitch.js | 148 +++ node/lib/util/git_util.js | 9 +- node/lib/util/shorthand_parser_util.js | 2 + node/lib/util/stitch_util.js | 896 ++++++++++++++++ node/test/util/git_util.js | 6 +- node/test/util/stitch_util.js | 1296 ++++++++++++++++++++++++ node/test/util/tree_util.js | 33 + 7 files changed, 2385 insertions(+), 5 deletions(-) create mode 100755 node/lib/stitch.js create mode 100644 node/lib/util/stitch_util.js create mode 100644 node/test/util/stitch_util.js diff --git a/node/lib/stitch.js b/node/lib/stitch.js new file mode 100755 index 000000000..dc106dd4a --- /dev/null +++ b/node/lib/stitch.js @@ -0,0 +1,148 @@ +#!/usr/bin/env node +/* + * Copyright (c) 2017, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +const ArgumentParser = require("argparse").ArgumentParser; +const co = require("co"); + +const StitchUtil = require("./util/stitch_util"); + +const description = `Stitch together the specified meta-repo commitish in \ +the specified repo.`; + +const parser = new ArgumentParser({ + addHelp: true, + description: description +}); + +parser.addArgument(["--no-fetch"], { + required: false, + action: "storeConst", + constant: true, + defaultValue: false, + help: `If provided, assume commits are present and do not fetch.`, +}); + +parser.addArgument(["-t", "--target-branch"], { + required: false, + type: "string", + defaultValue: "master", + help: "Branch to update with committed ref; default is 'master'.", +}); + +parser.addArgument(["-j"], { + required: false, + type: "int", + help: "number of parallel operations, default 8", + defaultValue: 8, +}); + +parser.addArgument(["-c", "--commitish"], { + type: "string", + help: "meta-repo commit to stitch, default is HEAD", + defaultValue: "HEAD", + required: false, +}); + +parser.addArgument(["-u", "--url"], { + type: "string", + defaultValue: null, + help: `location of the origin repository where submodules are rooted, \ +required unless '--no-fetch' is specified`, + required: false, +}); + +parser.addArgument(["-r", "--repo"], { + type: "string", + help: "location of the repo, default is \".\"", + defaultValue: ".", +}); + +parser.addArgument(["-k", "--keep-as-submodule"], { + type: "string", + help: `submodules whose paths are matched by this regex are not stitched, \ +but are instead kept as submodules.`, + required: false, + defaultValue: null, +}); + +parser.addArgument(["--skip-empty"], { + required: false, + action: "storeConst", + constant: true, + defaultValue: false, + help: "Skip a commit if its tree would be the same as its first parent.", +}); + +parser.addArgument(["--root"], { + type: "string", + help: "When provided, run a *join* operation that creates a new history \ +joining those of the submodules under the specified 'root' path. \ +In this history those paths will be relative to 'root', i.e., they will not \ +be prefixed by it. This option implies '--skip-empty'.", +}); + +co(function *() { + const args = parser.parseArgs(); + const keepRegex = (null === args.keep_as_submodule) ? + null : + new RegExp(args.keep_as_submodule); + function keep(name) { + return null !== keepRegex && null !== keepRegex.exec(name); + } + const options = { + numParallel: args.j, + keepAsSubmodule: keep, + fetch: !args.no_fetch, + skipEmpty: args.skip_empty || (null !== args.root), + }; + if (!args.no_fetch && null === args.url) { + console.error("URL is required unless '--no-fetch'"); + process.exit(-1); + } + if (null !== args.url) { + options.url = args.url; + } + if (null !== args.root) { + console.log(`Joining from ${args.root}`); + options.joinRoot = args.root; + } + try { + yield StitchUtil.stitch(args.repo, + args.commitish, + args.target_branch, + options); + } + catch (e) { + console.error(e.stack); + process.exit(-1); + } +}); diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 5ad13ae07..177e9f4ee 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -547,12 +547,14 @@ git -C '${repo.path()}' fetch -q '${remoteName}' '${branch}'`; /** * Fetch the specified `sha` from the specified `url` into the specified - * `repo`, if it does not already exist in `repo`. + * `repo`, if it does not already exist in `repo`; return true if a fetch + * happened and false if the commit already existed in `repo`. * * @async * @param {NodeGit.Repository} repo * @param {String} url * @param {String} sha + * @return {Bool} */ exports.fetchSha = co.wrap(function *(repo, url, sha) { assert.instanceOf(repo, NodeGit.Repository); @@ -563,7 +565,7 @@ exports.fetchSha = co.wrap(function *(repo, url, sha) { try { yield repo.getCommit(sha); - return; // RETURN + return false; // RETURN } catch (e) { } @@ -571,11 +573,12 @@ exports.fetchSha = co.wrap(function *(repo, url, sha) { const execString = `git -C '${repo.path()}' fetch -q '${url}' ${sha}`; try { yield ChildProcess.exec(execString); - return yield repo.getCommit(sha); + yield repo.getCommit(sha); } catch (e) { throw new UserError(e.message); } + return true; }); diff --git a/node/lib/util/shorthand_parser_util.js b/node/lib/util/shorthand_parser_util.js index 935bb27f9..c5799d3fb 100644 --- a/node/lib/util/shorthand_parser_util.js +++ b/node/lib/util/shorthand_parser_util.js @@ -181,6 +181,8 @@ const SequencerState = RepoAST.SequencerState; * be a submodule with a url 'baz' at commit '1' * S:Chello#2-1,3 -- a commit, "2", with two parents: "1" and "3", * and a message of "hello" + * S:C*#2-1,3 -- same as above, but ignore the commit message + * during validation * S:Rorigin=/foo.git -- 'S' repo with an origin of /foo.git * S:Rorigin=/foo.git master=1 -- same as above but with remote branch * -- named 'master' pointing to commit 1 diff --git a/node/lib/util/stitch_util.js b/node/lib/util/stitch_util.js new file mode 100644 index 000000000..29a024024 --- /dev/null +++ b/node/lib/util/stitch_util.js @@ -0,0 +1,896 @@ +/* + * Copyright (c) 2017, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const NodeGit = require("nodegit"); + +const Commit = require("./commit"); +const ConfigUtil = require("./config_util"); +const DoWorkQueue = require("./do_work_queue"); +const GitUtil = require("./git_util"); +const SubmoduleConfigUtil = require("./submodule_config_util"); +const SubmoduleUtil = require("./submodule_util"); +const SyntheticBranchUtil = require("./synthetic_branch_util"); +const TreeUtil = require("./tree_util"); + +const FILEMODE = NodeGit.TreeEntry.FILEMODE; + +// This constant defines the maximum number of simple, multi-threaded parallel +// operations we'll perform. We allow the user to configure the number of +// parallel operations that we must shell out for, but this value is just to +// prevent us from running out of JavaScript heap. + +const maxParallel = 1000; + +/** + * Return a string having the value of the specified `sha` with the "/" + * character inserted between the second and third characters of `sha`. The + * behavior is undefined unless `sha` is at least three characters long. + * + * @param {String} sha + * @return {String} + */ +exports.splitSha = function (sha) { + const pre = sha.slice(0, 2); + const post = sha.slice(2); + return `${pre}/${post}`; +}; + +/** + * The name of the note used to record conversion information. + * + * @property {String} + */ +exports.convertedNoteRef = "refs/notes/stitched/converted"; + +/** + * Write a note in the specified `repo` indicating that the commit having the + * specified `originalSha` was stitched into the specified `stitchedSha`, if + * provided, or that it was not processed (it's in completely skipped history) + * otherwise. + * + * @param {NodeGit.Repository} repo + * @param {String} originalSha + * @param {String|null} stitchedSha + */ +exports.writeConvertedNote = co.wrap(function *(repo, + originalSha, + stitchedSha) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(originalSha); + if (null !== stitchedSha) { + assert.isString(stitchedSha); + } + const content = null === stitchedSha ? "" : stitchedSha; + const sig = yield ConfigUtil.defaultSignature(repo); + yield NodeGit.Note.create(repo, + exports.convertedNoteRef, + sig, + sig, + NodeGit.Oid.fromString(originalSha), + content, + 1); +}); + +/** + * Return the commit message to use for a stitch commit coming from the + * specified `metaCommit` that introduces the specified `subCommits`. + * + * @param {NodeGit.Commit} metaCommit + * @param {Object} subCommits from name to NodeGit.Commit + * @return {String} + */ +exports.makeStitchCommitMessage = function (metaCommit, subCommits) { + assert.instanceOf(metaCommit, NodeGit.Commit); + assert.isObject(subCommits); + + const metaAuthor = metaCommit.author(); + const metaName = metaAuthor.name(); + const metaEmail = metaAuthor.email(); + const metaWhen = metaAuthor.when(); + const metaTime = metaWhen.time(); + const metaOffset = metaWhen.offset(); + const metaMessage = metaCommit.message(); + let result = metaCommit.message(); + + // Add information from submodule commits that differs from the the + // meta-repo commit. When all info (author, time, message) in a sub commit + // matches that of the meta-repo commit, skip it completely. + + Object.keys(subCommits).forEach(subName => { + const commit = subCommits[subName]; + const author = commit.author(); + const name = author.name(); + const email = author.email(); + let authorText = ""; + if (name !== metaName || email !== metaEmail) { + authorText = `Author: ${name} <${email}>\n`; + } + const when = author.when(); + let whenText = ""; + if (when.time() !== metaTime || when.offset() !== metaOffset) { + whenText = `Date: ${Commit.formatCommitTime(when)}\n`; + } + const message = commit.message(); + let messageText = ""; + if (message !== metaMessage) { + messageText = "\n" + message; + } + if ("" !== authorText || "" !== whenText || "" !== messageText) { + result += "\n"; + result += `From '${subName}'\n`; + result += authorText; + result += whenText; + result += messageText; + } + }); + return result; +}; + +/** + * The name of the note used to record conversion information. + * + * @property {String} + */ +exports.referenceNoteRef = "refs/notes/stitched/reference"; + + +/** + * Write a note recording the specified originating `metaRepoSha` and + * `subCommits` for the stitched commit having the specified + * `stitchedCommitSha` in the specified `repo`. + * + * @param {NodeGit.Repository} repo + * @param {String} stitchedCommitSha + * @param {String} metaRepoSha + * @param {Object} subCommits name to NodeGit.Commit + */ +exports.writeReferenceNote = co.wrap(function *(repo, + stitchedCommitSha, + metaRepoSha, + subCommits) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(stitchedCommitSha); + assert.isString(metaRepoSha); + assert.isObject(subCommits); + const object = { + metaRepoCommit: metaRepoSha, + submoduleCommits: {}, + }; + Object.keys(subCommits).forEach(name => { + object.submoduleCommits[name] = subCommits[name].id().tostrS(); + }); + const content = JSON.stringify(object, null, 4); + const sig = yield ConfigUtil.defaultSignature(repo); + yield NodeGit.Note.create(repo, + exports.referenceNoteRef, + sig, + sig, + NodeGit.Oid.fromString(stitchedCommitSha), + content, + 1); +}); + +/** + * Return true if the specified `newTree` and `originalTree` represent the same + * value and false otherwise. `newTree` represents the same value as + * `originalTree` if they have the same id, or if `null === originalTree` and + * `newTree` has no entries. + * + * @param {NodeGit.Tree} newTree + * @param {NodeGit.Tree|null} originalTree + * @return {Bool} + */ +exports.isTreeUnchanged = function (newTree, originalTree) { + assert.instanceOf(newTree, NodeGit.Tree); + if (null !== originalTree) { + assert.instanceOf(originalTree, NodeGit.Tree); + } + if (null === originalTree) { + return 0 === newTree.entries().length; + } + return newTree.id().tostrS() === originalTree.id().tostrS(); +}; + +/** + * From a map containing a shas mapped to sets of direct parents, and the + * specified starting `entry` sha, return a list of all shas ordered from least + * to most dependent, that is, no sha will appear in the list before any of its + * ancestors. If no relation exists between two shas, they will be ordered + * alphabetically. Note that it is valid for a sha to exist as a parent from a + * sha in `parentMap`, however, the behavior is undefined if there are entries + * in 'parentMap' that are not reachable from 'entry'. + * + * @param {String} entry + * @param {Object} parentMap from sha to Set of its direct parents + * @return {[String]} + */ +exports.listCommitsInOrder = function (entry, parentMap) { + assert.isString(entry); + assert.isObject(parentMap); + + // First, compute the generations of the commits. A generation '0' means + // that a commit has no parents. A generation '1' means that a commit + // depends only on commits with 0 parents, a generation N means that a + // commit depends only on commits with a generation less than N. + + const generations = {}; + let queue = [entry]; + while (0 !== queue.length) { + const next = queue[queue.length - 1]; + + // Exit if we've already computed this one; can happen if one gets into + // the queue more than once. + + if (next in generations) { + queue.pop(); + continue; // CONTINUE + } + let generation = 0; + const parents = parentMap[next] || []; + for (const parent of parents) { + const parentGeneration = generations[parent]; + if (undefined === parentGeneration) { + generation = undefined; + queue.push(parent); + } + else if (undefined !== generation) { + // If all parents computed thus far, recompute the max. It can + // not be less than or equal to any parent. + + generation = Math.max(generation, parentGeneration + 1); + } + } + if (undefined !== generation) { + // We were able to compute it, store and pop. + + generations[next] = generation; + queue.pop(); + } + } + + // Now we sort, placing lowest generation commits first. + + function compareCommits(a, b) { + const aGeneration = generations[a]; + const bGeneration = generations[b]; + if (aGeneration !== bGeneration) { + return aGeneration - bGeneration; // RETURN + } + + // 'a' can never be equal to 'b' because we're sorting keys. + + return a < b ? -1 : 1; + } + return Object.keys(parentMap).sort(compareCommits); +}; + +/** + * Return a map containing all converted SHAs in the specified `repo`. + */ +exports.listConvertedCommits = co.wrap(function *(repo) { + assert.instanceOf(repo, NodeGit.Repository); + + let ref; + try { + ref = yield NodeGit.Reference.lookup(repo, exports.convertedNoteRef); + } catch (e) { + // unfortunately, this is the only way to know the ref doesn't exist + return {}; + } + const result = {}; + const commit = yield repo.getCommit(ref.target()); + const tree = yield commit.getTree(); + const entries = tree.entries(); + const processEntry = co.wrap(function *(e) { + const blob = yield e.getBlob(); + const text = blob.toString(); + result[e.name()] = text === "" ? null : text; + }); + yield DoWorkQueue.doInParallel(entries, processEntry, maxParallel); + return result; +}); + +/** + * List, in order of least to most dependent, the specified `commit` and its + * ancestors in the specified `repo`. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + * @param {Object} convertedCommits map from sha to converted + * @return {[NodeGit.Commit]} + */ +exports.listCommitsToStitch = co.wrap(function *(repo, + commit, + convertedCommits) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(commit, NodeGit.Commit); + assert.isObject(convertedCommits); + + const toList = [commit]; + const allParents = {}; + const commitMap = {}; + + while (0 !== toList.length) { + const next = toList[toList.length - 1]; + const nextSha = next.id().tostrS(); + toList.pop(); + + // Skip processing commits we've seen. + + if (nextSha in allParents) { + continue; // CONTINUE + } + + // If it's converted, so, implicitly, are its parents. + + const converted = convertedCommits[nextSha]; + if (undefined !== converted) { + continue; // CONTINUE + } + const parents = yield next.getParents(); + const parentShas = []; + for (const parent of parents) { + toList.push(parent); + const parentSha = parent.id().tostrS(); + parentShas.push(parentSha); + } + allParents[nextSha] = parentShas; + commitMap[nextSha] = next; + } + const commitShas = exports.listCommitsInOrder(commit.id().tostrS(), + allParents); + return commitShas.map(sha => commitMap[sha]); +}); + +/** + * Return the `TreeUtilChange` object corresponding to the `.gitmodules` file + * synthesized in the specified `repo` from an original commit that had the + * specified `urls`; this modules will will contain only those urls that are + * being kept as submodules, i.e., for which the specified `keepAsSubmodule` + * returns true. + * + * @param {NodeGit.Repository} repo + * @param {Object} urls submodule name to url + * @param {(String) => Boolean} keepAsSubmodule + * @param {(String) => String|null} adjustPath + * @pram {TreeUtil.Change} + */ +exports.computeModulesFile = co.wrap(function *(repo, + urls, + keepAsSubmodule, + adjustPath) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isObject(urls); + assert.isFunction(keepAsSubmodule); + assert.isFunction(adjustPath); + + const keptUrls = {}; + for (let name in urls) { + const adjusted = adjustPath(name); + if (null !== adjusted && keepAsSubmodule(name)) { + keptUrls[adjusted] = urls[name]; + } + } + const modulesText = SubmoduleConfigUtil.writeConfigText(keptUrls); + const db = yield repo.odb(); + const BLOB = 3; + const id = yield db.write(modulesText, modulesText.length, BLOB); + return new TreeUtil.Change(id, FILEMODE.BLOB); +}); + +/** + * Return a map from submodule name to shas to list of objects containing the + * fields: + * - `metaSha` -- the meta-repo sha from which this subodule sha came + * - `url` -- url configured for the submodule + * - `sha` -- sha to fetch for the submodule + * this map contains entries for all shas introduced in the specified `toFetch` + * list in the specified `repo`. Note that the behavior is undefined unless + * `toFetch` is ordered from least to most dependent commits. Perform at most + * the specified `numParallel` operations in parallel. Do not process entries + * for submodules for which the specified `keepAsSubmodule` returns true or the + * specified `adjustPath` returns null. + * + * @param {NodeGit.Repository} repo + * @param {[NodeGit.Commit]} toFetch + * @param {(String) => Boolean} keepAsSubmodule + * @param {(String) => String|null} adjustPath + * @param {Number} numParallel + * @return {Object} map from submodule name -> { metaSha, url, sha } + */ +exports.listFetches = co.wrap(function *(repo, + toFetch, + keepAsSubmodule, + adjustPath, + numParallel) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isArray(toFetch); + assert.isFunction(keepAsSubmodule); + assert.isFunction(adjustPath); + assert.isNumber(numParallel); + + let urls = {}; + + // So that we don't have to continuously re-read the `.gitmodules` file, we + // will assume that submodule URLs never change. + + const getUrl = co.wrap(function *(commit, sub) { + let subUrl = urls[sub]; + + // If we don't have the url for this submodule, load them. + + if (undefined === subUrl) { + const newUrls = + yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, commit); + urls = Object.assign(urls, newUrls); + subUrl = urls[sub]; + } + return subUrl; + }); + + const result = {}; + + const commitChanges = {}; + + let numListed = 0; + + // This bit is pretty slow, but we run it on only unconverted commits. If + // necessary, we could cache the submodule changes in a note. + + const listChanges = co.wrap(function *(commit) { + const parents = yield commit.getParents(); + let parentCommit = null; + if (0 !== parents.length) { + parentCommit = parents[0]; + } + const changes = yield SubmoduleUtil.getSubmoduleChanges(repo, + commit, + parentCommit, + true); + commitChanges[commit.id().tostrS()] = changes; + ++numListed; + if (0 === numListed % 100) { + console.log("Listed", numListed, "of", toFetch.length); + } + }); + + yield DoWorkQueue.doInParallel(toFetch, listChanges, maxParallel); + + const addTodo = co.wrap(function *(commit, subName, sha) { + let subTodos = result[subName]; + if (undefined === subTodos) { + subTodos = []; + result[subName] = subTodos; + } + const subUrl = yield getUrl(commit, subName); + subTodos.push({ + metaSha: commit.id().tostrS(), + url: subUrl, + sha: sha, + }); + }); + + toFetch = toFetch.slice().reverse(); + for (const commit of toFetch) { + const changes = commitChanges[commit.id().tostrS()]; + + // look for added or modified submodules + + for (let name in changes) { + const change = changes[name]; + if (null !== adjustPath(name) && !keepAsSubmodule(name)) { + if (null !== change.newSha) { + yield addTodo(commit, name, change.newSha); + } + } + } + } + return result; +}); + +/** + * Write and return a new "stitched" commit for the specified `commit` in the + * specified `repo`. If the specified `keepAsSubmodule` function returns true + * for the path of a submodule, continue to treat it as a submodule in the new + * commit and do not stitch it. The specified `adjustPath` function may be + * used to move the contents of a submodule in the worktree and/or request that + * its changes be omitted completely (by returning `null`); this function is + * applied to the paths of submodules that are stitched and those that are kept + * as submodules. + * + * Once the commit has been written, record a reference indicating the mapping + * from the originally to new commit in the form of + * `refs/stitched/converted/${sha}`, and clean the refs created in + * `refs/stitched/fetched` for this commit. + * + * If the specified `skipEmpty` is true and the generated commit would be empty + * because either: + * + * 1. It would have an empty tree and no parents + * 2. It would have the same tree as its first parent + * + * Then do not generate a commit; instead, return null. In this case, we + * record that the commit maps to its first (mapped) parent; if it has no + * parents we do not record a mapping. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + * @param {[NodeGit.Commit]} parents + * @param {(String) => Boolean} keepAsSubmodule + * @param {(String) => String|null} adjustPath + * @param {Bool} skipEmpty + * @return {NodeGit.Commit} + */ +exports.writeStitchedCommit = co.wrap(function *(repo, + commit, + parents, + keepAsSubmodule, + adjustPath, + skipEmpty) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(commit, NodeGit.Commit); + assert.isArray(parents); + assert.isFunction(keepAsSubmodule); + assert.isFunction(adjustPath); + assert.isBoolean(skipEmpty); + + let parentTree = null; + if (0 !== parents.length) { + const parentCommit = parents[0]; + parentTree = yield parentCommit.getTree(); + } + + let originalParent = null; + const originalParents = yield commit.getParents(); + if (0 !== originalParents.length) { + originalParent = originalParents[0]; + } + const subChanges = yield SubmoduleUtil.getSubmoduleChanges(repo, + commit, + originalParent, + true); + + // changes and additions + + let updateModules = false; // if any kept subs added or removed + const changes = {}; + const subCommits = {}; + const stitchSub = co.wrap(function *(name, sha) { + let subCommit; + try { + subCommit = yield repo.getCommit(sha); + } + catch (e) { + console.error("On meta-commit", commit.id().tostrS(), + name, "is missing", sha); + throw e; + } + const subTreeId = subCommit.treeId(); + changes[name] = new TreeUtil.Change(subTreeId, FILEMODE.TREE); + subCommits[name] = subCommit; + }); + + function changeKept(name, newSha) { + const id = NodeGit.Oid.fromString(newSha); + changes[name] = new TreeUtil.Change(id, FILEMODE.COMMIT); + } + + const synthetics = []; // list of submodules whose refs need cleaned up + + for (let name in subChanges) { + const newSha = subChanges[name].newSha; + changes[name] = null; + if (keepAsSubmodule(name)) { + updateModules = true; + if (null !== newSha) { + changeKept(name, newSha); + } + } else if (null !== newSha && null !== adjustPath(name)) { + synthetics.push(name); + yield stitchSub(name, newSha); + } + } + + // If any kept submodules were added or removed, rewrite the modules + // file. + + if (updateModules) { + const newUrls = + yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, commit); + const content = yield exports.computeModulesFile(repo,newUrls, + keepAsSubmodule, + adjustPath); + changes[SubmoduleConfigUtil.modulesFileName] = content; + } + + // Adjust paths using the provided `adjustPath` function. + + const mappedChanges = {}; + + for (let name in changes) { + const mapped = adjustPath(name); + if (null !== mapped) { + mappedChanges[mapped] = changes[name]; + } + } + + const newTree = yield TreeUtil.writeTree(repo, parentTree, mappedChanges); + + let newCommit = null; + let mappedSha = null; + if (skipEmpty && exports.isTreeUnchanged(newTree, parentTree)) { + if (0 !== parents.length) { + mappedSha = parents[0].id().tostrS(); + } + } else { + const commitMessage = exports.makeStitchCommitMessage(commit, + subCommits); + const newCommitId = yield NodeGit.Commit.create( + repo, + null, + commit.author(), + commit.committer(), + commit.messageEncoding(), + commitMessage, + newTree, + parents.length, + parents); + newCommit = yield repo.getCommit(newCommitId); + mappedSha = newCommit.id().tostrS(); + yield exports.writeReferenceNote(repo, + mappedSha, + commit.id().tostrS(), + subCommits); + } + yield exports.writeConvertedNote(repo, commit.id().tostrS(), mappedSha); + return newCommit; +}); + +/** + * In the specified `repo`, perform the specified `subFetches`. Use the + * specified `url` to resolve relative submodule urls. Each entry in the + * `subFetches` array is an object containing the fields: + * + * - url -- submodule configured url + * - sha -- submodule sha + * - metaSha -- sha it was introcued on + * + * @param {NodeGit.Repository} repo + * @param {String} url + * @param {[Object]} subFetches + */ +exports.fetchSubCommits = co.wrap(function *(repo, url, subFetches) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(url); + assert.isArray(subFetches); + + for (const fetch of subFetches) { + const subUrl = SubmoduleConfigUtil.resolveSubmoduleUrl(url, fetch.url); + + const sha = fetch.sha; + let fetched; + try { + fetched = yield GitUtil.fetchSha(repo, subUrl, sha); + } + catch (e) { + console.log("Fetch of", subUrl, "failed:", e.message); + return; // RETURN + } + + // Make a ref to protect fetched submodule commits from GC. + + if (fetched) { + console.log("Fetched:", sha, "from", subUrl); + const refName = + SyntheticBranchUtil.getSyntheticBranchForCommit(sha); + yield NodeGit.Reference.create(repo, refName, sha, 1, "fetched"); + } + } +}); + +/** + * Return a function for adjusting paths while performing a join operation. + * For paths that start with the specified `root` path, return the part of that + * path following `root/`; for other paths return null. If `root` is null, + * return the identity function. + * + * @param {String|null} root + * @return {(String) => String|null} + */ +exports.makeAdjustPathFunction = function (root) { + if (null !== root) { + assert.isString(root); + } + if (null === root) { + return (x) => x; // RETURN + } + if (!root.endsWith("/")) { + root += "/"; + } + return function (filename) { + if (filename.startsWith(root)) { + return filename.substring(root.length); + } + return null; + }; +}; + +/** + * Stitch the repository at the specified `repoPath` starting with the + * specified `commitish` and point the specified `targetBranchName` to the + * result. Use the specified `options.keepAsSubmodule` function to determine + * which paths to keep as submodule rather than stitching. If the optionally + * specified `options.joinRoot` is provided, create a history containing only + * commits touching that tree of the meta-repo, rooted relative to + * `options.joinRoot`. Perform at most the specified `options.numParallel` + * fetch operations at once. If the specified `options.fetch` is provied and + * true, fetch submodule commits as needed using the specified `url` to resolve + * relative submodule URLs. If the specified `skipEmpty` is provided and true, + * omit, from the generated history, commits whose trees do not differ from + * their first parents. The behavior is undefined if `true === fetch` and + * `options.url` is not provided. + * + * TBD: Write a unit test for this; this logic used to be in the command + * handler, and all the pieces are tested. + * + * @param {String} repoPath + * @param {String} commitish + * @param {String} targetBranchName + * @param {Object} options + * @param {Bool} [options.fetch] + * @param {String} [options.url] + * @param {(String) => Bool} options.keepAsSubmodule + * @param {String} [options.joinRoot] + * @param {Number} numParallel + * @param {Bool} [skipEmpty] + */ +exports.stitch = co.wrap(function *(repoPath, + commitish, + targetBranchName, + options) { + assert.isString(repoPath); + assert.isString(commitish); + assert.isString(targetBranchName); + assert.isObject(options); + + let fetch = false; + let url = null; + if ("fetch" in options) { + assert.isBoolean(options.fetch); + fetch = options.fetch; + assert.isString(options.url, "url required with fetch"); + url = options.url; + } + + assert.isFunction(options.keepAsSubmodule); + + let joinRoot = null; + if ("joinRoot" in options) { + assert.isString(options.joinRoot); + joinRoot = options.joinRoot; + } + + assert.isNumber(options.numParallel); + + let skipEmpty = false; + if ("skipEmpty" in options) { + assert.isBoolean(options.skipEmpty); + skipEmpty = options.skipEmpty; + } + + const repo = yield NodeGit.Repository.open(repoPath); + const annotated = yield GitUtil.resolveCommitish(repo, commitish); + if (null === annotated) { + throw new Error(`Could not resolve ${commitish}.`); + } + const commit = yield repo.getCommit(annotated.id()); + + console.log("Listing previously converted commits."); + + const convertedCommits = yield exports.listConvertedCommits(repo); + + console.log("listing unconverted ancestors of", commit.id().tostrS()); + + const commitsToStitch = + yield exports.listCommitsToStitch(repo, commit, convertedCommits); + + const adjustPath = exports.makeAdjustPathFunction(joinRoot); + + console.log(commitsToStitch.length, "to stitch"); + + if (fetch) { + console.log("listing fetches"); + const fetches = yield exports.listFetches(repo, + commitsToStitch, + options.keepAsSubmodule, + adjustPath, + options.numParallel); + console.log("Found", Object.keys(fetches).length, "subs to fetch."); + const subNames = Object.keys(fetches); + const doFetch = co.wrap(function *(name, i) { + const subFetches = fetches[name]; + const fetchTimeMessage = `\ +(${i + 1}/${subNames.length}) -- fetched ${subFetches.length} SHAs for \ +${name}`; + console.time(fetchTimeMessage); + yield exports.fetchSubCommits(repo, url, subFetches); + console.timeEnd(fetchTimeMessage); + }); + yield DoWorkQueue.doInParallel(subNames, doFetch, options.numParallel); + } + + console.log("Now stitching"); + let lastCommit = null; + + for (let i = 0; i < commitsToStitch.length; ++i) { + const next = commitsToStitch[i]; + + const nextSha = next.id().tostrS(); + const parents = yield next.getParents(); + const newParents = []; + for (const parent of parents) { + const newParentSha = convertedCommits[parent.id().tostrS()]; + if (null !== newParentSha && undefined !== newParentSha) { + const newParent = yield repo.getCommit(newParentSha); + newParents.push(newParent); + } + } + + const newCommit = yield exports.writeStitchedCommit( + repo, + next, + newParents, + options.keepAsSubmodule, + adjustPath, + skipEmpty); + const newSha = null === newCommit ? null : newCommit.id().tostrS(); + convertedCommits[nextSha] = newSha; + const desc = null === newCommit ? "skipped" : newCommit.id().tostrS(); + const log = `\ +Of [${commitsToStitch.length}] done [${i + 1}] : ${nextSha} -> ${desc}`; + console.log(log); + + // If `writeStitchedCommit` returned null to indicate that it did not + // make a commit (because it would have been empty), leave `lastCommit` + // unchanged. + + lastCommit = newCommit || lastCommit; + } + if (null !== lastCommit) { + console.log( + `Updating ${targetBranchName} to ${lastCommit.id().tostrS()}.`); + yield NodeGit.Branch.create(repo, targetBranchName, lastCommit, 1); + } +}); + diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index b0f5d1752..10271c319 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -741,7 +741,8 @@ describe("GitUtil", function () { const written = yield WriteRepoASTUtil.writeRAST(ast, path); const commit = written.oldCommitMap["1"]; const repo = written.repo; - yield GitUtil.fetchSha(repo, "not a url", commit); + const result = yield GitUtil.fetchSha(repo, "not a url", commit); + assert.equal(result, false); })); it("fetch one", co.wrap(function *() { @@ -754,7 +755,8 @@ describe("GitUtil", function () { const writtenY = yield WriteRepoASTUtil.writeRAST(astY, yPath); const commit = writtenX.oldCommitMap["2"]; const repo = writtenY.repo; - yield GitUtil.fetchSha(repo, xPath, commit); + const result = yield GitUtil.fetchSha(repo, xPath, commit); + assert.equal(result, true); yield repo.getCommit(commit); })); diff --git a/node/test/util/stitch_util.js b/node/test/util/stitch_util.js new file mode 100644 index 000000000..9129610b2 --- /dev/null +++ b/node/test/util/stitch_util.js @@ -0,0 +1,1296 @@ +/* + * Copyright (c) 2016, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const NodeGit = require("nodegit"); + +const RepoAST = require("../../lib/util/repo_ast"); +const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const RepoASTUtil = require("../../lib/util/repo_ast_util"); +const StitchUtil = require("../../lib/util/stitch_util"); +const SubmoduleConfigUtil = require("../../lib/util/submodule_config_util"); +const TreeUtil = require("../../lib/util/tree_util"); + +const FILEMODE = NodeGit.TreeEntry.FILEMODE; + +function deSplitSha(sha) { + return sha.slice(0, 2) + sha.slice(3); +} + +/** + * Replace refs and notes with their equivalent logical mapping. + */ +function refMapper(actual, mapping) { + const fetchedSubRe = /(commits\/)(.*)/; + const commitMap = mapping.commitMap; + let result = {}; + + // Map refs + + Object.keys(actual).forEach(repoName => { + const ast = actual[repoName]; + const refs = ast.refs; + const newRefs = {}; + Object.keys(refs).forEach(refName => { + const ref = refs[refName]; + const fetchedSubMatch = fetchedSubRe.exec(refName); + if (null !== fetchedSubMatch) { + const sha = fetchedSubMatch[2]; + const logical = commitMap[sha]; + const newRefName = refName.replace(fetchedSubRe, + `$1${logical}`); + newRefs[newRefName] = ref; + return; // RETURN + } + newRefs[refName] = ref; + }); + + // map notes + + const notes = ast.notes; + const newNotes = {}; + Object.keys(notes).forEach(refName => { + const commits = notes[refName]; + if (StitchUtil.referenceNoteRef === refName) { + // We can't check these in the normal way, so we have a + // special test case instead. + + return; // RETURN + } + if ("refs/notes/stitched/converted" !== refName) { + newNotes[refName] = commits; + return; // RETURN + } + const newCommits = {}; + Object.keys(commits).forEach(originalSha => { + let stitchedSha = commits[originalSha]; + if ("" !== stitchedSha) { + stitchedSha = commitMap[stitchedSha]; + } + newCommits[originalSha] = stitchedSha; + }); + newNotes[refName] = newCommits; + }); + result[repoName] = ast.copy({ + refs: newRefs, + notes: newNotes, + }); + }); + return result; +} + +describe("StitchUtil", function () { +describe("writeConvertedNote", function () { + it("with target sha", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S:C2-1;Bfoo=2"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const foo = yield repo.getBranchCommit("foo"); + const fooSha = foo.id().tostrS(); + const refName = StitchUtil.convertedNoteRef; + yield StitchUtil.writeConvertedNote(repo, headSha, fooSha); + const note = yield NodeGit.Note.read(repo, refName, headSha); + const message = note.message(); + assert.equal(message, fooSha); + })); + it("without target sha", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const refName = StitchUtil.convertedNoteRef; + yield StitchUtil.writeConvertedNote(repo, headSha, null); + const note = yield NodeGit.Note.read(repo, refName, headSha); + const message = note.message(); + assert.equal(message, ""); + })); +}); +describe("makeStitchCommitMessage", function () { + it("just a meta", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const tree = yield head.getTree(); + const sig = NodeGit.Signature.create("me", "me@me", 3, 60); + const commitId = yield NodeGit.Commit.create(repo, + null, + sig, + sig, + null, + "hello world\n", + tree, + 0, + []); + const commit = yield repo.getCommit(commitId); + const result = StitchUtil.makeStitchCommitMessage(commit, {}); + const expected = "hello world\n"; + assert.equal(result, expected); + })); + it("same sub", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const tree = yield head.getTree(); + const sig = NodeGit.Signature.create("me", "me@me", 3, 60); + const commitId = yield NodeGit.Commit.create(repo, + null, + sig, + sig, + null, + "hello world\n", + tree, + 0, + []); + const commit = yield repo.getCommit(commitId); + const result = StitchUtil.makeStitchCommitMessage(commit, { + "foo/bar": commit, + }); + const expected = "hello world\n"; + assert.equal(result, expected); + })); + it("diff sub message", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const tree = yield head.getTree(); + const sig = NodeGit.Signature.create("me", "me@me", 3, 60); + const commitId = yield NodeGit.Commit.create(repo, + null, + sig, + sig, + null, + "hello world\n", + tree, + 0, + []); + const commit = yield repo.getCommit(commitId); + const fooBarId = yield NodeGit.Commit.create(repo, + null, + sig, + sig, + null, + "foo bar\n", + tree, + 0, + []); + const fooBar = yield repo.getCommit(fooBarId); + const result = StitchUtil.makeStitchCommitMessage(commit, { + "foo/bar": fooBar, + }); + const expected = `\ +hello world + +From 'foo/bar' + +foo bar +`; + assert.equal(result, expected); + })); + it("diff sub name", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const tree = yield head.getTree(); + const sig = NodeGit.Signature.create("me", "me@me", 3, 60); + const commitId = yield NodeGit.Commit.create(repo, + null, + sig, + sig, + null, + "hello world\n", + tree, + 0, + []); + const commit = yield repo.getCommit(commitId); + const fooBarSig = NodeGit.Signature.create("you", "me@me", 3, 60); + const fooBarId = yield NodeGit.Commit.create(repo, + null, + fooBarSig, + fooBarSig, + null, + "hello world\n", + tree, + 0, + []); + const fooBar = yield repo.getCommit(fooBarId); + const result = StitchUtil.makeStitchCommitMessage(commit, { + "foo/bar": fooBar, + }); + const expected = `\ +hello world + +From 'foo/bar' +Author: you +`; + assert.equal(result, expected); + })); + it("diff sub email", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const tree = yield head.getTree(); + const sig = NodeGit.Signature.create("me", "me@me", 3, 60); + const commitId = yield NodeGit.Commit.create(repo, + null, + sig, + sig, + null, + "hello world\n", + tree, + 0, + []); + const commit = yield repo.getCommit(commitId); + const fooBarSig = NodeGit.Signature.create("me", "you@you", 3, 60); + const fooBarId = yield NodeGit.Commit.create(repo, + null, + fooBarSig, + fooBarSig, + null, + "hello world\n", + tree, + 0, + []); + const fooBar = yield repo.getCommit(fooBarId); + const result = StitchUtil.makeStitchCommitMessage(commit, { + "foo/bar": fooBar, + }); + const expected = `\ +hello world + +From 'foo/bar' +Author: me +`; + assert.equal(result, expected); + })); + it("diff time", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const tree = yield head.getTree(); + const sig = NodeGit.Signature.create("me", "me@me", 3, 60); + const commitId = yield NodeGit.Commit.create(repo, + null, + sig, + sig, + null, + "hello world\n", + tree, + 0, + []); + const commit = yield repo.getCommit(commitId); + const fooBarSig = NodeGit.Signature.create("me", "me@me", 2, 60); + const fooBarId = yield NodeGit.Commit.create(repo, + null, + fooBarSig, + fooBarSig, + null, + "hello world\n", + tree, + 0, + []); + const fooBar = yield repo.getCommit(fooBarId); + const result = StitchUtil.makeStitchCommitMessage(commit, { + "foo/bar": fooBar, + }); + const expected = `\ +hello world + +From 'foo/bar' +Date: 1/1/1970, 01:00:02 100 +`; + assert.equal(result, expected); + })); + it("diff offset", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const tree = yield head.getTree(); + const sig = NodeGit.Signature.create("me", "me@me", 3, 60); + const commitId = yield NodeGit.Commit.create(repo, + null, + sig, + sig, + null, + "hello world\n", + tree, + 0, + []); + const commit = yield repo.getCommit(commitId); + const fooBarSig = NodeGit.Signature.create("me", "me@me", 3, 120); + const fooBarId = yield NodeGit.Commit.create(repo, + null, + fooBarSig, + fooBarSig, + null, + "hello world\n", + tree, + 0, + []); + const fooBar = yield repo.getCommit(fooBarId); + const result = StitchUtil.makeStitchCommitMessage(commit, { + "foo/bar": fooBar, + }); + const expected = `\ +hello world + +From 'foo/bar' +Date: 1/1/1970, 02:00:03 200 +`; + assert.equal(result, expected); + })); +}); +describe("writeReferenceNote", function () { + it("breathing", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S:C2-1;Bfoo=2"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const subs = {}; + subs["foo/bar"] = head; + yield StitchUtil.writeReferenceNote(repo, headSha, headSha, subs); + const refName = StitchUtil.referenceNoteRef; + const note = yield NodeGit.Note.read(repo, refName, headSha); + const content = note.message(); + const result = JSON.parse(content); + const expected = { + metaRepoCommit: headSha, + submoduleCommits: {}, + }; + expected.submoduleCommits["foo/bar"] = headSha; + assert.deepEqual(result, expected); + })); +}); +describe("listCommitsInOrder", function () { + const cases = { + "trival": { + input: { + a: [], + }, + entry: "a", + expected: ["a"], + }, + "skipped entry": { + input: {}, + entry: "a", + expected: [], + }, + "one parent": { + input: { + a: ["b"], + b: [], + }, + entry: "a", + expected: ["b", "a"], + }, + "one parent, skipped": { + input: { + b: [], + }, + entry: "b", + expected: ["b"], + }, + "two parents": { + input: { + b: ["a", "c"], + a: [], + c: [], + }, + entry: "b", + expected: ["a", "c", "b"], + }, + "chain": { + input: { + a: ["b"], + b: ["c"], + c: [], + }, + entry: "a", + expected: ["c", "b", "a"], + }, + "reference the same commit twice in history": { + input: { + c: ["b"], + a: ["b"], + d: ["a", "c"], + b: ["e", "f"], + e: ["f"], + f: [], + }, + entry: "d", + expected: ["f", "e", "b", "a", "c", "d"], + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, function () { + const result = StitchUtil.listCommitsInOrder(c.entry, c.input); + assert.deepEqual(c.expected, result); + }); + }); +}); +describe("listConvertedCommits", function () { + it("empty", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S:C2-1;Bfoo=2"); + const repo = written.repo; + const result = yield StitchUtil.listConvertedCommits(repo); + assert.deepEqual(result, {}); + })); + it("breathing", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S:C2-1;Bfoo=2"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const foo = yield repo.getBranchCommit("foo"); + const fooSha = foo.id().tostrS(); + yield StitchUtil.writeConvertedNote(repo, headSha, fooSha); + yield StitchUtil.writeConvertedNote(repo, fooSha, null); + const result = yield StitchUtil.listConvertedCommits(repo); + const expected = {}; + expected[headSha] = fooSha; + expected[fooSha] = null; + assert.deepEqual(result, expected); + })); +}); +describe("listCommitsToStitch", function () { + // We don't need to validate the ordering part; that is check in the + // test driver for 'listCommitsInOrder'. We need to validate basic + // functionality, and that we stop at previously converted commits. + + it("trivial", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const converted = yield StitchUtil.listConvertedCommits(repo); + const result = + yield StitchUtil.listCommitsToStitch(repo, head, converted); + const headSha = head.id().tostrS(); + const resultShas = result.map(c => c.id().tostrS()); + assert.deepEqual([headSha], resultShas); + })); + + it("skipped", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + yield StitchUtil.writeConvertedNote(repo, headSha, null); + const converted = yield StitchUtil.listConvertedCommits(repo); + const result = + yield StitchUtil.listCommitsToStitch(repo, head, converted); + const resultShas = result.map(c => c.id().tostrS()); + assert.deepEqual([], resultShas); + })); + + it("with parents", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo( + "S:C3-2,4;C4-2;C2-1;C5-3,4;Bmaster=5"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const converted = yield StitchUtil.listConvertedCommits(repo); + const result = + yield StitchUtil.listCommitsToStitch(repo, head, converted); + const expected = ["1", "2", "4", "3", "5"]; + const resultShas = result.map(c => { + const sha = c.id().tostrS(); + return written.commitMap[sha]; + }); + assert.deepEqual(expected, resultShas); + })); + + it("with parents and marker", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo( + "S:C3-2,4;C4-2;C2-1;C5-3,4;Bmaster=5"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const twoSha = written.oldCommitMap["2"]; + yield StitchUtil.writeConvertedNote(repo, twoSha, twoSha); + const converted = yield StitchUtil.listConvertedCommits(repo); + const result = + yield StitchUtil.listCommitsToStitch(repo, head, converted); + const expected = ["4", "3", "5"]; + const resultShas = result.map(c => { + const sha = c.id().tostrS(); + return written.commitMap[sha]; + }); + assert.deepEqual(expected, resultShas); + })); +}); +describe("isTreeUnchanged", function () { + it("null original, non-empty", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const tree = yield head.getTree(); + const result = StitchUtil.isTreeUnchanged(tree, null); + assert.isFalse(result); + })); + it("null original, empty", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const emptyTree = yield TreeUtil.writeTree(repo, null, {}); + const result = StitchUtil.isTreeUnchanged(emptyTree, null); + assert.isTrue(result); + })); + it("same tree", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const tree = yield head.getTree(); + const result = StitchUtil.isTreeUnchanged(tree, tree); + assert.isTrue(result); + })); + it("different tree", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S:C2-1;Bfoo=2"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headTree = yield head.getTree(); + const fooCommit = yield repo.getBranchCommit("foo"); + const fooTree = yield fooCommit.getTree(); + const result = StitchUtil.isTreeUnchanged(headTree, fooTree); + assert.isFalse(result); + })); +}); +it("deSplitSha", function () { + assert.equal("1234", deSplitSha("12/34")); +}); +describe("refMapper", function () { + const Commit = RepoAST.Commit; + const cases = { + "trivial": { + input: { + }, + expected: { + }, + }, + "simple": { + input: { + x: new RepoAST(), + }, + expected: { + x: new RepoAST(), + }, + }, + "no transform": { + input: { + x: new RepoAST({ + commits: { "1": new Commit() }, + refs: { + "foo/bar": "1", + }, + }), + }, + expected: { + x: new RepoAST({ + commits: { "1": new Commit() }, + refs: { + "foo/bar": "1", + }, + }), + }, + }, + "note": { + input: { + x: new RepoAST({ + head: "fffd", + commits: { + "fffd": new Commit(), + }, + notes: { + "refs/notes/stitched/converted": { + "fffd": "ffff", + }, + }, + }), + }, + expected: { + x: new RepoAST({ + head: "fffd", + commits: { + "fffd": new Commit(), + }, + notes: { + "refs/notes/stitched/converted": { + "fffd": "1", + }, + }, + }), + }, + commitMap: { + "ffff": "1", + }, + }, + "note, empty": { + input: { + x: new RepoAST({ + head: "fffd", + commits: { + "fffd": new Commit(), + }, + notes: { + "refs/notes/stitched/converted": { + "fffd": "", + }, + }, + }), + }, + expected: { + x: new RepoAST({ + head: "fffd", + commits: { + "fffd": new Commit(), + }, + notes: { + "refs/notes/stitched/converted": { + "fffd": "", + }, + }, + }), + }, + commitMap: { + "ffff": "1", + }, + }, + "note, unrelated": { + input: { + x: new RepoAST({ + head: "fffd", + commits: { + "fffd": new Commit(), + }, + notes: { + "refs/notes/foo": { + "fffd": "ffff", + }, + }, + }), + }, + expected: { + x: new RepoAST({ + head: "fffd", + commits: { + "fffd": new Commit(), + }, + notes: { + "refs/notes/foo": { + "fffd": "ffff", + }, + }, + }), + }, + commitMap: { + "ffff": "1", + }, + }, + "fetched sub": { + input: { + x: new RepoAST({ + commits: { + "fffd": new Commit(), + }, + refs: { + "commits/ffff": "fffd", + }, + }), + }, + expected: { + x: new RepoAST({ + commits: { + "fffd": new Commit(), + }, + refs: { + "commits/1": "fffd", + }, + }), + }, + commitMap: { + "ffff": "1", + "aabb": "2", + }, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, () => { + const result = refMapper(c.input, { + commitMap: c.commitMap || {}, + }); + RepoASTUtil.assertEqualRepoMaps(result, c.expected); + }); + }); + +}); +it("splitSha", function () { + assert.equal("34/56", StitchUtil.splitSha("3456")); +}); +describe("computeModulesFile", function () { + const cases = { + "one kept": { + newUrls: { foo: "bar/baz" }, + keepAsSubmodule: (name) => name === "foo", + expected: { foo: "bar/baz" }, + }, + "one not": { + newUrls: { foo: "bar/baz", bar: "zip/zap", }, + keepAsSubmodule: (name) => name === "foo", + expected: { foo: "bar/baz" }, + }, + "path omitted": { + newUrls: { foo: "bar/baz" }, + keepAsSubmodule: (name) => name === "foo", + expected: {}, + adjustPath: () => null, + }, + "path changed": { + newUrls: { foo: "bar/baz" }, + keepAsSubmodule: (name) => name === "foo", + adjustPath: () => "bam/bap", + expected: { "bam/bap": "bar/baz" }, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const text = SubmoduleConfigUtil.writeConfigText(c.expected); + const BLOB = 3; + const db = yield repo.odb(); + const id = yield db.write(text, text.length, BLOB); + const adjustPath = c.adjustPath || ((x) => x); + const result = yield StitchUtil.computeModulesFile( + repo, + c.newUrls, + c.keepAsSubmodule, + adjustPath); + assert.instanceOf(result, TreeUtil.Change); + assert.equal(id.tostrS(), result.id.tostrS(), "ids"); + assert.equal(FILEMODE.BLOB, result.mode, "mode"); + })); + }); +}); +describe("writeStitchedCommit", function () { + const cases = { + "trivial, no subs": { + input: "x=S", + commit: "1", + parents: [], + keepAsSubmodule: () => false, + expected: ` +x=E:Cthe first commit#s ;Bstitched=s;N refs/notes/stitched/converted 1=s`, + }, + "trivial, no subs, with a parent": { + input: "x=S:C2;Bp=2", + commit: "1", + parents: ["2"], + keepAsSubmodule: () => false, + expected: ` +x=E:Cthe first commit#s-2 ;Bstitched=s;N refs/notes/stitched/converted 1=s`, + }, + "new stitched sub": { + input: ` +x=B:Ca;Cfoo#2-1 s=S.:a;Ba=a;Bmaster=2`, + commit: "2", + parents: [], + keepAsSubmodule: () => false, + expected: ` +x=E:C*#s s/a=a;Bstitched=s;N refs/notes/stitched/converted 2=s`, + }, + "new stitched sub, with parent": { + input: ` +x=B:Ca;C2-1 s=S.:a;Ba=a;Bmaster=2`, + commit: "2", + parents: ["1"], + keepAsSubmodule: () => false, + expected: ` +x=E:C*#s-1 s/a=a;Bstitched=s;N refs/notes/stitched/converted 2=s`, + }, + "2 new stitched subs": { + input: ` +x=B:Ca;Cb;C2-1 s=S.:a,t=S.:b;Ba=a;Bb=b;Bmaster=2`, + commit: "2", + parents: [], + keepAsSubmodule: () => false, + expected: ` +x=E:C*#s s/a=a,t/b=b;Bstitched=s;N refs/notes/stitched/converted 2=s`, + }, + "modified stitched": { + input: ` +x=B:Ca;Cb;Cc;C2-1 s=S.:a,t=S.:b;C3-2 s=S.:c;Ba=a;Bb=b;Bc=c;Bmaster=3`, + commit: "3", + parents: [], + keepAsSubmodule: () => false, + expected: ` +x=E:C*#s s/c=c;Bstitched=s;N refs/notes/stitched/converted 3=s`, + }, + "removed stitched": { + input: ` +x=B:Ca;Cb;Cc s/a=b;Cfoo#2-1 s=S.:a,t=S.:b;C3-2 s;Ba=a;Bb=b;Bc=c;Bmaster=3`, + commit: "3", + parents: ["c"], + keepAsSubmodule: () => false, + expected: ` +x=E:Cs-c s/a;Bstitched=s;N refs/notes/stitched/converted 3=s`, + }, + "kept": { + input: ` +x=B:Ca;Cb;C2-1 s=S.:a,t=S.:b;Ba=a;Bb=b;Bmaster=2`, + commit: "2", + parents: [], + keepAsSubmodule: (name) => "t" === name, + expected: ` +x=E:C*#s s/a=a,t=S.:b;Bstitched=s;N refs/notes/stitched/converted 2=s`, + }, + "modified kept": { + input: ` +x=B:Ca;Cb;Ba=a;Bb=b;C2-1 s=S.:a;C3-2 s=S.:b;Cp foo=bar,s=S.:a;Bmaster=3;Bp=p`, + commit: "3", + parents: ["p"], + keepAsSubmodule: (name) => "s" === name, + expected: ` +x=E:Cs-p s=S.:b;Bstitched=s;N refs/notes/stitched/converted 3=s`, + }, + "removed kept": { + input: ` +x=B:Ca;Ba=a;C2-1 s=S.:a;C3-2 s;Cp foo=bar,s=S.:a;Bmaster=3;Bp=p`, + commit: "3", + parents: ["p"], + keepAsSubmodule: (name) => "s" === name, + expected: ` +x=E:Cs-p s;Bstitched=s;N refs/notes/stitched/converted 3=s`, + }, + "empty commit, but not skipped": { + input: ` +x=B:Ca;Cfoo#2 ;Ba=a;Bmaster=2;Bfoo=1`, + commit: "2", + parents: ["1"], + keepAsSubmodule: () => false, + expected: ` +x=E:C*#s-1 ;Bstitched=s;N refs/notes/stitched/converted 2=s`, + }, + "empty commit, skipped": { + input: ` +x=B:Ca;Cfoo#2 ;Ba=a;Bmaster=2;Bfoo=1`, + commit: "2", + parents: [], + keepAsSubmodule: () => false, + skipEmpty: true, + isNull: true, + expected: ` +x=E:N refs/notes/stitched/converted 2=`, + }, + "skipped empty, with parent": { + input: ` +x=B:Ca;C2-1 ;Ba=a;Bmaster=2`, + commit: "2", + parents: ["1"], + keepAsSubmodule: () => false, + skipEmpty: true, + isNull: true, + expected: ` +x=E:N refs/notes/stitched/converted 2=1`, + }, + "adjusted to new path": { + input: ` +x=B:Ca;Cfoo#2-1 s=S.:a;Ba=a;Bmaster=2`, + commit: "2", + parents: [], + keepAsSubmodule: () => false, + adjustPath: () => "foo/bar", + expected: ` +x=E:C*#s foo/bar/a=a;Bstitched=s;N refs/notes/stitched/converted 2=s`, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const stitcher = co.wrap(function *(repos, maps) { + const x = repos.x; + const revMap = maps.reverseCommitMap; + const commit = yield x.getCommit(revMap[c.commit]); + const parents = + yield c.parents.map(co.wrap(function *(sha) { + return yield x.getCommit(revMap[sha]); + })); + const adjustPath = c.adjustPath || ((x) => x); + const skipEmpty = c.skipEmpty || false; + const stitch = yield StitchUtil.writeStitchedCommit( + x, + commit, + parents, + c.keepAsSubmodule, + adjustPath, + skipEmpty); + if (true === c.isNull) { + assert.isNull(stitch); + return; + } else { + // Need to root the commit we wrote + yield NodeGit.Reference.create(x, + "refs/heads/stitched", + stitch, + 1, + "stitched"); + } + const commitMap = {}; + commitMap[stitch.id().tostrS()] = "s"; + return { + commitMap, + }; + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.input, + c.expected, + stitcher, + c.fails, { + actualTransformer: refMapper, + }); + })); + }); +}); +it("messaging", co.wrap(function *() { + + const state = "B:Ca;C2-1 s=S.:a;Ba=a;Bmaster=2"; + const written = yield RepoASTTestUtil.createRepo(state); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const stitch = yield StitchUtil.writeStitchedCommit(repo, + head, + [], + () => false, + (x) => x, + false); + const subCommitRef = yield NodeGit.Reference.lookup(repo, + "refs/heads/a"); + const subCommit = yield repo.getCommit(subCommitRef.target()); + const subCommits = { + s: subCommit, + }; + const expected = StitchUtil.makeStitchCommitMessage(head, subCommits); + const actual = stitch.message(); + assert.deepEqual(expected.split("\n"), actual.split("\n")); +})); +it("reference note", co.wrap(function *() { + + const state = "B:Ca;C2-1 s=S.:a;Ba=a;Bmaster=2"; + const written = yield RepoASTTestUtil.createRepo(state); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const stitch = yield StitchUtil.writeStitchedCommit(repo, + head, + [], + () => false, + (x) => x, + false); + const note = yield NodeGit.Note.read(repo, + StitchUtil.referenceNoteRef, + stitch); + const subCommitRef = yield NodeGit.Reference.lookup(repo, + "refs/heads/a"); + const subCommit = yield repo.getCommit(subCommitRef.target()); + const expected = { + metaRepoCommit: head.id().tostrS(), + submoduleCommits: { + s: subCommit.id().tostrS(), + }, + }; + const actual = JSON.parse(note.message()); + assert.deepEqual(actual, expected); +})); +describe("listFetches", function () { + const cases = { + "trivial": { + state: "S", + toFetch: [], + keepAsSubmodule: () => false, + numParallel: 2, + expected: {}, + }, + "a sub, not picked": { + state: "S:C2-1 s=S/a:1;Bmaster=2", + toFetch: ["1"], + keepAsSubmodule: () => false, + numParallel: 2, + expected: {}, + }, + "added sub": { + state: "S:C2-1 s=S/a:1;Bmaster=2", + toFetch: ["2"], + keepAsSubmodule: () => false, + numParallel: 2, + expected: { + "s": [ + { metaSha: "2", url: "/a", sha: "1" }, + ], + }, + }, + "added sub kept": { + state: "S:C2-1 s=S/a:1;Bmaster=2", + toFetch: ["2"], + keepAsSubmodule: (name) => "s" === name, + numParallel: 2, + expected: {}, + }, + "adjusted to null": { + state: "S:C2-1 s=S/a:1;Bmaster=2", + toFetch: ["2"], + keepAsSubmodule: () => false, + adjustPath: () => null, + numParallel: 2, + expected: {}, + }, + "changed sub": { + state: "S:Cx-1;Bx=x;C2-1 s=S/a:1;C3-2 s=S/a:x;Bmaster=3", + toFetch: ["3"], + keepAsSubmodule: () => false, + numParallel: 2, + expected: { + "s": [ + { metaSha: "3", url: "/a", sha: "x" }, + ], + }, + }, + "changed sub kept": { + state: "S:Cx-1;Bx=x;C2-1 s=S/a:1;C3-2 s=S/a:x;Bmaster=3", + toFetch: ["3"], + keepAsSubmodule: (name) => "s" === name, + numParallel: 2, + expected: {}, + }, + "two changes in a sub": { + state: "S:Cx-1;Bx=x;C2-1 s=S/a:1;C3-2 s=S/a:x;Bmaster=3", + toFetch: ["2", "3"], + keepAsSubmodule: () => false, + numParallel: 2, + expected: { + "s": [ + { metaSha: "3", url: "/a", sha: "x" }, + { metaSha: "2", url: "/a", sha: "1" }, + ], + }, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(c.state); + const repo = written.repo; + const revMap = written.oldCommitMap; + const commitMap = written.commitMap; + const toFetch = yield c.toFetch.map(co.wrap(function *(e) { + const sha = revMap[e]; + return yield repo.getCommit(sha); + })); + const adjustPath = c.adjustPath || ((x) => x); + const result = yield StitchUtil.listFetches(repo, + toFetch, + c.keepAsSubmodule, + adjustPath, + c.numParallel); + function mapFetch(f) { + return { + url: f.url, + metaSha: commitMap[f.metaSha], + sha: commitMap[f.sha], + }; + } + for (let name in result) { + result[name] = result[name].map(mapFetch); + } + assert.deepEqual(result, c.expected); + })); + }); +}); +describe("fetchSubCommits", function () { + const cases = { + "trivial": { + input: "a=B|x=S", + fetches: [], + url: "a", + }, + "one, w sub": { + input: "a=B:Cz-1;Bz=z|x=U", + fetches: [ + { + url: "../a", + sha: "z", + metaSha: "2" + } + ], + url: "a", + expected: "x=E:Fcommits/z=z", + }, + "one, w sub no need to fetch": { + input: "a=B|x=U", + fetches: [ + { + url: "a", + sha: "1", + metaSha: "2" + } + ], + url: "a", + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const fetcher = co.wrap(function *(repos, maps) { + const x = repos.x; + const revMap = maps.reverseCommitMap; + const fetches = c.fetches.map(e => { + return { + url: e.url, + sha: revMap[e.sha], + metaSha: revMap[e.metaSha], + }; + }); + const url = maps.reverseUrlMap[c.url]; + yield StitchUtil.fetchSubCommits(x, url, fetches); + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.input, + c.expected, + fetcher, + c.fails, { + actualTransformer: refMapper, + }); + + })); + }); +}); +describe("makeAdjustPathFunction", function () { + const cases = { + "null root": { + root: null, + filename: "foo", + expected: "foo", + }, + "match it": { + root: "foo/", + filename: "foo/bar", + expected: "bar", + }, + "miss it": { + root: "foo/", + filename: "meh", + expected: null, + }, + "match it with missing slash": { + root: "foo", + filename: "foo/bar", + expected: "bar", + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, function () { + const adjustPath = StitchUtil.makeAdjustPathFunction(c.root); + const result = adjustPath(c.filename); + assert.equal(result, c.expected); + }); + }); +}); +describe("stitch", function () { + const cases = { + "breathing": { + input: ` +x=B:Ca;Cfoo#2-1 s=S.:a;Ba=a;Bmaster=2`, + commit: "2", + targetBranchName: "my-branch", + keepAsSubmodule: () => false, + numParallel: 8, + expected: ` +x=E:C*#s2-s1 s/a=a;C*#s1 ; +Bmy-branch=s2; +N refs/notes/stitched/converted 2=s2; +N refs/notes/stitched/converted 1=s1`, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const stitcher = co.wrap(function *(repos, maps) { + const x = repos.x; + const options = { + numParallel: c.numParallel, + keepAsSubmodule: c.keepAsSubmodule, + }; + if ("fetch" in c) { + options.fetch = c.fetch; + } + if ("url" in c) { + options.url = c.url; + } + if ("joinRoot" in c) { + options.joinRoot = c.joinRoot; + } + if ("skipEmpty" in c) { + options.skipEmpty = c.skipEmpty; + } + const revMap = maps.reverseCommitMap; + yield StitchUtil.stitch(x.path(), + revMap[c.commit], + c.targetBranchName, + options); + const noteRefs = []; + function listNoteRefs(_, objectId) { + noteRefs.push(objectId.tostrS()); + } + yield NodeGit.Note.foreach(x, + StitchUtil.convertedNoteRef, + listNoteRefs); + const commitMap = {}; + yield noteRefs.map(co.wrap(function *(noteRef) { + const note = yield NodeGit.Note.read( + x, + StitchUtil.convertedNoteRef, + noteRef); + const content = note.message(); + if ("" !== content) { + const name = "s" + maps.commitMap[noteRef]; + commitMap[content] = name; + } + })); + return { + commitMap, + }; + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.input, + c.expected, + stitcher, + c.fails, { + actualTransformer: refMapper, + }); + })); + }); +}); +}); diff --git a/node/test/util/tree_util.js b/node/test/util/tree_util.js index abbf421fd..eda4f9222 100644 --- a/node/test/util/tree_util.js +++ b/node/test/util/tree_util.js @@ -105,6 +105,24 @@ describe("TreeUtil", function () { "a": { "b": "1", }, + } + }, + "creation and deletion": { + input: { + "a/b": "1", + "a": null, + }, + expected: { + a: { b: "1" }, + }, + }, + "deletion and creation": { + input: { + "a": null, + "a/b": "1", + }, + expected: { + a: { b: "1" }, }, }, }; @@ -310,6 +328,21 @@ describe("TreeUtil", function () { const entry = yield secondTree.entryByPath("foo"); assert.equal(entry.id().tostrS(), id.tostrS()); })); + it("implicitly overwrite blob with directory", co.wrap(function *() { + const repo = yield makeRepo(); + const blobAId = yield hashObject(repo, "xxxxxxxh"); + const parent = yield TreeUtil.writeTree(repo, null, { + foo: new Change(blobAId, FILEMODE.BLOB), + }); + const blobBId = yield hashObject(repo, "bazzzz"); + const result = yield TreeUtil.writeTree(repo, parent, { + "foo/bar": new Change(blobBId, FILEMODE.BLOB), + }); + const entry = yield result.entryByPath("foo/bar"); + assert.isNotNull(entry); + assert(entry.isBlob()); + assert.equal(entry.id().tostrS(), blobBId.tostrS()); + })); }); describe("hashFile", function () { it("breathing", co.wrap(function *() { From fa8e1c8d6537ac7e41760495797cb44a2aeeffcd Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Tue, 25 Sep 2018 14:39:29 -0400 Subject: [PATCH 206/402] Fixed bug when adding files to removed directory Need to make sure we don't base it off of the original tree. --- node/lib/util/tree_util.js | 15 ++++++++++----- node/test/util/tree_util.js | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/node/lib/util/tree_util.js b/node/lib/util/tree_util.js index e6af3d2b6..4ef86b390 100644 --- a/node/lib/util/tree_util.js +++ b/node/lib/util/tree_util.js @@ -174,10 +174,11 @@ exports.writeTree = co.wrap(function *(repo, baseTree, changes) { // This method does the real work, but assumes an already aggregated // directory structure. - const writeSubtree = co.wrap(function *(parentTree, subDir) { + const writeSubtree = co.wrap(function *(parentTree, subDir, basePath) { const builder = yield NodeGit.Treebuilder.create(repo, parentTree); for (let filename in subDir) { const entry = subDir[filename]; + const fullPath = path.join(basePath, filename); if (null === entry) { // Null means the entry was deleted. @@ -192,7 +193,11 @@ exports.writeTree = co.wrap(function *(repo, baseTree, changes) { else { let subtree; let treeEntry = null; - if (null !== parentTree) { + + // If we have a directory that was removed in `changes`, we do + // not want to base it on the original parent tree. + + if (null !== changes[fullPath] && null !== parentTree) { try { treeEntry = yield parentTree.entryByPath(filename); } @@ -204,10 +209,10 @@ exports.writeTree = co.wrap(function *(repo, baseTree, changes) { treeEntries.push(treeEntry); const treeId = treeEntry.id(); const curTree = yield repo.getTree(treeId); - subtree = yield writeSubtree(curTree, entry); + subtree = yield writeSubtree(curTree, entry, fullPath); } else { - subtree = yield writeSubtree(null, entry); + subtree = yield writeSubtree(null, entry, fullPath); } if (0 === subtree.entryCount()) { builder.remove(filename); @@ -224,7 +229,7 @@ exports.writeTree = co.wrap(function *(repo, baseTree, changes) { const id = builder.write(); return yield repo.getTree(id); }); - return yield writeSubtree(baseTree, directory); + return yield writeSubtree(baseTree, directory, ""); }); /** diff --git a/node/test/util/tree_util.js b/node/test/util/tree_util.js index eda4f9222..d5efd0995 100644 --- a/node/test/util/tree_util.js +++ b/node/test/util/tree_util.js @@ -328,6 +328,24 @@ describe("TreeUtil", function () { const entry = yield secondTree.entryByPath("foo"); assert.equal(entry.id().tostrS(), id.tostrS()); })); + it("rm directory but add new content", co.wrap(function *() { + const repo = yield makeRepo(); + const id = yield hashObject(repo, "xxxxxxxh"); + const firstTree = yield TreeUtil.writeTree(repo, null, { + "foo/bam": new Change(id, FILEMODE.BLOB), + }); + const secondTree = yield TreeUtil.writeTree(repo, firstTree, { + "foo": null, + "foo/bar/baz": new Change(id, FILEMODE.BLOB), + }); + let failed = false; + try { + yield secondTree.entryByPath("foo/bam"); + } catch (e) { + failed = true; + } + assert(failed, "it's there"); + })); it("implicitly overwrite blob with directory", co.wrap(function *() { const repo = yield makeRepo(); const blobAId = yield hashObject(repo, "xxxxxxxh"); From 8a72c300601153490ff4ab0e096047cf3838912f Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 26 Sep 2018 10:01:45 -0400 Subject: [PATCH 207/402] Added `GitUtil.getReference` --- node/lib/util/git_util.js | 16 ++++++++++++++++ node/test/util/git_util.js | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 177e9f4ee..f6fa41e00 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -1069,3 +1069,19 @@ git -C '${repo.path()}' merge-base ${id1} ${id2}`; } return result.stdout.split("\n").filter(x => x); }); + +/** + * Return the `Reference` object having the specified `name` in the specified + * `repo`, or null if no such reference exists. + */ +exports.getReference = co.wrap(function *(repo, name) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(name); + + try { + return yield NodeGit.Reference.lookup(repo, name); + } catch (e) { + // only way to tell `name` doesn't exist. + } + return null; +}); diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index 10271c319..9fb6053ab 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -1280,4 +1280,20 @@ describe("GitUtil", function () { })); }); }); + describe("getReference", function () { + it("good", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const result = yield GitUtil.getReference(repo, + "refs/heads/master"); + assert.instanceOf(result, NodeGit.Reference); + assert.equal(result.name(), "refs/heads/master"); + })); + it("bad", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const result = yield GitUtil.getReference(repo, "refs/foo"); + assert.isNull(result); + })); + }); }); From 44fc106061e0a2340186130a464da62ceedbf558 Mon Sep 17 00:00:00 2001 From: Zokir Tiliaev Date: Wed, 26 Sep 2018 11:10:22 -0400 Subject: [PATCH 208/402] Invoke post-commit hook after commit/amend --- node/lib/cmd/commit.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/node/lib/cmd/commit.js b/node/lib/cmd/commit.js index f0338278e..edf64af00 100644 --- a/node/lib/cmd/commit.js +++ b/node/lib/cmd/commit.js @@ -31,6 +31,7 @@ "use strict"; const co = require("co"); +const Hook = require("../util/hook"); /** * this module is the entrypoint for the `commit` command. @@ -119,6 +120,7 @@ const doCommit = co.wrap(function *(args) { args.file, args.interactive, GitUtil.editMessage); + yield Hook.execHook(repo, "post-commit"); }); const doAmend = co.wrap(function *(args) { @@ -140,6 +142,7 @@ const doAmend = co.wrap(function *(args) { args.all, args.interactive, args.no_edit ? null : GitUtil.editMessage); + yield Hook.execHook(repo, "post-commit"); }); /** From 0e25280865ce5a4594597a62f21a03f3255cc253 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 26 Sep 2018 21:12:09 -0400 Subject: [PATCH 209/402] Better logic to compute which submodules to push The previous logic was to rule out pushing submodules if their current commits (or their descendants) existed in one of a set of relevant branches (e.g., tracking branches). The downside of this strategy was that we couldn't deduce parent-child relationships of submodule commits that didn't exist locally. The new strategy is to include as candidates only submodules that were not changed between the commit being pushed and any of the (merge bases of the) candidate branches. Also fixed a bug where we were not constructing the ref name of a tracking branch correctly. --- node/lib/util/push.js | 109 ++++++++++++++++------------------------- node/test/util/push.js | 19 +++---- 2 files changed, 48 insertions(+), 80 deletions(-) diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 94ca0bcbd..6e929333a 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -51,21 +51,12 @@ const UserError = require("./user_error"); * For a given proposed push, return a map from submodule to sha, * excluding any submodules that the server likely already has. * - * Start with the set of submodules that are either open, or that - * exist in .git/modules. Populate a pushMap from submodule name to - * commit (SHA) registered in the meta-commit being pushed. + * Create a list of relevant branches, including any tracking branches + * associated with the one being pushed and the target branch. * - * If the push target exists in remotes/$remote/$branch, remove from - * pushMap, any entry where the commit for a submodule already exists - * in the target. - * - * Note that for "exists" we mean the target points to exactly the - * commit needed or a descendant of that commit. - * - * If the user is pushing a branch, B, further reduce the size of - * pushMap by removing commits referenced in the branches that B pulls - * from and/or pushes to by default, if they exist and are different - * from the target branch. + * Return in the map only those submodules that (1) exist locally, in + * `.git/modules` and (2) are changed between `commit` and the merge base of + * `commit` and each relevant branch. * * @async * @param {NodeGit.Repository} repo @@ -110,7 +101,7 @@ exports.getPushMap = co.wrap(function*(repo, remoteName, source, target, if (tracking !== null) { if (tracking.remoteName !== null) { trackingBranches.add( - `refs/remotes/${tracking.remoteName}/${target}`); + `refs/remotes/${tracking.remoteName}/${tracking.branchName}`); } if (tracking.pushRemoteName !== null) { trackingBranches.add( @@ -118,68 +109,52 @@ exports.getPushMap = co.wrap(function*(repo, remoteName, source, target, } } - let anyTrackingBranches = false; - for (const branch of trackingBranches) { + // List all the merge bases for all tracking branches. + + const bases = []; + + yield Array.from(trackingBranches).map(co.wrap(function *(branch) { let reference = null; try { reference = yield NodeGit.Reference.lookup(repo, branch); } catch (e) { // if this ref doesn't exist, OK - continue; + return; } - anyTrackingBranches = true; const id = reference.target(); - const trackingCommit = yield NodeGit.Commit.lookup( - repo, id); - const getter = SubmoduleUtil.getSubmoduleShasForCommit; - const submoduleShasForCommit = yield getter(repo, Object.keys(pushMap), - trackingCommit); - for (const sub of Object.keys(pushMap)) { - const sha = submoduleShasForCommit[sub]; - const subRepo = bareRepos[sub]; - if (sha === pushMap[sub]) { - delete pushMap[sub]; - continue; - } - let commitToPush; - try { - commitToPush = yield subRepo.getCommit(pushMap[sub]); - } catch (e) { - // We haven't fetched this commit in this submodule, - // so we can't push - delete pushMap[sub]; - continue; - } - let trackingCommit; - try { - trackingCommit = yield subRepo.getCommit(sha); - } catch (e) { - // We haven't fetched this commit in this submodule, - // so we can't do an ancestry check - continue; - } - const descendantCheck = NodeGit.Graph.descendantOf; - const isDescendant = yield descendantCheck(subRepo, - trackingCommit, - commitToPush); - if (isDescendant) { - delete pushMap[sub]; + const trackingCommit = yield NodeGit.Commit.lookup(repo, id); + const branchBases = yield GitUtil.mergeBases(repo, + trackingCommit, + commit); + bases.push(...branchBases); + })); + + // Then clear out all entries that weren't changed in each merge base. + + yield bases.map(co.wrap(function *(baseCommitSha) { + const baseCommit = yield repo.getCommit(baseCommitSha); + const changes = yield SubmoduleUtil.getSubmoduleChanges(repo, + commit, + baseCommit, + true); + // Remove any entry in `pushMap` that wasn't changed between `commit` + // and this merge base. + + for (let name in pushMap) { + if (!(name in changes)) { + delete pushMap[name]; } } - } + })); - if (!anyTrackingBranches) { - // We make sure we actually locally have the commits we want - // to push. If there were any tracking branches, this would - // have been handled above, but since there aren't, we've got - // to do it here. - for (const sub of Object.keys(pushMap)) { - const subRepo = bareRepos[sub]; - try { - yield subRepo.getCommit(pushMap[sub]); - } catch (e) { - delete pushMap[sub]; - } + // Make sure we have the commits we want to push. + + for (const sub of Object.keys(pushMap)) { + const subRepo = bareRepos[sub]; + try { + yield subRepo.getCommit(pushMap[sub]); + } catch (e) { + delete pushMap[sub]; } } return pushMap; diff --git a/node/test/util/push.js b/node/test/util/push.js index 1e30a4aef..46bc10193 100644 --- a/node/test/util/push.js +++ b/node/test/util/push.js @@ -239,18 +239,11 @@ describe("getPushMap", function () { d : "7", }, }, - "origin has a child (which we know is a child)" : { - initial: `sub=S:C7-1;C8-7;Bmaster=8| - x=S:C2-1 d=Ssub:8;C3-1 d=Ssub:7;Bmaster=3; - Rorigin=foo target=2; - Od`, - extraFetch: { - "d" : { - sub: "sub", - commits: ["8"], - }, - }, - source: "3", + "origin has different commit, but we didn't change anything": { + initial: ` +a=B:B3=3;C3-2 s=Sa:z;Cz-1;Bz=z|b=B| +x=S:B3=3;C2-1 s=Sa:1;Rorigin=a master=3;Rtarget=b;Bmaster=2 origin/master;Os`, + source: "refs/heads/master", expectedPushMap: {}, }, "origin is equal" : { @@ -276,7 +269,7 @@ describe("getPushMap", function () { if (parseInt(source) > 0) { sha = commitMap.reverseCommitMap[source]; } else { - sha = yield repo.getReference(source).target(); + sha = (yield repo.getReference(source)).target(); } const commit = yield repo.getCommit(sha); From 7c6be526d03d75870aca727c223971eb299fe3ac Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 26 Sep 2018 10:04:03 -0400 Subject: [PATCH 210/402] Improved performance, functionality of stitch * cache reading of submodule changes, in memory and on disk * bulk read and write notes when necessary * option to always bulk read when, e.g., doing a large operation * whitelist --- node/lib/stitch.js | 19 +- node/lib/util/stitch_util.js | 788 +++++++++++++++++++++++----------- node/test/util/stitch_util.js | 760 ++++++++++++++++++++++---------- 3 files changed, 1089 insertions(+), 478 deletions(-) diff --git a/node/lib/stitch.js b/node/lib/stitch.js index dc106dd4a..49c97d19e 100755 --- a/node/lib/stitch.js +++ b/node/lib/stitch.js @@ -34,6 +34,7 @@ const ArgumentParser = require("argparse").ArgumentParser; const co = require("co"); const StitchUtil = require("./util/stitch_util"); +const UserError = require("./util/user_error"); const description = `Stitch together the specified meta-repo commitish in \ the specified repo.`; @@ -110,6 +111,17 @@ In this history those paths will be relative to 'root', i.e., they will not \ be prefixed by it. This option implies '--skip-empty'.", }); +parser.addArgument(["--preload-cache"], { + required: false, + action: "storeConst", + constant: true, + defaultValue: false, + help: "Load, in one shot, information about previously converted commits \ +and submodule changes. Use this option when processing many commits to \ +avoid many individual note reads. Do not use this option when doing \ +incremental updates as the initial load time will be very slow.", +}); + co(function *() { const args = parser.parseArgs(); const keepRegex = (null === args.keep_as_submodule) ? @@ -123,6 +135,7 @@ co(function *() { keepAsSubmodule: keep, fetch: !args.no_fetch, skipEmpty: args.skip_empty || (null !== args.root), + preloadCache: args.preload_cache, }; if (!args.no_fetch && null === args.url) { console.error("URL is required unless '--no-fetch'"); @@ -142,7 +155,11 @@ co(function *() { options); } catch (e) { - console.error(e.stack); + if (e instanceof UserError) { + console.error(e.message); + } else { + console.error(e.stack); + } process.exit(-1); } }); diff --git a/node/lib/util/stitch_util.js b/node/lib/util/stitch_util.js index 29a024024..66f9ec62f 100644 --- a/node/lib/util/stitch_util.js +++ b/node/lib/util/stitch_util.js @@ -42,29 +42,120 @@ const SubmoduleConfigUtil = require("./submodule_config_util"); const SubmoduleUtil = require("./submodule_util"); const SyntheticBranchUtil = require("./synthetic_branch_util"); const TreeUtil = require("./tree_util"); +const UserError = require("./user_error"); const FILEMODE = NodeGit.TreeEntry.FILEMODE; -// This constant defines the maximum number of simple, multi-threaded parallel -// operations we'll perform. We allow the user to configure the number of -// parallel operations that we must shell out for, but this value is just to -// prevent us from running out of JavaScript heap. +// TODO: the `writeNotes` and `readNotes` methods should be moved to a utility +// for notes, if they're needed elsewhwere. -const maxParallel = 1000; +/** + * Write the specified `contents` to the note having the specified `refName` in + * the specified `repo`. + * + * Writing notes oneo-at-a-time is slow. This method let's you write them in + * bulk, far more efficiently. + * + * @param {NodeGit.Repository} repo + * @param {String} refName + * @param {Object} contents SHA to data + */ +exports.writeNotes = co.wrap(function *(repo, refName, contents) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(refName); + assert.isObject(contents); + + if (0 === Object.keys(contents).length) { + // Nothing to do if no contents; no point in making an empty commit or + // in making clients check themselves. + return; // RETURN + } + + // We're going to directly write the tree/commit for a new note containing + // `contents`. + + let currentCommit = null; + let currentTree = null; + const parents = []; + const ref = yield GitUtil.getReference(repo, refName); + if (null !== ref) { + currentCommit = yield repo.getCommit(ref.target()); + parents.push(currentCommit); + currentTree = yield currentCommit.getTree(); + } + const odb = yield repo.odb(); + const changes = {}; + const ODB_BLOB = 3; + const BLOB = NodeGit.TreeEntry.FILEMODE.BLOB; + const writeBlob = co.wrap(function *(sha) { + const content = contents[sha]; + const blobId = yield odb.write(content, content.length, ODB_BLOB); + changes[sha] = new TreeUtil.Change(blobId, BLOB); + }); + yield DoWorkQueue.doInParallel(Object.keys(contents), writeBlob); + + const newTree = yield TreeUtil.writeTree(repo, currentTree, changes); + const sig = yield ConfigUtil.defaultSignature(repo); + const commit = yield NodeGit.Commit.create(repo, + null, + sig, + sig, + null, + "git-meta updating notes", + newTree, + parents.length, + parents); + yield NodeGit.Reference.create(repo, refName, commit, 1, "updated"); +}); /** - * Return a string having the value of the specified `sha` with the "/" - * character inserted between the second and third characters of `sha`. The - * behavior is undefined unless `sha` is at least three characters long. + * Return the contents of the note having the specified `refName` in the + * specified `repo` or an empty object if no such note exists. * - * @param {String} sha - * @return {String} + * Reading notes one-at-a-time is slow. This method let's you read them all at + * once for a given ref. + * + * @param {NodeGit.Repository} repo + * @param {String} refName + * @return {Object} sha to content */ -exports.splitSha = function (sha) { - const pre = sha.slice(0, 2); - const post = sha.slice(2); - return `${pre}/${post}`; -}; +exports.readNotes = co.wrap(function *(repo, refName) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(refName); + + const ref = yield GitUtil.getReference(repo, refName); + if (null === ref) { + return {}; + } + const result = {}; + const commit = yield repo.getCommit(ref.target()); + const tree = yield commit.getTree(); + const entries = tree.entries(); + const processEntry = co.wrap(function *(e) { + const blob = yield e.getBlob(); + result[e.name()] = blob.toString(); + }); + yield DoWorkQueue.doInParallel(entries, processEntry); + return result; +}); + +/** + * @property {String} + */ +exports.whitelistNoteRef = "refs/notes/stitched/whitelist"; + +/** + * Return a set of meta-repo commits that are allowed to fail. + * + * @param {NodeGit.Repository} repo + * @return {Set} + */ +exports.readWhitelist = co.wrap(function *(repo) { + assert.instanceOf(repo, NodeGit.Repository); + + const notes = yield exports.readNotes(repo, exports.whitelistNoteRef); + return new Set(Object.keys(notes)); +}); /** * The name of the note used to record conversion information. @@ -74,33 +165,18 @@ exports.splitSha = function (sha) { exports.convertedNoteRef = "refs/notes/stitched/converted"; /** - * Write a note in the specified `repo` indicating that the commit having the - * specified `originalSha` was stitched into the specified `stitchedSha`, if - * provided, or that it was not processed (it's in completely skipped history) - * otherwise. + * Return the content of the note used to record that a commit was stitched + * into the specified `stitchedSha`, or, if `null === stitchedSha`, that the + * commit could not be stitched. * - * @param {NodeGit.Repository} repo - * @param {String} originalSha - * @param {String|null} stitchedSha + * @param {String|null} stitchedSha */ -exports.writeConvertedNote = co.wrap(function *(repo, - originalSha, - stitchedSha) { - assert.instanceOf(repo, NodeGit.Repository); - assert.isString(originalSha); +exports.makeConvertedNoteContent = function (stitchedSha) { if (null !== stitchedSha) { assert.isString(stitchedSha); } - const content = null === stitchedSha ? "" : stitchedSha; - const sig = yield ConfigUtil.defaultSignature(repo); - yield NodeGit.Note.create(repo, - exports.convertedNoteRef, - sig, - sig, - NodeGit.Oid.fromString(originalSha), - content, - 1); -}); + return null === stitchedSha ? "" : stitchedSha; +}; /** * Return the commit message to use for a stitch commit coming from the @@ -120,7 +196,11 @@ exports.makeStitchCommitMessage = function (metaCommit, subCommits) { const metaWhen = metaAuthor.when(); const metaTime = metaWhen.time(); const metaOffset = metaWhen.offset(); - const metaMessage = metaCommit.message(); + + // Sometimes (I don't know if this is libgit2 vs. git or what) otherwise + // matching messages may be missing line endings. + + const metaMessage = Commit.ensureEolOnLastLine(metaCommit.message()); let result = metaCommit.message(); // Add information from submodule commits that differs from the the @@ -141,12 +221,15 @@ exports.makeStitchCommitMessage = function (metaCommit, subCommits) { if (when.time() !== metaTime || when.offset() !== metaOffset) { whenText = `Date: ${Commit.formatCommitTime(when)}\n`; } - const message = commit.message(); + const message = Commit.ensureEolOnLastLine(commit.message()); let messageText = ""; if (message !== metaMessage) { messageText = "\n" + message; } if ("" !== authorText || "" !== whenText || "" !== messageText) { + if (!result.endsWith("\n")) { + result += "\n"; + } result += "\n"; result += `From '${subName}'\n`; result += authorText; @@ -164,23 +247,15 @@ exports.makeStitchCommitMessage = function (metaCommit, subCommits) { */ exports.referenceNoteRef = "refs/notes/stitched/reference"; - /** - * Write a note recording the specified originating `metaRepoSha` and - * `subCommits` for the stitched commit having the specified - * `stitchedCommitSha` in the specified `repo`. + * Return the content to be used for a note indicating that a stitched commit + * originated from the specified `metaRepoSha` and `subCommits`. * * @param {NodeGit.Repository} repo - * @param {String} stitchedCommitSha * @param {String} metaRepoSha * @param {Object} subCommits name to NodeGit.Commit */ -exports.writeReferenceNote = co.wrap(function *(repo, - stitchedCommitSha, - metaRepoSha, - subCommits) { - assert.instanceOf(repo, NodeGit.Repository); - assert.isString(stitchedCommitSha); +exports.makeReferenceNoteContent = function (metaRepoSha, subCommits) { assert.isString(metaRepoSha); assert.isObject(subCommits); const object = { @@ -190,36 +265,7 @@ exports.writeReferenceNote = co.wrap(function *(repo, Object.keys(subCommits).forEach(name => { object.submoduleCommits[name] = subCommits[name].id().tostrS(); }); - const content = JSON.stringify(object, null, 4); - const sig = yield ConfigUtil.defaultSignature(repo); - yield NodeGit.Note.create(repo, - exports.referenceNoteRef, - sig, - sig, - NodeGit.Oid.fromString(stitchedCommitSha), - content, - 1); -}); - -/** - * Return true if the specified `newTree` and `originalTree` represent the same - * value and false otherwise. `newTree` represents the same value as - * `originalTree` if they have the same id, or if `null === originalTree` and - * `newTree` has no entries. - * - * @param {NodeGit.Tree} newTree - * @param {NodeGit.Tree|null} originalTree - * @return {Bool} - */ -exports.isTreeUnchanged = function (newTree, originalTree) { - assert.instanceOf(newTree, NodeGit.Tree); - if (null !== originalTree) { - assert.instanceOf(originalTree, NodeGit.Tree); - } - if (null === originalTree) { - return 0 === newTree.entries().length; - } - return newTree.id().tostrS() === originalTree.id().tostrS(); + return JSON.stringify(object, null, 4); }; /** @@ -295,84 +341,6 @@ exports.listCommitsInOrder = function (entry, parentMap) { return Object.keys(parentMap).sort(compareCommits); }; -/** - * Return a map containing all converted SHAs in the specified `repo`. - */ -exports.listConvertedCommits = co.wrap(function *(repo) { - assert.instanceOf(repo, NodeGit.Repository); - - let ref; - try { - ref = yield NodeGit.Reference.lookup(repo, exports.convertedNoteRef); - } catch (e) { - // unfortunately, this is the only way to know the ref doesn't exist - return {}; - } - const result = {}; - const commit = yield repo.getCommit(ref.target()); - const tree = yield commit.getTree(); - const entries = tree.entries(); - const processEntry = co.wrap(function *(e) { - const blob = yield e.getBlob(); - const text = blob.toString(); - result[e.name()] = text === "" ? null : text; - }); - yield DoWorkQueue.doInParallel(entries, processEntry, maxParallel); - return result; -}); - -/** - * List, in order of least to most dependent, the specified `commit` and its - * ancestors in the specified `repo`. - * - * @param {NodeGit.Repository} repo - * @param {NodeGit.Commit} commit - * @param {Object} convertedCommits map from sha to converted - * @return {[NodeGit.Commit]} - */ -exports.listCommitsToStitch = co.wrap(function *(repo, - commit, - convertedCommits) { - assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(commit, NodeGit.Commit); - assert.isObject(convertedCommits); - - const toList = [commit]; - const allParents = {}; - const commitMap = {}; - - while (0 !== toList.length) { - const next = toList[toList.length - 1]; - const nextSha = next.id().tostrS(); - toList.pop(); - - // Skip processing commits we've seen. - - if (nextSha in allParents) { - continue; // CONTINUE - } - - // If it's converted, so, implicitly, are its parents. - - const converted = convertedCommits[nextSha]; - if (undefined !== converted) { - continue; // CONTINUE - } - const parents = yield next.getParents(); - const parentShas = []; - for (const parent of parents) { - toList.push(parent); - const parentSha = parent.id().tostrS(); - parentShas.push(parentSha); - } - allParents[nextSha] = parentShas; - commitMap[nextSha] = next; - } - const commitShas = exports.listCommitsInOrder(commit.id().tostrS(), - allParents); - return commitShas.map(sha => commitMap[sha]); -}); - /** * Return the `TreeUtilChange` object corresponding to the `.gitmodules` file * synthesized in the specified `repo` from an original commit that had the @@ -409,6 +377,133 @@ exports.computeModulesFile = co.wrap(function *(repo, return new TreeUtil.Change(id, FILEMODE.BLOB); }); +exports.changeCacheRef = "refs/notes/stitched/submodule-change-cache"; + +/** + * Add the specified `submoduleChanges` to the changed cache in the specified + * `repo`. + * + * @param {NodeGit.Repository} repo + * @param {Object} changes from sha to path to SubmoduleChange + */ +exports.writeSubmoduleChangeCache = co.wrap(function *(repo, changes) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isObject(changes); + + const cache = {}; + for (let sha in changes) { + const shaChanges = changes[sha]; + const cachedChanges = {}; + for (let path in shaChanges) { + const change = shaChanges[path]; + cachedChanges[path] = { + oldSha: change.oldSha, + newSha: change.newSha + }; + } + cache[sha] = JSON.stringify(cachedChanges, null, 4); + } + yield exports.writeNotes(repo, exports.changeCacheRef, cache); +}); + +/** + * Return the result of transforming the specified `map` so that each of the + * (string) values are replaced by the result of JSON parsing them. + * + * @param {Object} map string to string + * @return {Object} map string to object + */ +exports.parseNotes = function (map) { + const result = {}; + for (let key in map) { + result[key] = JSON.parse(map[key]); + } + return result; +}; + +/** + * Read the cached list of submodule changes per commit in the specified + * `repo`. + * + * @param {NodeGit.Repository} repo + * @return {Object} sha to path to SubmoduleChange-like object + */ +exports.readSubmoduleChangeCache = co.wrap(function *(repo) { + assert.instanceOf(repo, NodeGit.Repository); + + const cached = yield exports.readNotes(repo, exports.changeCacheRef); + return exports.parseNotes(cached); +}); + +/** + * Return a map of the submodule changes for the specified `commits` in the + * specified `repo`. + * + * @param {[NodeGit.Commit]} commits + * @return {Object} sha -> name -> SubmoduleChange + */ +exports.listSubmoduleChanges = co.wrap(function *(repo, commits) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isArray(commits); + + const result = yield exports.readSubmoduleChangeCache(repo); + let numListed = Object.keys(result).length; + console.log("Loaded", numListed, "from cache"); + + console.log("Listing submodule changes"); + + let toCache = {}; // bulk up changes that we'll write out periodically + let caching = false; // true if we're in the middle of writing the cache + const writeCache = co.wrap(function *() { + + // This code is reentrant, so we skip out if we're already writing some + // notes. Also, we don't want to write tiny note changes, so we + // arbitrarily wait until we've got 1,000 changes. + + if (caching || 1000 > Object.keys(toCache).length) { + return; // RETURN + } + + + caching = true; + const oldCache = toCache; + toCache = {}; + yield exports.writeSubmoduleChangeCache(repo, oldCache); + caching = false; + }); + + const listForCommit = co.wrap(function *(commit) { + const sha = commit.id().tostrS(); + if (sha in result) { + // was cached + return; // RETURN + } + const parents = yield commit.getParents(); + let parentCommit = null; + if (0 !== parents.length) { + parentCommit = parents[0]; + } + const changes = yield SubmoduleUtil.getSubmoduleChanges(repo, + commit, + parentCommit, + true); + result[sha] = changes; + toCache[sha] = changes; + ++numListed; + if (0 === numListed % 100) { + console.log("Listed", numListed, "of", commits.length); + } + yield writeCache(); + }); + yield DoWorkQueue.doInParallel(commits, listForCommit); + + // If there's anything left in the cache, write it out now. + + yield exports.writeSubmoduleChangeCache(repo, toCache); + + return result; +}); + /** * Return a map from submodule name to shas to list of objects containing the * fields: @@ -424,6 +519,7 @@ exports.computeModulesFile = co.wrap(function *(repo, * * @param {NodeGit.Repository} repo * @param {[NodeGit.Commit]} toFetch + * @param {Object} commitChanges sha->name->SubmoduleChange * @param {(String) => Boolean} keepAsSubmodule * @param {(String) => String|null} adjustPath * @param {Number} numParallel @@ -431,11 +527,13 @@ exports.computeModulesFile = co.wrap(function *(repo, */ exports.listFetches = co.wrap(function *(repo, toFetch, + commitChanges, keepAsSubmodule, adjustPath, numParallel) { assert.instanceOf(repo, NodeGit.Repository); assert.isArray(toFetch); + assert.isObject(commitChanges); assert.isFunction(keepAsSubmodule); assert.isFunction(adjustPath); assert.isNumber(numParallel); @@ -461,32 +559,6 @@ exports.listFetches = co.wrap(function *(repo, const result = {}; - const commitChanges = {}; - - let numListed = 0; - - // This bit is pretty slow, but we run it on only unconverted commits. If - // necessary, we could cache the submodule changes in a note. - - const listChanges = co.wrap(function *(commit) { - const parents = yield commit.getParents(); - let parentCommit = null; - if (0 !== parents.length) { - parentCommit = parents[0]; - } - const changes = yield SubmoduleUtil.getSubmoduleChanges(repo, - commit, - parentCommit, - true); - commitChanges[commit.id().tostrS()] = changes; - ++numListed; - if (0 === numListed % 100) { - console.log("Listed", numListed, "of", toFetch.length); - } - }); - - yield DoWorkQueue.doInParallel(toFetch, listChanges, maxParallel); - const addTodo = co.wrap(function *(commit, subName, sha) { let subTodos = result[subName]; if (undefined === subTodos) { @@ -519,6 +591,37 @@ exports.listFetches = co.wrap(function *(repo, return result; }); +/** + * Return true if any parent of the specified `commit` other than the first has + * the specified `sha` for the submodule having the specified `name` in the + * specified repo. + * + * @param {NodeGit.Repository} repo + * @param {NodEGit.Commit} commit + * @param {String} name + * @param {String} sha + */ +exports.sameInAnyOtherParent = co.wrap(function *(repo, commit, name, sha) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(commit, NodeGit.Commit); + assert.isString(name); + assert.isString(sha); + + const parents = yield commit.getParents(); + for (const parent of parents.slice(1)) { + const tree = yield parent.getTree(); + try { + const entry = yield tree.entryByPath(name); + if (entry.sha() === sha) { + return true; + } + } catch (e) { + // missing in parent + } + } + return false; +}); + /** * Write and return a new "stitched" commit for the specified `commit` in the * specified `repo`. If the specified `keepAsSubmodule` function returns true @@ -529,76 +632,77 @@ exports.listFetches = co.wrap(function *(repo, * applied to the paths of submodules that are stitched and those that are kept * as submodules. * - * Once the commit has been written, record a reference indicating the mapping - * from the originally to new commit in the form of - * `refs/stitched/converted/${sha}`, and clean the refs created in - * `refs/stitched/fetched` for this commit. - * * If the specified `skipEmpty` is true and the generated commit would be empty * because either: * * 1. It would have an empty tree and no parents * 2. It would have the same tree as its first parent * - * Then do not generate a commit; instead, return null. In this case, we - * record that the commit maps to its first (mapped) parent; if it has no - * parents we do not record a mapping. + * Then do not generate a commit; instead, return the first parent (and an + * empty `subCommits` map), or null if there are no parents. + * + * Allow commits in the specified `whitelist` to reference invalid submodule + * commits; skip those (submodule) commits. * * @param {NodeGit.Repository} repo * @param {NodeGit.Commit} commit + * @param {Object} subChanges path to SubmoduleChange * @param {[NodeGit.Commit]} parents * @param {(String) => Boolean} keepAsSubmodule * @param {(String) => String|null} adjustPath * @param {Bool} skipEmpty - * @return {NodeGit.Commit} + * @param {Set of String} whitelist + * @return {Object} + * @return {NodeGit.Commit} [return.stitchedCommit] + * @return {Object} return.subCommits path to NodeGit.Commit */ exports.writeStitchedCommit = co.wrap(function *(repo, commit, + subChanges, parents, keepAsSubmodule, adjustPath, - skipEmpty) { + skipEmpty, + whitelist) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); + assert.isObject(subChanges); assert.isArray(parents); assert.isFunction(keepAsSubmodule); assert.isFunction(adjustPath); assert.isBoolean(skipEmpty); - - let parentTree = null; - if (0 !== parents.length) { - const parentCommit = parents[0]; - parentTree = yield parentCommit.getTree(); - } - - let originalParent = null; - const originalParents = yield commit.getParents(); - if (0 !== originalParents.length) { - originalParent = originalParents[0]; - } - const subChanges = yield SubmoduleUtil.getSubmoduleChanges(repo, - commit, - originalParent, - true); - - // changes and additions + assert.instanceOf(whitelist, Set); let updateModules = false; // if any kept subs added or removed - const changes = {}; - const subCommits = {}; - const stitchSub = co.wrap(function *(name, sha) { + const changes = {}; // changes and additions + let subCommits = {}; // included submodule commits + const stitchSub = co.wrap(function *(name, oldName, sha) { let subCommit; try { subCommit = yield repo.getCommit(sha); } catch (e) { - console.error("On meta-commit", commit.id().tostrS(), - name, "is missing", sha); - throw e; + const metaSha = commit.id().tostrS(); + if (whitelist.has(metaSha)) { + return; // RETURN + } + throw new UserError(`\ +On meta-commit ${metaSha}, ${name} is missing ${sha}. +To add to allow this submodule change to be skipped, run: +git notes --ref ${exports.whitelistNoteRef} add -m skip ${metaSha}`); } const subTreeId = subCommit.treeId(); changes[name] = new TreeUtil.Change(subTreeId, FILEMODE.TREE); - subCommits[name] = subCommit; + + // Now, record this submodule change as introduced by this commit, + // unless it already existed in another of its parents, i.e., it was + // merged in. + + const alreadyExisted = + yield exports.sameInAnyOtherParent(repo, commit, oldName, sha); + if (!alreadyExisted) { + subCommits[name] = subCommit; + } }); function changeKept(name, newSha) { @@ -606,19 +710,21 @@ exports.writeStitchedCommit = co.wrap(function *(repo, changes[name] = new TreeUtil.Change(id, FILEMODE.COMMIT); } - const synthetics = []; // list of submodules whose refs need cleaned up - for (let name in subChanges) { + const mapped = adjustPath(name); + if (null === mapped) { + continue; // CONTINUE + } const newSha = subChanges[name].newSha; - changes[name] = null; + changes[mapped] = null; + if (keepAsSubmodule(name)) { updateModules = true; if (null !== newSha) { changeKept(name, newSha); } - } else if (null !== newSha && null !== adjustPath(name)) { - synthetics.push(name); - yield stitchSub(name, newSha); + } else if (null !== newSha) { + yield stitchSub(mapped, name, newSha); } } @@ -634,26 +740,19 @@ exports.writeStitchedCommit = co.wrap(function *(repo, changes[SubmoduleConfigUtil.modulesFileName] = content; } - // Adjust paths using the provided `adjustPath` function. + let newCommit = null; - const mappedChanges = {}; + if (!skipEmpty || 0 !== Object.keys(changes).length) { + // If we've got changes or are not skipping commits, we make one. - for (let name in changes) { - const mapped = adjustPath(name); - if (null !== mapped) { - mappedChanges[mapped] = changes[name]; + let parentTree = null; + if (0 !== parents.length) { + const parentCommit = parents[0]; + parentTree = yield parentCommit.getTree(); } - } - const newTree = yield TreeUtil.writeTree(repo, parentTree, mappedChanges); + const newTree = yield TreeUtil.writeTree(repo, parentTree, changes); - let newCommit = null; - let mappedSha = null; - if (skipEmpty && exports.isTreeUnchanged(newTree, parentTree)) { - if (0 !== parents.length) { - mappedSha = parents[0].id().tostrS(); - } - } else { const commitMessage = exports.makeStitchCommitMessage(commit, subCommits); const newCommitId = yield NodeGit.Commit.create( @@ -667,14 +766,17 @@ exports.writeStitchedCommit = co.wrap(function *(repo, parents.length, parents); newCommit = yield repo.getCommit(newCommitId); - mappedSha = newCommit.id().tostrS(); - yield exports.writeReferenceNote(repo, - mappedSha, - commit.id().tostrS(), - subCommits); + } else if (0 !== parents.length) { + // If we skip this commit, map to its parent to indicate that whenever + // we see this commit in the future, substitute its parent. + + newCommit = parents[0]; + subCommits = {}; } - yield exports.writeConvertedNote(repo, commit.id().tostrS(), mappedSha); - return newCommit; + return { + stitchedCommit: newCommit, + subCommits: subCommits, + }; }); /** @@ -746,6 +848,129 @@ exports.makeAdjustPathFunction = function (root) { }; }; +/** + * Return the SHA in the specified `content` or null if it represents no sha. + * + * @param {String} content + * @retiurm {String|null} + */ +exports.readConvertedContent = function (content) { + assert.isString(content); + return content === "" ? null : content; +}; + +/** + * Return the converted commit for the specified `sha` in the specified `repo`, + * null if it could not be converted, or undefined if it has not been + * attempted. + * + * @param {NodeGit.Repository} repo + * @param {String} sha + * @return {String|null|undefined} + */ +exports.readConvertedCommit = co.wrap(function *(repo, sha) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(sha); + const result = yield GitUtil.readNote(repo, exports.convertedNoteRef, sha); + return result === null ? + undefined : + exports.readConvertedContent(result.message()); +}); + +/** + * Return the previously converted commits in the specified `repo`. + * + * @param {Repo} + * @return {Object} map from string to string or null + */ +exports.readConvertedCommits = co.wrap(function *(repo) { + + // We use "" to indicate that a commit could not be converted. + + const result = yield exports.readNotes(repo, exports.convertedNoteRef); + for (let key in result) { + result[key] = exports.readConvertedContent(result[key]); + } + return result; +}); + +/** + * Return a function that can return the result of previous attempts to convert + * the specified `commit` in the specified `repo`. If there is a value in + * `cache`, return it, otherwise try to read the note and then cache that + * result. + * + * @param {NodeGit.Repository} repo + * @param {Object} cache + * @return {(String) => Promise} + */ +exports.makeGetConvertedCommit = function (repo, cache) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isObject(cache); + return co.wrap(function *(sha) { + assert.isString(sha); + + if (sha in cache) { + return cache[sha]; + } + const result = yield exports.readConvertedCommit(repo, sha); + cache[sha] = result; + return result; + }); +}; + +/** + * List, in order of least to most dependent, the specified `commit` and its + * ancestors in the specified `repo`. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + * @param {(String) => Promise(String|null|undefined)} + * @return {[NodeGit.Commit]} + */ +exports.listCommitsToStitch = co.wrap(function *(repo, + commit, + getConvertedCommit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(commit, NodeGit.Commit); + assert.isFunction(getConvertedCommit); + + const toList = [commit]; + const allParents = {}; + const commitMap = {}; + + while (0 !== toList.length) { + const next = toList[toList.length - 1]; + const nextSha = next.id().tostrS(); + toList.pop(); + + // Skip processing commits we've seen. + + if (nextSha in allParents) { + continue; // CONTINUE + } + + // If it's converted, so, implicitly, are its parents. + + const converted = yield getConvertedCommit(nextSha); + if (undefined !== converted) { + continue; // CONTINUE + } + const parents = yield next.getParents(); + const parentShas = []; + for (const parent of parents) { + toList.push(parent); + const parentSha = parent.id().tostrS(); + parentShas.push(parentSha); + } + allParents[nextSha] = parentShas; + commitMap[nextSha] = next; + } + const commitShas = exports.listCommitsInOrder(commit.id().tostrS(), + allParents); + return commitShas.map(sha => commitMap[sha]); +}); + /** * Stitch the repository at the specified `repoPath` starting with the * specified `commitish` and point the specified `targetBranchName` to the @@ -818,12 +1043,21 @@ exports.stitch = co.wrap(function *(repoPath, console.log("Listing previously converted commits."); - const convertedCommits = yield exports.listConvertedCommits(repo); + let convertedCommits = {}; + if (options.preloadCache) { + convertedCommits = yield exports.readConvertedCommits(repo); + } + const getConverted = exports.makeGetConvertedCommit(repo, + convertedCommits); console.log("listing unconverted ancestors of", commit.id().tostrS()); const commitsToStitch = - yield exports.listCommitsToStitch(repo, commit, convertedCommits); + yield exports.listCommitsToStitch(repo, commit, getConverted); + + console.log("listing submodule changes"); + + const changes = yield exports.listSubmoduleChanges(repo, commitsToStitch); const adjustPath = exports.makeAdjustPathFunction(joinRoot); @@ -833,6 +1067,7 @@ exports.stitch = co.wrap(function *(repoPath, console.log("listing fetches"); const fetches = yield exports.listFetches(repo, commitsToStitch, + changes, options.keepAsSubmodule, adjustPath, options.numParallel); @@ -853,6 +1088,36 @@ ${name}`; console.log("Now stitching"); let lastCommit = null; + let records = {}; + + const writeNotes = co.wrap(function *() { + console.log( + `Writing notes for ${Object.keys(records).length} commits.`); + const convertedNotes = {}; + const referenceNotes = {}; + for (let sha in records) { + const record = records[sha]; + const stitchedCommit = record.stitchedCommit; + const stitchedSha = + null === stitchedCommit ? null : stitchedCommit.id().tostrS(); + convertedNotes[sha] = + exports.makeConvertedNoteContent(stitchedSha); + if (null !== stitchedSha) { + referenceNotes[stitchedSha] = + exports.makeReferenceNoteContent(sha, record.subCommits); + } + } + yield exports.writeNotes(repo, + exports.referenceNoteRef, + referenceNotes); + yield exports.writeNotes(repo, + exports.convertedNoteRef, + convertedNotes); + records = {}; + }); + + const whitelist = yield exports.readWhitelist(repo); + for (let i = 0; i < commitsToStitch.length; ++i) { const next = commitsToStitch[i]; @@ -860,26 +1125,34 @@ ${name}`; const parents = yield next.getParents(); const newParents = []; for (const parent of parents) { - const newParentSha = convertedCommits[parent.id().tostrS()]; + const newParentSha = yield getConverted(parent.id().tostrS()); if (null !== newParentSha && undefined !== newParentSha) { const newParent = yield repo.getCommit(newParentSha); newParents.push(newParent); } } - const newCommit = yield exports.writeStitchedCommit( + const result = yield exports.writeStitchedCommit( repo, next, + changes[nextSha], newParents, options.keepAsSubmodule, adjustPath, - skipEmpty); + skipEmpty, + whitelist); + records[nextSha] = result; + const newCommit = result.stitchedCommit; const newSha = null === newCommit ? null : newCommit.id().tostrS(); convertedCommits[nextSha] = newSha; const desc = null === newCommit ? "skipped" : newCommit.id().tostrS(); const log = `\ Of [${commitsToStitch.length}] done [${i + 1}] : ${nextSha} -> ${desc}`; - console.log(log); + console.log(log); + + if (10000 <= Object.keys(records).length) { + yield writeNotes(); + } // If `writeStitchedCommit` returned null to indicate that it did not // make a commit (because it would have been empty), leave `lastCommit` @@ -887,6 +1160,15 @@ Of [${commitsToStitch.length}] done [${i + 1}] : ${nextSha} -> ${desc}`; lastCommit = newCommit || lastCommit; } + yield writeNotes(); + + // Delete submodule change cache if we succeeded; we won't need these + // submodules again. + + if (0 !== Object.keys(changes)) { + NodeGit.Reference.remove(repo, exports.changeCacheRef); + } + if (null !== lastCommit) { console.log( `Updating ${targetBranchName} to ${lastCommit.id().tostrS()}.`); diff --git a/node/test/util/stitch_util.js b/node/test/util/stitch_util.js index 9129610b2..8116c39c4 100644 --- a/node/test/util/stitch_util.js +++ b/node/test/util/stitch_util.js @@ -38,15 +38,13 @@ const RepoAST = require("../../lib/util/repo_ast"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); const RepoASTUtil = require("../../lib/util/repo_ast_util"); const StitchUtil = require("../../lib/util/stitch_util"); +const SubmoduleChange = require("../../lib/util/submodule_change"); const SubmoduleConfigUtil = require("../../lib/util/submodule_config_util"); +const SubmoduleUtil = require("../../lib/util/submodule_util"); const TreeUtil = require("../../lib/util/tree_util"); const FILEMODE = NodeGit.TreeEntry.FILEMODE; -function deSplitSha(sha) { - return sha.slice(0, 2) + sha.slice(3); -} - /** * Replace refs and notes with their equivalent logical mapping. */ @@ -81,7 +79,8 @@ function refMapper(actual, mapping) { const newNotes = {}; Object.keys(notes).forEach(refName => { const commits = notes[refName]; - if (StitchUtil.referenceNoteRef === refName) { + if (StitchUtil.referenceNoteRef === refName || + StitchUtil.changeCacheRef === refName) { // We can't check these in the normal way, so we have a // special test case instead. @@ -109,33 +108,101 @@ function refMapper(actual, mapping) { return result; } +/** + * Return the submodule changes in the specified `commit` in the specified + * `repo`. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + */ +const getCommitChanges = co.wrap(function *(repo, commit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(commit, NodeGit.Commit); + + const commitParents = yield commit.getParents(); + let parentCommit = null; + if (0 !== commitParents.length) { + parentCommit = commitParents[0]; + } + return yield SubmoduleUtil.getSubmoduleChanges(repo, + commit, + parentCommit, + true); +}); + describe("StitchUtil", function () { -describe("writeConvertedNote", function () { - it("with target sha", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo("S:C2-1;Bfoo=2"); +describe("writeNotes", function () { + it("with a parent", co.wrap(function *() { + const ref = "refs/notes/foo/bar"; + const written = yield RepoASTTestUtil.createRepo(` +S:C2-1;H=2;N ${ref} 1=hello`); const repo = written.repo; - const head = yield repo.getHeadCommit(); - const headSha = head.id().tostrS(); - const foo = yield repo.getBranchCommit("foo"); + const foo = yield repo.getHeadCommit(); const fooSha = foo.id().tostrS(); - const refName = StitchUtil.convertedNoteRef; - yield StitchUtil.writeConvertedNote(repo, headSha, fooSha); - const note = yield NodeGit.Note.read(repo, refName, headSha); - const message = note.message(); - assert.equal(message, fooSha); + const first = (yield foo.getParents())[0]; + const firstSha = first.id().tostrS(); + const newNotes = {}; + newNotes[fooSha] = "foobar"; + yield StitchUtil.writeNotes(repo, ref, newNotes); + const result = {}; + const shas = []; + yield NodeGit.Note.foreach(repo, ref, (_, sha) => { + shas.push(sha); + }); + yield shas.map(co.wrap(function *(sha) { + const note = yield NodeGit.Note.read(repo, ref, sha); + result[sha] = note.message(); + })); + const expected = newNotes; + newNotes[firstSha] = "hello"; + assert.deepEqual(result, expected); })); - it("without target sha", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo("S"); + it("no parents", co.wrap(function *() { + const ref = "refs/notes/foo/bar"; + const written = yield RepoASTTestUtil.createRepo("S:C2-1;H=2"); + const repo = written.repo; + const foo = yield repo.getHeadCommit(); + const fooSha = foo.id().tostrS(); + const first = (yield foo.getParents())[0]; + const firstSha = first.id().tostrS(); + const newNotes = {}; + newNotes[firstSha] = "hello"; + newNotes[fooSha] = "foobar"; + yield StitchUtil.writeNotes(repo, ref, newNotes); + const result = {}; + const shas = []; + yield NodeGit.Note.foreach(repo, ref, (_, sha) => { + shas.push(sha); + }); + yield shas.map(co.wrap(function *(sha) { + const note = yield NodeGit.Note.read(repo, ref, sha); + result[sha] = note.message(); + })); + const expected = newNotes; + assert.deepEqual(result, expected); + })); +}); +describe("readWhitelist", function () { + it("breathing", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(` +S:N refs/notes/stitched/whitelist 1=`); const repo = written.repo; const head = yield repo.getHeadCommit(); const headSha = head.id().tostrS(); - const refName = StitchUtil.convertedNoteRef; - yield StitchUtil.writeConvertedNote(repo, headSha, null); - const note = yield NodeGit.Note.read(repo, refName, headSha); - const message = note.message(); - assert.equal(message, ""); + const result = yield StitchUtil.readWhitelist(repo); + assert(result.has(headSha)); })); }); +describe("makeConvertedNoteContent", function () { + it("with target sha", function () { + const result = StitchUtil.makeConvertedNoteContent("foo"); + assert.equal(result, "foo"); + }); + it("without target sha", function () { + const result = StitchUtil.makeConvertedNoteContent(null); + assert.equal(result, ""); + }); +}); describe("makeStitchCommitMessage", function () { it("just a meta", co.wrap(function *() { const written = yield RepoASTTestUtil.createRepo("S"); @@ -370,21 +437,18 @@ Date: 1/1/1970, 02:00:03 200 assert.equal(result, expected); })); }); -describe("writeReferenceNote", function () { +describe("makeReferenceNoteContent", function () { it("breathing", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo("S:C2-1;Bfoo=2"); + const written = yield RepoASTTestUtil.createRepo("S"); const repo = written.repo; const head = yield repo.getHeadCommit(); const headSha = head.id().tostrS(); const subs = {}; subs["foo/bar"] = head; - yield StitchUtil.writeReferenceNote(repo, headSha, headSha, subs); - const refName = StitchUtil.referenceNoteRef; - const note = yield NodeGit.Note.read(repo, refName, headSha); - const content = note.message(); - const result = JSON.parse(content); + const result = + JSON.parse(StitchUtil.makeReferenceNoteContent("1", subs)); const expected = { - metaRepoCommit: headSha, + metaRepoCommit: "1", submoduleCommits: {}, }; expected.submoduleCommits["foo/bar"] = headSha; @@ -459,11 +523,12 @@ describe("listCommitsInOrder", function () { }); }); }); -describe("listConvertedCommits", function () { +describe("readNotes", function () { it("empty", co.wrap(function *() { const written = yield RepoASTTestUtil.createRepo("S:C2-1;Bfoo=2"); const repo = written.repo; - const result = yield StitchUtil.listConvertedCommits(repo); + const refName = "refs/notes/foo/bar"; + const result = yield StitchUtil.readNotes(repo, refName); assert.deepEqual(result, {}); })); it("breathing", co.wrap(function *() { @@ -473,117 +538,17 @@ describe("listConvertedCommits", function () { const headSha = head.id().tostrS(); const foo = yield repo.getBranchCommit("foo"); const fooSha = foo.id().tostrS(); - yield StitchUtil.writeConvertedNote(repo, headSha, fooSha); - yield StitchUtil.writeConvertedNote(repo, fooSha, null); - const result = yield StitchUtil.listConvertedCommits(repo); + const refName = "refs/notes/foo/bar"; + const sig = repo.defaultSignature(); + yield NodeGit.Note.create(repo, refName, sig, sig, fooSha, "foo", 1); + yield NodeGit.Note.create(repo, refName, sig, sig, headSha, "bar", 1); + const result = yield StitchUtil.readNotes(repo, refName); const expected = {}; - expected[headSha] = fooSha; - expected[fooSha] = null; + expected[fooSha] = "foo"; + expected[headSha] = "bar"; assert.deepEqual(result, expected); })); }); -describe("listCommitsToStitch", function () { - // We don't need to validate the ordering part; that is check in the - // test driver for 'listCommitsInOrder'. We need to validate basic - // functionality, and that we stop at previously converted commits. - - it("trivial", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo("S"); - const repo = written.repo; - const head = yield repo.getHeadCommit(); - const converted = yield StitchUtil.listConvertedCommits(repo); - const result = - yield StitchUtil.listCommitsToStitch(repo, head, converted); - const headSha = head.id().tostrS(); - const resultShas = result.map(c => c.id().tostrS()); - assert.deepEqual([headSha], resultShas); - })); - - it("skipped", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo("S"); - const repo = written.repo; - const head = yield repo.getHeadCommit(); - const headSha = head.id().tostrS(); - yield StitchUtil.writeConvertedNote(repo, headSha, null); - const converted = yield StitchUtil.listConvertedCommits(repo); - const result = - yield StitchUtil.listCommitsToStitch(repo, head, converted); - const resultShas = result.map(c => c.id().tostrS()); - assert.deepEqual([], resultShas); - })); - - it("with parents", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo( - "S:C3-2,4;C4-2;C2-1;C5-3,4;Bmaster=5"); - const repo = written.repo; - const head = yield repo.getHeadCommit(); - const converted = yield StitchUtil.listConvertedCommits(repo); - const result = - yield StitchUtil.listCommitsToStitch(repo, head, converted); - const expected = ["1", "2", "4", "3", "5"]; - const resultShas = result.map(c => { - const sha = c.id().tostrS(); - return written.commitMap[sha]; - }); - assert.deepEqual(expected, resultShas); - })); - - it("with parents and marker", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo( - "S:C3-2,4;C4-2;C2-1;C5-3,4;Bmaster=5"); - const repo = written.repo; - const head = yield repo.getHeadCommit(); - const twoSha = written.oldCommitMap["2"]; - yield StitchUtil.writeConvertedNote(repo, twoSha, twoSha); - const converted = yield StitchUtil.listConvertedCommits(repo); - const result = - yield StitchUtil.listCommitsToStitch(repo, head, converted); - const expected = ["4", "3", "5"]; - const resultShas = result.map(c => { - const sha = c.id().tostrS(); - return written.commitMap[sha]; - }); - assert.deepEqual(expected, resultShas); - })); -}); -describe("isTreeUnchanged", function () { - it("null original, non-empty", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo("S"); - const repo = written.repo; - const head = yield repo.getHeadCommit(); - const tree = yield head.getTree(); - const result = StitchUtil.isTreeUnchanged(tree, null); - assert.isFalse(result); - })); - it("null original, empty", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo("S"); - const repo = written.repo; - const emptyTree = yield TreeUtil.writeTree(repo, null, {}); - const result = StitchUtil.isTreeUnchanged(emptyTree, null); - assert.isTrue(result); - })); - it("same tree", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo("S"); - const repo = written.repo; - const head = yield repo.getHeadCommit(); - const tree = yield head.getTree(); - const result = StitchUtil.isTreeUnchanged(tree, tree); - assert.isTrue(result); - })); - it("different tree", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo("S:C2-1;Bfoo=2"); - const repo = written.repo; - const head = yield repo.getHeadCommit(); - const headTree = yield head.getTree(); - const fooCommit = yield repo.getBranchCommit("foo"); - const fooTree = yield fooCommit.getTree(); - const result = StitchUtil.isTreeUnchanged(headTree, fooTree); - assert.isFalse(result); - })); -}); -it("deSplitSha", function () { - assert.equal("1234", deSplitSha("12/34")); -}); describe("refMapper", function () { const Commit = RepoAST.Commit; const cases = { @@ -750,9 +715,6 @@ describe("refMapper", function () { }); }); -it("splitSha", function () { - assert.equal("34/56", StitchUtil.splitSha("3456")); -}); describe("computeModulesFile", function () { const cases = { "one kept": { @@ -799,6 +761,113 @@ describe("computeModulesFile", function () { })); }); }); +describe("writeSubmoduleChangeCache", function () { + it("breathing", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S:C2-1;H=2"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const next = (yield head.getParents())[0]; + const nextSha = next.id().tostrS(); + const changes = {}; + changes[headSha] = { + "foo/bar": new SubmoduleChange("1", "2") + }; + changes[nextSha] = { + "baz/bam": new SubmoduleChange("3", "4") + }; + yield StitchUtil.writeSubmoduleChangeCache(repo, changes); + const refName = StitchUtil.changeCacheRef; + const headNote = yield NodeGit.Note.read(repo, refName, headSha); + const headObj = JSON.parse(headNote.message()); + assert.deepEqual(headObj, { + "foo/bar": { oldSha: "1", newSha: "2"}, + }); + const nextNote = yield NodeGit.Note.read(repo, refName, nextSha); + const nextObj = JSON.parse(nextNote.message()); + assert.deepEqual(nextObj, { + "baz/bam": { oldSha: "3", newSha: "4" }, + }); + })); +}); +describe("parseNotes", function () { + it("breathing", function () { + const obj = { foo: "bar" }; + const str = JSON.stringify(obj, null, 4); + const input = { yay: str }; + const result = StitchUtil.parseNotes(input); + assert.deepEqual(result, { yay: obj }); + }); +}); +describe("readSubmoduleChangeCache", function () { + it("breathing", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S:C2-1;H=2"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const next = (yield head.getParents())[0]; + const nextSha = next.id().tostrS(); + const changes = {}; + changes[headSha] = { + "foo/bar": new SubmoduleChange("1", "2") + }; + changes[nextSha] = { + "baz/bam": new SubmoduleChange("3", "4") + }; + yield StitchUtil.writeSubmoduleChangeCache(repo, changes); + const read = yield StitchUtil.readSubmoduleChangeCache(repo); + const expected = {}; + expected[headSha] = { + "foo/bar": { + oldSha: "1", + newSha: "2", + }, + }; + expected[nextSha] = { + "baz/bam": { + oldSha: "3", + newSha: "4", + }, + }; + assert.deepEqual(read, expected); + })); +}); +describe("sameInAnyOtherParent", function () { + const cases = { + "no other parents": { + state: "B:Ca;C2-1 s=S.:a;Ba=a;Bmaster=2", + expected: false, + }, + "missing in other parent": { + state: "B:Ca;C3-1,2 s=S.:a;C2-1 foo=bar;Ba=a;Bmaster=3", + expected: false, + }, + "different in other parent": { + state: ` +B:Ca;Cb;C3-1,2 s=S.:a;C2-1 s=S.:b;Ba=a;Bmaster=3;Bb=b`, + expected: false, + }, + "same in other parent": { + state: "B:Ca;C3-1,2 s=S.:a;C2-1 s=S.:a;Ba=a;Bmaster=3", + expected: true, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(c.state); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const a = yield repo.getBranchCommit("a"); + const aSha = a.id().tostrS(); + const result = yield StitchUtil.sameInAnyOtherParent(repo, + head, + "s", + aSha); + assert.equal(result, c.expected); + })); + }); +}); describe("writeStitchedCommit", function () { const cases = { "trivial, no subs": { @@ -806,16 +875,17 @@ describe("writeStitchedCommit", function () { commit: "1", parents: [], keepAsSubmodule: () => false, + subCommits: {}, expected: ` -x=E:Cthe first commit#s ;Bstitched=s;N refs/notes/stitched/converted 1=s`, +x=E:Cthe first commit#s ;Bstitched=s`, }, "trivial, no subs, with a parent": { input: "x=S:C2;Bp=2", commit: "1", parents: ["2"], keepAsSubmodule: () => false, - expected: ` -x=E:Cthe first commit#s-2 ;Bstitched=s;N refs/notes/stitched/converted 1=s`, + subCommits: {}, + expected: `x=E:Cthe first commit#s-2 ;Bstitched=s`, }, "new stitched sub": { input: ` @@ -823,8 +893,8 @@ x=B:Ca;Cfoo#2-1 s=S.:a;Ba=a;Bmaster=2`, commit: "2", parents: [], keepAsSubmodule: () => false, - expected: ` -x=E:C*#s s/a=a;Bstitched=s;N refs/notes/stitched/converted 2=s`, + subCommits: { s: "a" }, + expected: `x=E:C*#s s/a=a;Bstitched=s`, }, "new stitched sub, with parent": { input: ` @@ -832,8 +902,8 @@ x=B:Ca;C2-1 s=S.:a;Ba=a;Bmaster=2`, commit: "2", parents: ["1"], keepAsSubmodule: () => false, - expected: ` -x=E:C*#s-1 s/a=a;Bstitched=s;N refs/notes/stitched/converted 2=s`, + subCommits: { s: "a" }, + expected: `x=E:C*#s-1 s/a=a;Bstitched=s`, }, "2 new stitched subs": { input: ` @@ -841,8 +911,9 @@ x=B:Ca;Cb;C2-1 s=S.:a,t=S.:b;Ba=a;Bb=b;Bmaster=2`, commit: "2", parents: [], keepAsSubmodule: () => false, + subCommits: { s: "a", t: "b" }, expected: ` -x=E:C*#s s/a=a,t/b=b;Bstitched=s;N refs/notes/stitched/converted 2=s`, +x=E:C*#s s/a=a,t/b=b;Bstitched=s`, }, "modified stitched": { input: ` @@ -850,8 +921,40 @@ x=B:Ca;Cb;Cc;C2-1 s=S.:a,t=S.:b;C3-2 s=S.:c;Ba=a;Bb=b;Bc=c;Bmaster=3`, commit: "3", parents: [], keepAsSubmodule: () => false, - expected: ` -x=E:C*#s s/c=c;Bstitched=s;N refs/notes/stitched/converted 3=s`, + subCommits: { s: "c" }, + expected: `x=E:C*#s s/c=c;Bstitched=s`, + }, + "deletion stitched": { + input: ` +x=B:Ca;Cb-a f=c,a; C2-1 s=S.:a;C3-2 s=S.:b;Bb=b;Bmaster=3; + Cr s/a=a;Br=r`, + commit: "3", + parents: ["r"], + keepAsSubmodule: () => false, + subCommits: { s: "b" }, + expected: `x=E:C*#s-r s/f=c,s/a;Bstitched=s`, + }, + "submodule deletion stitched": { + input: ` +x=B:Ca;Cb-a;Cc a/b=1,c/d=2;C2-1 s=S.:a,t/u=S.:c;C3-2 s=S.:b,t/u; + Bb=b;Bc=c;Bmaster=3; + Cr s/a=a,t/u/a/b=1,t/u/c/d=2;Br=r`, + commit: "3", + parents: ["r"], + keepAsSubmodule: () => false, + subCommits: { s: "b" }, + expected: `x=E:C*#s-r s/b=b,t/u/a/b,t/u/c/d;Bstitched=s`, + }, + "submodule deletion, but new subs added under it": { + input: ` +x=B:Ca;Cb-a;C2-1 s=S.:a,t=S.:b;C3-2 s=S.:b,t/u=Sa:a,t; + Bb=b;Bmaster=3; + Cr s/a=a,t/b=b;Br=r`, + commit: "3", + parents: ["r"], + keepAsSubmodule: () => false, + subCommits: { s: "b", "t/u": "a" }, + expected: `x=E:C*#s-r s/b=b,t/b,t/u/a=a;Bstitched=s`, }, "removed stitched": { input: ` @@ -859,8 +962,8 @@ x=B:Ca;Cb;Cc s/a=b;Cfoo#2-1 s=S.:a,t=S.:b;C3-2 s;Ba=a;Bb=b;Bc=c;Bmaster=3`, commit: "3", parents: ["c"], keepAsSubmodule: () => false, - expected: ` -x=E:Cs-c s/a;Bstitched=s;N refs/notes/stitched/converted 3=s`, + subCommits: {}, + expected: `x=E:Cs-c s/a;Bstitched=s`, }, "kept": { input: ` @@ -868,8 +971,8 @@ x=B:Ca;Cb;C2-1 s=S.:a,t=S.:b;Ba=a;Bb=b;Bmaster=2`, commit: "2", parents: [], keepAsSubmodule: (name) => "t" === name, - expected: ` -x=E:C*#s s/a=a,t=S.:b;Bstitched=s;N refs/notes/stitched/converted 2=s`, + subCommits: { s: "a" }, + expected: `x=E:C*#s s/a=a,t=S.:b;Bstitched=s`, }, "modified kept": { input: ` @@ -877,8 +980,8 @@ x=B:Ca;Cb;Ba=a;Bb=b;C2-1 s=S.:a;C3-2 s=S.:b;Cp foo=bar,s=S.:a;Bmaster=3;Bp=p`, commit: "3", parents: ["p"], keepAsSubmodule: (name) => "s" === name, - expected: ` -x=E:Cs-p s=S.:b;Bstitched=s;N refs/notes/stitched/converted 3=s`, + subCommits: {}, + expected: `x=E:Cs-p s=S.:b;Bstitched=s`, }, "removed kept": { input: ` @@ -886,8 +989,8 @@ x=B:Ca;Ba=a;C2-1 s=S.:a;C3-2 s;Cp foo=bar,s=S.:a;Bmaster=3;Bp=p`, commit: "3", parents: ["p"], keepAsSubmodule: (name) => "s" === name, - expected: ` -x=E:Cs-p s;Bstitched=s;N refs/notes/stitched/converted 3=s`, + subCommits: {}, + expected: `x=E:Cs-p s;Bstitched=s`, }, "empty commit, but not skipped": { input: ` @@ -895,8 +998,8 @@ x=B:Ca;Cfoo#2 ;Ba=a;Bmaster=2;Bfoo=1`, commit: "2", parents: ["1"], keepAsSubmodule: () => false, - expected: ` -x=E:C*#s-1 ;Bstitched=s;N refs/notes/stitched/converted 2=s`, + subCommits: {}, + expected: `x=E:C*#s-1 ;Bstitched=s`, }, "empty commit, skipped": { input: ` @@ -906,19 +1009,16 @@ x=B:Ca;Cfoo#2 ;Ba=a;Bmaster=2;Bfoo=1`, keepAsSubmodule: () => false, skipEmpty: true, isNull: true, - expected: ` -x=E:N refs/notes/stitched/converted 2=`, + subCommits: {}, }, "skipped empty, with parent": { input: ` -x=B:Ca;C2-1 ;Ba=a;Bmaster=2`, +x=B:Ca;C2-1 ;Ba=a;Bmaster=2;Bstitched=1`, commit: "2", parents: ["1"], keepAsSubmodule: () => false, skipEmpty: true, - isNull: true, - expected: ` -x=E:N refs/notes/stitched/converted 2=1`, + subCommits: {}, }, "adjusted to new path": { input: ` @@ -927,8 +1027,47 @@ x=B:Ca;Cfoo#2-1 s=S.:a;Ba=a;Bmaster=2`, parents: [], keepAsSubmodule: () => false, adjustPath: () => "foo/bar", - expected: ` -x=E:C*#s foo/bar/a=a;Bstitched=s;N refs/notes/stitched/converted 2=s`, + subCommits: { "foo/bar": "a" }, + expected: `x=E:C*#s foo/bar/a=a;Bstitched=s`, + }, + "missing commit": { + input: ` +a=B|b=B:Cb-1;Bb=b|x=U:C3-2 s=Sa:b;H=3`, + commit: "3", + parents: [], + keepAsSubmodule: () => false, + fails: true, + }, + "missing commit, whitelisted": { + input: ` +a=B|b=B:Cb-1;Bb=b|x=U:C3-2 s=Sa:b;H=3`, + commit: "3", + parents: [], + keepAsSubmodule: () => false, + whitelist: ["3"], + subCommits: {}, + expected: `x=E:Cs ;Bstitched=s`, + }, + "omit, from subCommits, non-new commits (existed in one parent)": { + input: ` +a=B:Ca a=a;Ba=a| +x=S:C2-1 s=Sa:a;C3-1 t=Sa:a;C4-2,3 t=Sa:a,u=Sa:a;H=4;Ba=a`, + commit: "4", + parents: [], + keepAsSubmodule: () => false, + subCommits: { "u": "a" }, + expected: `x=E:C*#s u/a=a,t/a=a;Bstitched=s`, + }, + "omit, from subCommits, with adjusted path": { + input: ` +a=B:Ca a=a;Ba=a| +x=S:C2-1 s=Sa:a;C3-1 t=Sa:a;C4-2,3 t=Sa:a,u=Sa:a;H=4;Ba=a`, + commit: "4", + parents: [], + keepAsSubmodule: () => false, + subCommits: { "foo/bar/u": "a" }, + adjustPath: (path) => `foo/bar/${path}`, + expected: `x=E:C*#s foo/bar/u/a=a,foo/bar/t/a=a;Bstitched=s`, }, }; Object.keys(cases).forEach(caseName => { @@ -944,26 +1083,44 @@ x=E:C*#s foo/bar/a=a;Bstitched=s;N refs/notes/stitched/converted 2=s`, })); const adjustPath = c.adjustPath || ((x) => x); const skipEmpty = c.skipEmpty || false; + + const changes = yield getCommitChanges(x, commit); + const whitelist = new Set((c.whitelist || []).map(c => { + return revMap[c]; + })); const stitch = yield StitchUtil.writeStitchedCommit( x, commit, + changes, parents, c.keepAsSubmodule, adjustPath, - skipEmpty); + skipEmpty, + whitelist); + const subCommits = {}; + for (let path in stitch.subCommits) { + const commit = stitch.subCommits[path]; + const sha = commit.id().tostrS(); + subCommits[path] = maps.commitMap[sha]; + } + assert.deepEqual(subCommits, c.subCommits); if (true === c.isNull) { - assert.isNull(stitch); + assert(null === stitch.stitchedCommit, + "stitchedCommit should have been null"); return; } else { // Need to root the commit we wrote yield NodeGit.Reference.create(x, "refs/heads/stitched", - stitch, + stitch.stitchedCommit, 1, "stitched"); } + const stitchSha = stitch.stitchedCommit.id().tostrS(); const commitMap = {}; - commitMap[stitch.id().tostrS()] = "s"; + if (!(stitchSha in maps.commitMap)) { + commitMap[stitch.stitchedCommit.id().tostrS()] = "s"; + } return { commitMap, }; @@ -976,56 +1133,56 @@ x=E:C*#s foo/bar/a=a;Bstitched=s;N refs/notes/stitched/converted 2=s`, }); })); }); -}); -it("messaging", co.wrap(function *() { - - const state = "B:Ca;C2-1 s=S.:a;Ba=a;Bmaster=2"; - const written = yield RepoASTTestUtil.createRepo(state); - const repo = written.repo; - const head = yield repo.getHeadCommit(); - const stitch = yield StitchUtil.writeStitchedCommit(repo, - head, - [], - () => false, - (x) => x, - false); - const subCommitRef = yield NodeGit.Reference.lookup(repo, - "refs/heads/a"); - const subCommit = yield repo.getCommit(subCommitRef.target()); - const subCommits = { - s: subCommit, - }; - const expected = StitchUtil.makeStitchCommitMessage(head, subCommits); - const actual = stitch.message(); - assert.deepEqual(expected.split("\n"), actual.split("\n")); -})); -it("reference note", co.wrap(function *() { + it("messaging", co.wrap(function *() { - const state = "B:Ca;C2-1 s=S.:a;Ba=a;Bmaster=2"; - const written = yield RepoASTTestUtil.createRepo(state); - const repo = written.repo; - const head = yield repo.getHeadCommit(); - const stitch = yield StitchUtil.writeStitchedCommit(repo, - head, - [], - () => false, - (x) => x, - false); - const note = yield NodeGit.Note.read(repo, - StitchUtil.referenceNoteRef, - stitch); - const subCommitRef = yield NodeGit.Reference.lookup(repo, - "refs/heads/a"); - const subCommit = yield repo.getCommit(subCommitRef.target()); - const expected = { - metaRepoCommit: head.id().tostrS(), - submoduleCommits: { - s: subCommit.id().tostrS(), - }, - }; - const actual = JSON.parse(note.message()); - assert.deepEqual(actual, expected); -})); + const state = "B:Ca;C2-1 s=S.:a;Ba=a;Bmaster=2"; + const written = yield RepoASTTestUtil.createRepo(state); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const changes = yield getCommitChanges(repo, head); + const whitelist = new Set(); + const stitch = yield StitchUtil.writeStitchedCommit(repo, + head, + changes, + [], + () => false, + (x) => x, + false, + whitelist); + const expected = StitchUtil.makeStitchCommitMessage(head, + stitch.subCommits); + const stitchedCommit = stitch.stitchedCommit; + const actual = stitchedCommit.message(); + assert.deepEqual(expected.split("\n"), actual.split("\n")); + })); +}); +describe("listSubmoduleChanges", function () { + it("empty", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const result = yield StitchUtil.listSubmoduleChanges(repo, [head]); + const expected = {}; + expected[head.id().tostrS()] = {}; + assert.deepEqual(result, expected); + })); + it("with one", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S:C2-1 s=Sa:1;H=2"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const expected = {}; + const parents = yield head.getParents(); + const parent = parents[0]; + const firstSha = parent.id().tostrS(); + expected[head.id().tostrS()] = { + "s": new SubmoduleChange(null, firstSha), + }; + expected[firstSha] = {}; + const result = + yield StitchUtil.listSubmoduleChanges(repo, [head, parent]); + assert.deepEqual(result, expected); + })); +}); describe("listFetches", function () { const cases = { "trivial": { @@ -1111,8 +1268,11 @@ describe("listFetches", function () { return yield repo.getCommit(sha); })); const adjustPath = c.adjustPath || ((x) => x); + const changes = yield StitchUtil.listSubmoduleChanges(repo, + toFetch); const result = yield StitchUtil.listFetches(repo, toFetch, + changes, c.keepAsSubmodule, adjustPath, c.numParallel); @@ -1219,6 +1379,156 @@ describe("makeAdjustPathFunction", function () { }); }); }); +describe("readConvertedContent", function () { + it("empty", function () { + assert.equal(StitchUtil.readConvertedContent(""), null); + }); + it("not empty", function () { + assert.equal(StitchUtil.readConvertedContent("1"), "1"); + }); +}); +describe("readConvertedCommit", function () { + it("missing", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(`S`); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const result = yield StitchUtil.readConvertedCommit(repo, headSha); + assert.isUndefined(result); + })); + it("there", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(` +S:N refs/notes/stitched/converted 1=`); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const result = yield StitchUtil.readConvertedCommit(repo, headSha); + assert.isNull(result); + })); +}); +describe("readConvertedCommits", function () { + it("breathing", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(` +S:C2-1;B2=2;N refs/notes/stitched/converted 1=; + N refs/notes/stitched/converted 2=1`); + const repo = written.repo; + const one = yield repo.getHeadCommit(); + const oneSha = one.id().tostrS(); + const two = yield repo.getBranchCommit("2"); + const twoSha = two.id().tostrS(); + const result = yield StitchUtil.readConvertedCommits(repo); + const expected = {}; + expected[oneSha] = null; + expected[twoSha] = "1"; + assert.deepEqual(result, expected); + })); +}); +describe("makeGetConvertedCommit", function () { + it("empty", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(`S`); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const fun = StitchUtil.makeGetConvertedCommit(repo, {}); + const result = yield fun(headSha); + assert.isUndefined(result); + })); + it("got one", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(` +S:N refs/notes/stitched/converted 1=`); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const fun = StitchUtil.makeGetConvertedCommit(repo, {}); + const result = yield fun(headSha); + assert.isNull(result); + + // Now delete and make sure we're remembering the result. + + NodeGit.Reference.remove(repo, StitchUtil.convertedNoteRef); + const nextResult = yield fun(headSha); + assert.isNull(nextResult); + })); + it("got one from cache", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(`S`); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const cache = {}; + cache[headSha] = null; + const fun = StitchUtil.makeGetConvertedCommit(repo, cache); + const result = yield fun(headSha); + assert.isNull(result); + })); +}); +describe("listCommitsToStitch", function () { + // We don't need to validate the ordering part; that is check in the + // test driver for 'listCommitsInOrder'. We need to validate basic + // functionality, and that we stop at previously converted commits. + + it("trivial", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const getConv = StitchUtil.makeGetConvertedCommit(repo, {}); + const result = + yield StitchUtil.listCommitsToStitch(repo, head, getConv); + const headSha = head.id().tostrS(); + const resultShas = result.map(c => c.id().tostrS()); + assert.deepEqual([headSha], resultShas); + })); + + it("skipped", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const notes = {}; + notes[headSha] = StitchUtil.makeConvertedNoteContent(null); + yield StitchUtil.writeNotes(repo, StitchUtil.convertedNoteRef, notes); + const getConv = StitchUtil.makeGetConvertedCommit(repo, {}); + const result = + yield StitchUtil.listCommitsToStitch(repo, head, getConv); + const resultShas = result.map(c => c.id().tostrS()); + assert.deepEqual([], resultShas); + })); + + it("with parents", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo( + "S:C3-2,4;C4-2;C2-1;C5-3,4;Bmaster=5"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const getConv = StitchUtil.makeGetConvertedCommit(repo, {}); + const result = + yield StitchUtil.listCommitsToStitch(repo, head, getConv); + const expected = ["1", "2", "4", "3", "5"]; + const resultShas = result.map(c => { + const sha = c.id().tostrS(); + return written.commitMap[sha]; + }); + assert.deepEqual(expected, resultShas); + })); + + it("with parents and marker", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo( + "S:C3-2,4;C4-2;C2-1;C5-3,4;Bmaster=5"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const twoSha = written.oldCommitMap["2"]; + const notes = {}; + notes[twoSha] = StitchUtil.makeConvertedNoteContent(twoSha); + yield StitchUtil.writeNotes(repo, StitchUtil.convertedNoteRef, notes); + const getConv = StitchUtil.makeGetConvertedCommit(repo, {}); + const result = + yield StitchUtil.listCommitsToStitch(repo, head, getConv); + const expected = ["4", "3", "5"]; + const resultShas = result.map(c => { + const sha = c.id().tostrS(); + return written.commitMap[sha]; + }); + assert.deepEqual(expected, resultShas); + })); +}); describe("stitch", function () { const cases = { "breathing": { @@ -1228,6 +1538,7 @@ x=B:Ca;Cfoo#2-1 s=S.:a;Ba=a;Bmaster=2`, targetBranchName: "my-branch", keepAsSubmodule: () => false, numParallel: 8, + preloadCache: true, expected: ` x=E:C*#s2-s1 s/a=a;C*#s1 ; Bmy-branch=s2; @@ -1243,6 +1554,7 @@ N refs/notes/stitched/converted 1=s1`, const options = { numParallel: c.numParallel, keepAsSubmodule: c.keepAsSubmodule, + preloadCache: c.preloadCache, }; if ("fetch" in c) { options.fetch = c.fetch; From 4b1b9f191826766c2cf76a86221aaa46f25b352a Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 26 Sep 2018 10:07:03 -0400 Subject: [PATCH 211/402] Added tool to destitch commits --- node/lib/destitch.js | 83 ++++ node/lib/util/destitch_util.js | 507 +++++++++++++++++++++ node/test/util/destitch_util.js | 759 ++++++++++++++++++++++++++++++++ 3 files changed, 1349 insertions(+) create mode 100755 node/lib/destitch.js create mode 100644 node/lib/util/destitch_util.js create mode 100644 node/test/util/destitch_util.js diff --git a/node/lib/destitch.js b/node/lib/destitch.js new file mode 100755 index 000000000..2d257272d --- /dev/null +++ b/node/lib/destitch.js @@ -0,0 +1,83 @@ +#!/usr/bin/env node +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +const ArgumentParser = require("argparse").ArgumentParser; +const co = require("co"); + +const DestitchUtil = require("./util/destitch_util"); +const GitUtil = require("./util/git_util"); +const UserError = require("./util/user_error"); + +const description = `\ +Create a meta-repo commit from a stitched history and print its SHA.`; + +const parser = new ArgumentParser({ + addHelp: true, + description: description +}); + +parser.addArgument(["-r", "--remote"], { + required: true, + type: "string", + help: `The name of the remote for the meta-repo`, +}); + +parser.addArgument(["-b", "--branch"], { + required: true, + type: "string", + help: "If set BRANCH to the destitched commit", +}); + +parser.addArgument(["commitish"], { + type: "string", + help: "the commit to destitch", +}); + +co(function *() { + const args = parser.parseArgs(); + try { + const repo = yield GitUtil.getCurrentRepo(); + yield DestitchUtil.destitch(repo, + args.commitish, + args.remote, + `refs/heads/${args.branch}`); + + } + catch (e) { + if (e instanceof UserError) { + console.error(e.message); + } else { + console.error(e.stack); + } + process.exit(-1); + } +}); diff --git a/node/lib/util/destitch_util.js b/node/lib/util/destitch_util.js new file mode 100644 index 000000000..c1d15f4f9 --- /dev/null +++ b/node/lib/util/destitch_util.js @@ -0,0 +1,507 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const NodeGit = require("nodegit"); +const path = require("path"); + +const DoWorkQueue = require("./do_work_queue"); +const GitUtil = require("./git_util"); +const StitchUtil = require("./stitch_util"); +const SubmoduleConfigUtil = require("./submodule_config_util"); +const SubmoduleUtil = require("./submodule_util"); +const SyntheticBranchUtil = require("./synthetic_branch_util"); +const TreeUtil = require("./tree_util"); +const UserError = require("./user_error"); + +const FILEMODE = NodeGit.TreeEntry.FILEMODE; + +/** + * @property {String} local record of stitched commits + */ +exports.localReferenceNoteRef = "refs/notes/stitched/local-reference"; + +/** + * Return the destitched data corresponding to the specified `stitchedSha` in + * the specified `repo` if it can be found in `refs/notes/stitched/reference` + * or `refs/notes/stitched/local-reference` or in the specified + * `newlyStitched`, and null if it has not been destitched. + * + * @param {NodeGit.Repository} repo + * @param {Object} newlyStitched + * @param {String} stitchedSha + * @return {Object} + * @return {String} metaRepoCommit + * @return {Object} subCommits name to sha + */ +exports.getDestitched = co.wrap(function *(repo, newlyStitched, stitchedSha) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isObject(newlyStitched); + assert.isString(stitchedSha); + if (stitchedSha in newlyStitched) { + return newlyStitched[stitchedSha]; + } + let note = yield GitUtil.readNote(repo, + exports.localReferenceNoteRef, + stitchedSha); + if (null === note) { + note = yield GitUtil.readNote(repo, + StitchUtil.referenceNoteRef, + stitchedSha); + } + return note && JSON.parse(note.message()); +}); + +/** + * Return the name in the specified `submodules` to which the specified + * `filename` maps or null if it maps to no submodule. A filename maps to a + * submodule if the submodule's path contains the filename. * + * @param {Object} submodules name to URL + * @param {String} filename + * @return {String|null} + */ +exports.findSubmodule = function (submodules, filename) { + assert.isObject(submodules); + assert.isString(filename); + + while ("." !== filename) { + if (filename in submodules) { + return filename; // RETURN + } + filename = path.dirname(filename); + } + return null; +}; + +/** + * Return the names of the specified `submodules` in the specified `repo` that + * are affected by the changes introduced in the specified `stitchedCommit` as + * compared against the specified `parentCommit`. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} submodules name to URL + * @param {NodeGit.Commit} stitchedCommit + * @param {NodeGit.Commit} parentCommit + * @return {Set String} + */ +exports.computeChangedSubmodules = co.wrap(function *(repo, + submodules, + stitchedCommit, + parentCommit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isObject(submodules); + assert.instanceOf(stitchedCommit, NodeGit.Commit); + assert.instanceOf(parentCommit, NodeGit.Commit); + + // We're going to take a diff between `stitchedCommit` and `parentCommit`, + // and return a set of all submodule names that map to the modified files. + + const result = new Set(); + const tree = yield stitchedCommit.getTree(); + const parentTree = yield parentCommit.getTree(); + const diff = yield NodeGit.Diff.treeToTree(repo, parentTree, tree, null); + const numDeltas = diff.numDeltas(); + const modulesFileName = SubmoduleConfigUtil.modulesFileName; + for (let i = 0; i < numDeltas; ++i) { + const delta = diff.getDelta(i); + const file = delta.newFile(); + const filename = file.path(); + if (modulesFileName === filename) { + continue; // CONTINUE + } + const subname = exports.findSubmodule(submodules, filename); + if (null === subname) { + throw new UserError(`\ +Could not map ${filename} to a submodule, and additions are not supported.`); + } + result.add(subname); + } + return result; +}); + +/** + * Make a destitched commit created by applying changes to the specified + * `changedSubmodules` from the specified `stitchedCommit` on top of the + * specified `metaRepoCommits` in the specified `repo`. Use the specified + * `subUrls` to compute a new `.gitmodules` file when necessary (e.g., a + * submodule is deleted). Return an object describing commits that were + * created. The behavior is undefined all commits referenced in + * `changedSubmodules` exist in `repo`. + * + * @param {NodeGit.Repository} repo + * @param {[NodeGit.Commit]} metaRepoCommits + * @param {NodeGit.Commit} stitchedCommit + * @param {Object} subUrls name to URL + * @param {Object} changedSubmodules name to SHA + * @return {Object} + * @return {String} return.metaRepoCommit + * @return {Object} subCommits name to String + */ +exports.makeDestitchedCommit = co.wrap(function *(repo, + metaRepoCommits, + stitchedCommit, + changedSubmodules, + subUrls) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isArray(metaRepoCommits); + assert.instanceOf(stitchedCommit, NodeGit.Commit); + assert.isObject(changedSubmodules); + assert.isObject(subUrls); + + const subCommits = {}; // created submodule commits + const tree = yield stitchedCommit.getTree(); + let baseTree = null; // tree of first parent if there is one + if (0 !== metaRepoCommits.length) { + const metaRepoCommit = metaRepoCommits[0]; + baseTree = yield metaRepoCommit.getTree(); + } + const author = stitchedCommit.author(); + const committer = stitchedCommit.committer(); + const messageEncoding = stitchedCommit.messageEncoding(); + const message = stitchedCommit.message(); + const changes = {}; // changes from the meta parent's tree + const commitUrls = Object.assign({}, subUrls); + + const computeSubChanges = co.wrap(function *(sub) { + const sha = changedSubmodules[sub]; + let stitchedEntry = null; + try { + stitchedEntry = yield tree.entryByPath(sub); + } catch(e) { + delete commitUrls[sub]; + changes[sub] = null; + return; // RETURN + } + const mode = stitchedEntry.filemode(); + if (FILEMODE.TREE !== mode) { + // Changes must reside in submodules; we're not going to put files + // directly in the meta-repo. + // TBD: allow COMMIT changes. + + throw new UserError(`\ +Change change of mode ${mode} to '${sub}' is not supported.`); + } + + // Now we have an entry that's a tree. We're going to make a new + // commit whose contents are exactly that tree. + + const treeId = stitchedEntry.id(); + const stitchedTree = yield NodeGit.Tree.lookup(repo, treeId); + const parent = yield repo.getCommit(sha); + const commitId = yield NodeGit.Commit.create(repo, + null, + author, + committer, + messageEncoding, + message, + stitchedTree, + 1, + [parent]); + const commit = yield repo.getCommit(commitId); + subCommits[sub] = commit.id().tostrS(); + changes[sub] = new TreeUtil.Change(commit, FILEMODE.COMMIT); + }); + yield DoWorkQueue.doInParallel(Object.keys(changedSubmodules), + computeSubChanges); + + // Update the modules file if we've removed one. + + const modulesContent = SubmoduleConfigUtil.writeConfigText(commitUrls); + const modulesId = yield GitUtil.hashObject(repo, modulesContent); + changes[SubmoduleConfigUtil.modulesFileName] = + new TreeUtil.Change(modulesId, FILEMODE.BLOB); + + // Now we make a new commit using the changes we've computed. + + const newTree = yield TreeUtil.writeTree(repo, baseTree, changes); + const commitId = yield NodeGit.Commit.create(repo, + null, + author, + committer, + messageEncoding, + message, + newTree, + metaRepoCommits.length, + metaRepoCommits); + return { + metaRepoCommit: commitId.tostrS(), + subCommits: subCommits, + }; +}); + +/** + * Destitch the specified `stitchedCommit` and (recursively) any of its + * ancestors, but do nothing if the SHA for `stitchedCommit` exists in the + * specified `newlyDestitched` map or in reference notes. If a destitched + * commit is created, record it in `newlyDestitched`. Both of these map from + * SHA to { metaRepoCommit, subCommits: submodule name to commit}. Use the + * specified `baseUrl` to fetch needed meta-repo commits and to resolve + * submodule URLs. Return the SHA of the destitched version of + * `stitchedCommit`. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} stitchedCommit + * @param {String} baseUrl + * @param {Object} newlyDestitched + * @return {String} + */ +exports.destitchChain = co.wrap(function *(repo, + stitchedCommit, + baseUrl, + newlyDestitched) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(stitchedCommit, NodeGit.Commit); + assert.isString(baseUrl); + assert.isObject(newlyDestitched); + + const stitchedSha = stitchedCommit.id().tostrS(); + const done = yield exports.getDestitched(repo, + newlyDestitched, + stitchedSha); + if (null !== done) { + // Nothing to do here if it's been destitched. + + return done.metaRepoCommit; // RETURN + } + + // Make sure all destitched parents are available and load their commits. + + const parents = yield stitchedCommit.getParents(); + if (0 === parents.length) { + throw new UserError(`Cannot destitch orphan commit ${stitchedSha}`); + } + const destitchedParents = []; + for (const stitchedParent of parents) { + const stitchedSha = stitchedParent.id().tostrS(); + const destitched = yield exports.getDestitched(repo, + newlyDestitched, + stitchedSha); + let destitchedSha; + if (null === destitched) { + // If a parent has yet to be destiched, recurse. + + destitchedSha = yield exports.destitchChain(repo, + stitchedParent, + baseUrl, + newlyDestitched); + } else { + // If the parent was already destitched, make sure its meta-repo + // commit is present,. + + destitchedSha = destitched.metaRepoCommit; + console.log(`Fetching meta-repo commit, ${destitchedSha}.`); + yield GitUtil.fetchSha(repo, baseUrl, destitchedSha); + } + const commit = yield repo.getCommit(destitchedSha); + destitchedParents.push(commit); + } + + const firstParent = destitchedParents[0]; + const urls = + yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, firstParent); + const changes = yield exports.computeChangedSubmodules(repo, + urls, + stitchedCommit, + parents[0]); + + const names = Array.from(changes); + const shas = yield SubmoduleUtil.getSubmoduleShasForCommit(repo, + names, + firstParent); + + // Make sure we have all the commits needed for each changed submodule, and + // do the fetch before processing ancestor commits to minimize the number + // of fetches. + + const fetchSub = co.wrap(function *(name) { + const url = SubmoduleConfigUtil.resolveUrl(baseUrl, urls[name]); + const sha = shas[name]; + if (undefined !== sha) { + console.log(`Fetching submodule ${name}.`); + yield GitUtil.fetchSha(repo, url, sha); + } + }); + yield DoWorkQueue.doInParallel(names, fetchSub); + + const result = yield exports.makeDestitchedCommit(repo, + destitchedParents, + stitchedCommit, + shas, + urls); + newlyDestitched[stitchedSha] = result; + return result.metaRepoCommit; +}); + +/** + * Push synthetic refs for the submodule commits described in the specified + * `newCommits` created for the specified `destitchedCommit` in the specified + * `repo`. Use the specified `baseUrl` to resolve relative URLS + * + * TBD: Minimize the number of pushes so that we do not push a commit and its + * ancestor. + * + * @param {NodeGit.Repository} repo + * @param {String} baseUrl + * @param {NodeGit.Commit} destitchedCommit + * @param {Object} commits map from sha to { metaRepoCommit, subCommits } + */ +exports.pushSyntheticRefs = co.wrap(function *(repo, + baseUrl, + destitchedCommit, + newCommits) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(baseUrl); + assert.instanceOf(destitchedCommit, NodeGit.Commit); + assert.isObject(newCommits); + + const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit( + repo, + destitchedCommit); + + const toPush = []; // Array of url and sha + Object.keys(newCommits).forEach(sha => { + const subs = newCommits[sha].subCommits; + Object.keys(subs).forEach(sub => { + const subUrl = SubmoduleConfigUtil.resolveSubmoduleUrl(baseUrl, + urls[sub]); + toPush.push({ + url: subUrl, + sha: subs[sub], + }); + }); + }); + const pushOne = co.wrap(function *(push) { + const sha = push.sha; + const refName = SyntheticBranchUtil.getSyntheticBranchForCommit(sha); + yield GitUtil.push(repo, push.url, sha, refName, true, true); + }); + yield DoWorkQueue.doInParallel(toPush, pushOne); +}); + +/** + * Record the specified `newCommits` to local reference notes in the specified + * `repo`. + * + * @param {NodeGit.Repository} repo + * @param {Object} newCommits sha to { metaRepoCommit, subCommits} + */ +exports.recordLocalNotes = co.wrap(function *(repo, newCommits) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isObject(newCommits); + const content = {}; + Object.keys(newCommits).forEach(sha => { + content[sha] = JSON.stringify(newCommits[sha], null, 4); + }); + yield StitchUtil.writeNotes(repo, exports.localReferenceNoteRef, content); +}); + +/** + * Create a destitched version of the specified `commitish`, including any + * ancestors for which destitched versions cannot be found, in the specified + * `repo`. Use the specified `metaRemote` to fetch neede meta-repo commits and + * to resolve submodule URLs. Use the notes stored in + * `refs/notes/stitched/reference` and `refs/notes/stitched/local-reference` to + * match source meta-repo commits to stitched commits. The behavior is + * undefined if `stitchedCommit` or any of its (transitive) ancestors is a root + * commit (having no parents) that cannot be mapped to a destitched commit in + * `refs/notes/stitched/reference`. Create and push synthetic references to + * root all sub-module commits created as part of this operation. If the + * specified `targetRefName` is provided, create or update the reference with + * that name to point to the destitched version of `stitchedCommit`. Write, to + * `refs/notes/stitched/local-reference` a record of the destitched notes + * generated and return an object that describes them. Throw a `UserError` if + * `commitish` cannot be resolved or if `metaRemoteName` does not map to a + * valid remote. + * + * @param {NodeGit.Repository} repo + * @param {String} commitish + * @param {String} metaRemoteName + * @param {String|null} targetRefName + * @return {Object} map from stitched sha to + * { metaRepoCommit, submoduleCommits (from name to sha)} + */ +exports.destitch = co.wrap(function *(repo, + commitish, + metaRemoteName, + targetRefName) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(commitish); + assert.isString(metaRemoteName); + if (null !== targetRefName) { + assert.isString(targetRefName); + } + + const annotated = yield GitUtil.resolveCommitish(repo, commitish); + if (null === annotated) { + throw new UserError(`\ +Could not resolve '${commitish}' to a commit.`); + } + const commit = yield repo.getCommit(annotated.id()); + if (!(yield GitUtil.isValidRemoteName(repo, metaRemoteName))) { + throw new UserError(`Invalid remote name: '${metaRemoteName}'.`); + } + const remote = yield NodeGit.Remote.lookup(repo, metaRemoteName); + const baseUrl = remote.url(); + + const newlyStitched = {}; + + console.log("Destitching"); + + const result = yield exports.destitchChain(repo, + commit, + baseUrl, + newlyStitched); + const resultCommit = yield repo.getCommit(result); + + // Push synthetic-refs + + console.log("Pushing synthetic refs"); + yield exports.pushSyntheticRefs(repo, baseUrl, resultCommit, newlyStitched); + + // Record local notes + + console.log("Recording local note"); + yield exports.recordLocalNotes(repo, newlyStitched); + + // Update the branch if requested. + + if (null !== targetRefName) { + console.log(`Updating ${targetRefName}`); + yield NodeGit.Reference.create(repo, + targetRefName, + resultCommit, + 1, + "destitched"); + } + return newlyStitched; +}); diff --git a/node/test/util/destitch_util.js b/node/test/util/destitch_util.js new file mode 100644 index 000000000..515eeca5e --- /dev/null +++ b/node/test/util/destitch_util.js @@ -0,0 +1,759 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const NodeGit = require("nodegit"); + +const DestitchUtil = require("../../lib/util/destitch_util"); +const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const StitchUtil = require("../../lib/util/stitch_util"); +const SyntheticBranchUtil = require("../../lib/util/synthetic_branch_util"); +const UserError = require("../../lib/util/user_error"); + +/** + * Replace refs and notes with their equivalent logical mapping and otherwise + * handle things we can't map well with our shorthand. + */ +function refMapper(actual, mapping) { + const fetchedSubRe = /(commits\/)(.*)/; + const commitMap = mapping.commitMap; + let result = {}; + + // Map refs + + Object.keys(actual).forEach(repoName => { + const ast = actual[repoName]; + const refs = ast.refs; + const newRefs = {}; + Object.keys(refs).forEach(refName => { + const ref = refs[refName]; + const fetchedSubMatch = fetchedSubRe.exec(refName); + if (null !== fetchedSubMatch) { + const sha = fetchedSubMatch[2]; + const logical = commitMap[sha]; + const newRefName = refName.replace(fetchedSubRe, + `$1${logical}`); + newRefs[newRefName] = ref; + return; // RETURN + } + newRefs[refName] = ref; + }); + + // Wipe out notes, we validate these by expicitly reading and + // processing the new notes. + + result[repoName] = ast.copy({ + refs: newRefs, + notes: {}, + }); + }); + return result; +} + +/** + * Return the result of mapping logical commit IDs to actual commit SHAs in the + * specified `map` using the specified `revMap`. + * + * @param {Object} revMap + * @param {Object} map SHA -> { metaRepoCommit, subCommits } + */ +function mapDestitched(revMap, map) { + const result = {}; + Object.keys(map).forEach(id => { + const commitData = map[id]; + const sha = revMap[id]; + const metaSha = revMap[commitData.metaRepoCommit]; + const subCommits = {}; + Object.keys(commitData.subCommits).forEach(sub => { + const subId = commitData.subCommits[sub]; + subCommits[sub] = revMap[subId]; + }); + result[sha] = { + metaRepoCommit: metaSha, + subCommits: subCommits, + }; + }); + return result; +} + +describe("destitch_util", function () { +describe("findSubmodule", function () { + const cases = { + "empty": { + subs: {}, + filename: "foo", + expected: null, + }, + "direct match": { + subs: { + "foo/bar": "", + "bam": "", + }, + filename: "foo/bar", + expected: "foo/bar", + }, + "inside it": { + subs: { + "foo/bar": "", + "bam": "", + }, + filename: "foo/bar/bam/baz/ttt.xx", + expected: "foo/bar", + }, + "missed": { + subs: { + "f/bar": "", + "bam": "", + }, + filename: "foo/bar/bam/baz/ttt.xx", + expected: null, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, function () { + const result = DestitchUtil.findSubmodule(c.subs, c.filename); + assert.equal(result, c.expected); + }); + }); +}); +describe("computeChangedSubmodules", function () { + const cases = { + "no changes": { + state: "S", + subs: { "foo/bar": "" }, + stitched: "1", + parent: "1", + expected: [], + }, + "sub not found": { + state: "S:C2-1 heya=baa;B2=2", + subs: { "foo/bar": "" }, + stitched: "2", + parent: "1", + expected: [], + fails: true, + }, + "sub found": { + state: "S:C2-1 hey/there/bob=baa;B2=2", + subs: { "hey/there": "" }, + stitched: "2", + parent: "1", + expected: ["hey/there"], + }, + "removal": { + state: "S:C2-1 hey/there/bob=baa;C3-2 hey/there/bob;B3=3", + subs: { "hey/there": "" }, + stitched: "3", + parent: "2", + expected: ["hey/there"], + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo(c.state); + const repo = written.repo; + const revMap = written.oldCommitMap; + const stitched = yield repo.getCommit(revMap[c.stitched]); + const parent = yield repo.getCommit(revMap[c.parent]); + let result; + let error; + try { + result = yield DestitchUtil.computeChangedSubmodules(repo, + c.subs, + stitched, + parent); + } catch (e) { + error = e; + } + if (undefined !== error) { + if (!c.fails || !(error instanceof UserError)) { + throw error; + } + return; // RETURN + } + assert(!c.fails, "did not fail"); + const sorted = Array.from(result).sort(); + assert.deepEqual(sorted, c.expected.sort()); + })); + }); +}); +describe("makeDestitchedCommit", function () { + const cases = { + "no changes": { + state: "x=S", + metaRepoCommits: [], + stitchedCommit: "1", + changedSubmodules: {}, + subUrls: {}, + expected: "x=E:Cthe first commit#d ;Bd=d", + }, + "with a base commit": { + state: "x=S", + metaRepoCommits: ["1"], + stitchedCommit: "1", + changedSubmodules: {}, + subUrls: {}, + expected: "x=E:Cthe first commit#d-1 ;Bd=d", + }, + "bad sub": { + state: "x=S:C2 foo=bar;B2=2", + metaRepoCommits: [], + stitchedCommit: "2", + changedSubmodules: { + "foo": "1", + }, + subUrls: {"foo": "bam"}, + fails: true, + }, + "deletion": { + state: "x=S:C2-1 s=Sa:1;Cw s/a=ss;Cx-w s/a;H=2;Bx=x", + metaRepoCommits: ["2"], + stitchedCommit: "x", + changedSubmodules: { "s": "1" }, + subUrls: {s: "a"}, + expected: "x=E:Cd-2 s;Bd=d", + }, + "actual change": { + state: ` +a=B:Ca foo=bar;Ba=a| +x=S:C2-1 s=Sa:a;B2=2;Cx s/foo=bam;Bx=x;Ba=a`, + metaRepoCommits: ["2"], + stitchedCommit: "x", + changedSubmodules: { s: "a" }, + subUrls: { s: "a" }, + expected: `x=E:Cs-a foo=bam;Cd-2 s=Sa:s;Bd=d;Bs=s` + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + const destitcher = co.wrap(function *(repos, maps) { + const repo = repos.x; + const revMap = maps.reverseCommitMap; + const metaRepoCommits = + yield c.metaRepoCommits.map(co.wrap(function *(metaRepoCommit) { + const sha = revMap[metaRepoCommit]; + return yield repo.getCommit(sha); + })); + const stitchedSha = revMap[c.stitchedCommit]; + const stitchedCommit = yield repo.getCommit(stitchedSha); + const subUrls = {}; + Object.keys(c.subUrls).forEach(sub => { + const url = c.subUrls[sub]; + subUrls[sub] = maps.reverseUrlMap[url] || url; + }); + const changedSubmodules = {}; + Object.keys(c.changedSubmodules).forEach(sub => { + const sha = c.changedSubmodules[sub]; + changedSubmodules[sub] = revMap[sha]; + }); + const result = yield DestitchUtil.makeDestitchedCommit( + repo, + metaRepoCommits, + stitchedCommit, + changedSubmodules, + subUrls); + const commits = {}; + + // Need to anchor the destitched commit + + yield NodeGit.Reference.create(repo, + "refs/heads/d", + result.metaRepoCommit, + 1, + "destitched"); + + commits[result.metaRepoCommit] = "d"; + for (let sub in result.subCommits) { + const sha = result.subCommits[sub]; + commits[sha] = sub; + yield NodeGit.Reference.create(repo, + `refs/heads/${sub}`, + sha, + 1, + "destitched"); + } + return { + commitMap: commits, + }; + }); + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.state, + c.expected, + destitcher, + c.fails); + })); + }); +}); +describe("destitchChain", function () { + const cases = { + "already stitched, noop": { + state: "x=S", + commit: "1", + url: "foo", + already: { "1": { metaRepoCommit: "1", subCommits: {}}}, + newly: {}, + expectedNewly: {}, + result: "1", + }, + "newly stitched, noop": { + state: "x=S", + commit: "1", + url: "foo", + already: {}, + newly: { "1": { metaRepoCommit: "1", subCommits: {}}}, + expectedNewly: { "1": { metaRepoCommit: "1", subCommits: {}}}, + result: "1", + }, + "bad, orphan": { + state: ` +a=B:Ca;Ba=a| +x=B:C2 foo/bar=Sa:a;B2=2;Cy foo/bar/a=baz;By=y`, + commit: "y", + url: "a", + already: {}, + newly: {}, + expectedNewly: {}, + fails: true, + }, + "destitch one": { + state: ` +a=B:Ca;Ba=a;C2 s=Sa:a;B2=2| +x=B:Cx s/a=bam;Cy-x s/a=baz;By=y`, + commit: "y", + url: "a", + already: { "x": { metaRepoCommit: "2", subCommits: {}}}, + newly: {}, + expectedNewly: { + "y": { + metaRepoCommit: "d.y", + subCommits: { + "s": "s.y.s", + }, + }, + }, + result: "d.y", + expected: ` +x=E:Cs.y.s-a a=baz;Cd.y-2 s=Sa:s.y.s;Bs.y.s=s.y.s;Bd.y=d.y`, + }, + "destitch one, some unchanged": { + state: ` +a=B:Ca;Ba=a;C2 s=Sa:a,t=Sa:a;B2=2| +x=B:Cx s/a=a,t/a=a;Cy-x s/a=baz;By=y`, + commit: "y", + url: "a", + already: { "x": { metaRepoCommit: "2", subCommits: {}}}, + newly: {}, + expectedNewly: { + "y": { + metaRepoCommit: "d.y", + subCommits: { + "s": "s.y.s", + }, + }, + }, + result: "d.y", + expected: ` +x=E:Cs.y.s-a a=baz;Cd.y-2 s=Sa:s.y.s;Bs.y.s=s.y.s;Bd.y=d.y`, + }, + "destitch with an ancestor": { + state: ` +a=B:Ca;Ba=a;C2 s=Sa:a;B2=2| +x=B:Cx s/a=bam;Cy-x s/a=baz;Cz-y s/a=ya;Bz=z`, + commit: "z", + url: "a", + already: { "x": { metaRepoCommit: "2", subCommits: {}}}, + newly: {}, + expectedNewly: { + "y": { + metaRepoCommit: "d.y", + subCommits: { + "s": "s.y.s", + }, + }, + "z": { + metaRepoCommit: "d.z", + subCommits: { + "s": "s.z.s", + }, + }, + }, + result: "d.z", + expected: ` +x=E:Cs.y.s-a a=baz;Cd.y-2 s=Sa:s.y.s;Bs.y.s=s.y.s;Bd.y=d.y; + Cs.z.s-s.y.s a=ya;Cd.z-d.y s=Sa:s.z.s;Bd.z=d.z;Bs.z.s=s.z.s`, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + const destitcher = co.wrap(function *(repos, maps) { + const repo = repos.x; + const revMap = maps.reverseCommitMap; + const commitMap = maps.commitMap; + const sha = revMap[c.commit]; + const commit = yield repo.getCommit(sha); + const baseUrl = maps.reverseUrlMap[c.url] || c.url; + const already = mapDestitched(revMap, c.already); + for (let sha in already) { + already[sha] = JSON.stringify(already[sha], null, 4); + } + yield StitchUtil.writeNotes(repo, + StitchUtil.referenceNoteRef, + already); + const originalNewly = mapDestitched(revMap, c.newly); + const newly = Object.assign({}, originalNewly); + const result = yield DestitchUtil.destitchChain(repo, + commit, + baseUrl, + newly); + + // clean up the ref so we don't get cofused when checking final + // state. + + NodeGit.Reference.remove(repo, StitchUtil.referenceNoteRef); + const commits = {}; + + // Anchor generated commits and generate commit map. + + const actualNewly = {}; + yield Object.keys(newly).map(co.wrap(function *(stitchedSha) { + const commitInfo = newly[stitchedSha]; + const stitchedId = commitMap[stitchedSha]; + const newMetaSha = commitInfo.metaRepoCommit; + let newMetaId; + const inOriginal = stitchedSha in originalNewly; + + // Only make ref and add to commit map if the commit was + // created. + + if (inOriginal) { + newMetaId = commitMap[newMetaSha]; + } else { + newMetaId = `d.${stitchedId}`; + yield NodeGit.Reference.create(repo, + `refs/heads/${newMetaId}`, + newMetaSha, + 1, + "destitched"); + commits[newMetaSha] = newMetaId; + } + const actualSubCommits = {}; + const subCommits = commitInfo.subCommits; + yield Object.keys(subCommits).map(co.wrap(function *(sub) { + const newSubSha = subCommits[sub]; + let newSubId; + if (inOriginal) { + newSubId = commitMap[newSubSha]; + } else { + newSubId = `s.${stitchedId}.${sub}`; + yield NodeGit.Reference.create( + repo, + `refs/heads/${newSubId}`, + newSubSha, + 1, + "destitched"); + commits[newSubSha] = newSubId; + } + actualSubCommits[sub] = newSubId; + })); + actualNewly[stitchedId] = { + metaRepoCommit: newMetaId, + subCommits: actualSubCommits, + }; + })); + const resultId = commitMap[result] || commits[result]; + assert.equal(resultId, c.result); + assert.deepEqual(actualNewly, c.expectedNewly); + return { + commitMap: commits, + }; + }); + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.state, + c.expected, + destitcher, + c.fails); + })); + }); +}); +describe("getDestitched", function () { + it("nowhere", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const result = yield DestitchUtil.getDestitched(repo, {}, headSha); + assert.isNull(result); + })); + it("in newly", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const newly = {}; + newly[headSha] = { + metaCommit: "1", + subCommits: {}, + }; + const result = yield DestitchUtil.getDestitched(repo, newly, headSha); + assert.deepEqual(result, newly[headSha]); + })); + it("in local", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const destitched = { + metaCommit: "1", + subCommits: {}, + }; + const sig = repo.defaultSignature(); + const refName = DestitchUtil.localReferenceNoteRef; + const data = JSON.stringify(destitched, null, 4); + yield NodeGit.Note.create(repo, refName, sig, sig, headSha, data, 1); + const result = yield DestitchUtil.getDestitched(repo, {}, headSha); + assert.deepEqual(result, destitched); + })); + it("in remote", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const destitched = { + metaCommit: "1", + subCommits: {}, + }; + const sig = repo.defaultSignature(); + const refName = StitchUtil.referenceNoteRef; + const data = JSON.stringify(destitched, null, 4); + yield NodeGit.Note.create(repo, refName, sig, sig, headSha, data, 1); + const result = yield DestitchUtil.getDestitched(repo, {}, headSha); + assert.deepEqual(result, destitched); + })); +}); +describe("pushSyntheticRefs", function () { + it("breathing", co.wrap(function *() { + + // We're going to set up a typical looking state where we've destitched + // two commits: + // + // s -- first to destitch + // t -- second + // 2 -- destitched version of s, introduces commit b in sub s + // 3 -- destitched version of t, introduces commit c in sub t + // + // After the push, we should see synthetic refs in b and c. + + const state = ` +a=B|b=B|c=B| +x=S:Cs s/b=b;Ct-s t/c=c;Bs=s;Bt=t; + Cb;Cc;Bb=b;Bc=c;C2-1 s=S../b:b;C3-2 t=S../c:c;H=3;B2=2`; + const written = yield RepoASTTestUtil.createMultiRepos(state); + const repos = written.repos; + const x = repos.x; + const sCommit = yield x.getBranchCommit("s"); + const sSha = sCommit.id().tostrS(); + const tCommit = yield x.getBranchCommit("t"); + const tSha = tCommit.id().tostrS(); + const twoCommit = yield x.getHeadCommit(); + const twoSha = twoCommit.id().tostrS(); + const threeCommit = yield x.getHeadCommit(); + const threeSha = threeCommit.id().tostrS(); + const bCommit = yield x.getBranchCommit("b"); + const bSha = bCommit.id().tostrS(); + const cCommit = yield x.getBranchCommit("c"); + const cSha = cCommit.id().tostrS(); + const newCommits = {}; + newCommits[sSha] = { + metaRepoCommit: twoSha, + subCommits: { + s: bSha, + }, + }; + newCommits[tSha] = { + metaRepoCommit: threeSha, + subCommits: { + t: cSha, + }, + }; + const baseUrl = written.reverseUrlMap.a; + yield DestitchUtil.pushSyntheticRefs(x, + baseUrl, + twoCommit, + newCommits); + const b = repos.b; + const bRefName = SyntheticBranchUtil.getSyntheticBranchForCommit(bSha); + const bRef = yield NodeGit.Reference.lookup(b, bRefName); + assert(undefined !== bRef); + assert.equal(bRef.target().tostrS(), bSha); + + const c = repos.c; + const cRefName = SyntheticBranchUtil.getSyntheticBranchForCommit(cSha); + const cRef = yield NodeGit.Reference.lookup(c, cRefName); + assert.equal(cRef.target().tostrS(), cSha); + })); +}); +describe("recordLocalNotes", function () { + it("breathing", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S:C2-1;B2=2"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const two = yield repo.getBranchCommit("2"); + const twoSha = two.id().tostrS(); + const newCommits = {}; + newCommits[headSha] = { + metaRepoCommit: twoSha, + subCommits: { s: headSha }, + }; + yield DestitchUtil.recordLocalNotes(repo, newCommits); + const notes = yield StitchUtil.readNotes( + repo, + DestitchUtil.localReferenceNoteRef); + const expected = {}; + Object.keys(newCommits).forEach(sha => { + expected[sha] = JSON.stringify(newCommits[sha], null, 4); + }); + assert.deepEqual(notes, expected); + })); +}); +describe("destitch", function () { + const cases = { + "already done": { + state: "a=B|x=S:Ra=a;C2;B2=2", + already: { + "1": { + metaRepoCommit: "2", + subCommits: {} + }, + }, + commitish: "HEAD", + remote: "a", + ref: "refs/heads/foo", + expected: "x=E:Bfoo=2", + }, + "already done, no ref": { + state: "a=B|x=S:Ra=a;C2;B2=2", + already: { + "1": { + metaRepoCommit: "2", + subCommits: {} + }, + }, + commitish: "HEAD", + remote: "a", + }, + "destitch one": { + state: ` +a=B|b=B| +x=B:Ra=a;Cb foo=bar;Bb=b;C2 s=S../b:b;B2=2;Cy s/foo=bar;Cx-y s/foo=bam;Bx=x`, + already: { + "y": { + metaRepoCommit: "2", + subCommits: { s: "b" }, + }, + }, + commitish: "x", + remote: "a", + ref: "refs/heads/foo", + expected: ` +x=E:Cs.x.s-b foo=bam;Cd.x-2 s=S../b:s.x.s;Bfoo=d.x;Bs.x.s=s.x.s| +b=E:Fcommits/s.x.s=s.x.s`, + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + const destitcher = co.wrap(function *(repos, maps) { + const repo = repos.x; + const revMap = maps.reverseCommitMap; + const commitMap = maps.commitMap; + const already = mapDestitched(revMap, c.already); + const alreadyContent = {}; + Object.keys(already).forEach(sha => { + alreadyContent[sha] = JSON.stringify(already[sha], null, 4); + }); + + // We're going to prime the remote refs based on `c.already` so we + // can exercise this capability, but we'll remove them afterwards + // so that we don't see a state change. + + yield StitchUtil.writeNotes(repo, + StitchUtil.referenceNoteRef, + alreadyContent); + yield DestitchUtil.destitch(repo, + c.commitish, + c.remote, + c.ref || null); + NodeGit.Reference.remove(repo, StitchUtil.referenceNoteRef); + + // At this point, the only stored ones are those newly created. + const localNotesRef = DestitchUtil.localReferenceNoteRef; + const localNotes = yield StitchUtil.readNotes(repo, localNotesRef); + const notes = StitchUtil.parseNotes(localNotes); + const commits = {}; + for (let stitchedSha in notes) { + const data = notes[stitchedSha]; + const stitchedId = commitMap[stitchedSha]; + const metaId = `d.${stitchedId}`; + commits[data.metaRepoCommit] = metaId; + for (let subName in data.subCommits) { + const newSubSha = data.subCommits[subName]; + const id = `s.${stitchedId}.${subName}`; + + // We have to anchor these commits with a branch. + + yield NodeGit.Reference.create(repo, + `refs/heads/${id}`, + newSubSha, + 1, + "testing"); + + commits[newSubSha] = id; + } + } + return { + commitMap: commits, + }; + }); + it(caseName, co.wrap(function *() { + yield RepoASTTestUtil.testMultiRepoManipulator(c.state, + c.expected, + destitcher, + c.fails, { + actualTransformer: refMapper, + }); + })); + }); +}); +}); From 60ed21f3d612ed18c1fde26199be8d206d6caea0 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 26 Sep 2018 10:07:19 -0400 Subject: [PATCH 212/402] `bin/` entry points for stitch/destitch --- node/bin/destitch | 34 ++++++++++++++++++++++++++++++++++ node/bin/stitch | 34 ++++++++++++++++++++++++++++++++++ node/lib/destitch.js | 2 +- node/lib/stitch.js | 2 +- 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100755 node/bin/destitch create mode 100755 node/bin/stitch diff --git a/node/bin/destitch b/node/bin/destitch new file mode 100755 index 000000000..068a35540 --- /dev/null +++ b/node/bin/destitch @@ -0,0 +1,34 @@ +#!/usr/bin/env node +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +require("./../lib/destitch.js"); + diff --git a/node/bin/stitch b/node/bin/stitch new file mode 100755 index 000000000..0d7b9090c --- /dev/null +++ b/node/bin/stitch @@ -0,0 +1,34 @@ +#!/usr/bin/env node +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +require("./../lib/stitch.js"); + diff --git a/node/lib/destitch.js b/node/lib/destitch.js index 2d257272d..c2941b692 100755 --- a/node/lib/destitch.js +++ b/node/lib/destitch.js @@ -78,6 +78,6 @@ co(function *() { } else { console.error(e.stack); } - process.exit(-1); + process.exit(1); } }); diff --git a/node/lib/stitch.js b/node/lib/stitch.js index 49c97d19e..a841f1568 100755 --- a/node/lib/stitch.js +++ b/node/lib/stitch.js @@ -160,6 +160,6 @@ co(function *() { } else { console.error(e.stack); } - process.exit(-1); + process.exit(1); } }); From b7c2cd9168b0f50dc720f3bbe78e8fdded4de741 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 27 Sep 2018 10:59:12 -0400 Subject: [PATCH 213/402] Move bulk notes logic to `bulk_notes_util` --- node/lib/util/bulk_notes_util.js | 180 ++++++++++++++++++++++++++++++ node/lib/util/destitch_util.js | 5 +- node/lib/util/stitch_util.js | 134 +++------------------- node/test/util/bulk_notes_util.js | 127 +++++++++++++++++++++ node/test/util/destitch_util.js | 20 ++-- node/test/util/stitch_util.js | 95 ++-------------- 6 files changed, 343 insertions(+), 218 deletions(-) create mode 100644 node/lib/util/bulk_notes_util.js create mode 100644 node/test/util/bulk_notes_util.js diff --git a/node/lib/util/bulk_notes_util.js b/node/lib/util/bulk_notes_util.js new file mode 100644 index 000000000..63a45031f --- /dev/null +++ b/node/lib/util/bulk_notes_util.js @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const NodeGit = require("nodegit"); + +const ConfigUtil = require("./config_util"); +const DoWorkQueue = require("./do_work_queue"); +const GitUtil = require("./git_util"); +const TreeUtil = require("./tree_util"); + +// This constant defines the maximum number of simple, multi-threaded parallel +// operations we'll perform. We allow the user to configure the number of +// parallel operations that we must shell out for, but this value is just to +// prevent us from running out of JavaScript heap. + +const maxParallel = 1000; + +/** + * Write the specified `contents` to the note having the specified `refName` in + * the specified `repo`. + * + * Writing notes oneo-at-a-time is slow. This method let's you write them in + * bulk, far more efficiently. + * + * @param {NodeGit.Repository} repo + * @param {String} refName + * @param {Object} contents SHA to data + */ +exports.writeNotes = co.wrap(function *(repo, refName, contents) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(refName); + assert.isObject(contents); + + if (0 === Object.keys(contents).length) { + // Nothing to do if no contents; no point in making an empty commit or + // in making clients check themselves. + return; // RETURN + } + + // We're going to directly write the tree/commit for a new note containing + // `contents`. + + let currentCommit = null; + let currentTree = null; + const parents = []; + const ref = yield GitUtil.getReference(repo, refName); + if (null !== ref) { + currentCommit = yield repo.getCommit(ref.target()); + parents.push(currentCommit); + currentTree = yield currentCommit.getTree(); + } + const odb = yield repo.odb(); + const changes = {}; + const ODB_BLOB = 3; + const BLOB = NodeGit.TreeEntry.FILEMODE.BLOB; + const writeBlob = co.wrap(function *(sha) { + const content = contents[sha]; + const blobId = yield odb.write(content, content.length, ODB_BLOB); + changes[sha] = new TreeUtil.Change(blobId, BLOB); + }); + yield DoWorkQueue.doInParallel(Object.keys(contents), writeBlob); + + const newTree = yield TreeUtil.writeTree(repo, currentTree, changes); + const sig = yield ConfigUtil.defaultSignature(repo); + const commit = yield NodeGit.Commit.create(repo, + null, + sig, + sig, + null, + "git-meta updating notes", + newTree, + parents.length, + parents); + yield NodeGit.Reference.create(repo, refName, commit, 1, "updated"); +}); + +/** + * Load, into the specified `result`, note entries found in the specified + * `tree`, prefixing their key with the specified `basePath`. If subtrees are + * found, recurse. Use the specified `repo` to read trees from their IDs. + * + * @param {Object} result + * @param {NodeGit.Tree} tree + * @param {String} basePath + */ +const processTree = co.wrap(function *(result, repo, tree, basePath) { + const entries = tree.entries(); + const processEntry = co.wrap(function *(e) { + const fullPath = basePath + e.name(); + if (e.isBlob()) { + const blob = yield e.getBlob(); + result[fullPath] = blob.toString(); + } else if (e.isTree()) { + // Recurse if we find a tree, + + const id = e.id(); + const nextTree = yield repo.getTree(id); + yield processTree(result, repo, nextTree, fullPath); + } + // Ignore anything that's neither blob nor tree. + }); + yield DoWorkQueue.doInParallel(entries, processEntry); +}); + +/** + * Return the contents of the note having the specified `refName` in the + * specified `repo` or an empty object if no such note exists. + * + * Reading notes one-at-a-time is slow. This method lets you read them all at + * once for a given ref. + * + * @param {NodeGit.Repository} repo + * @param {String} refName + * @return {Object} sha to content + */ +exports.readNotes = co.wrap(function *(repo, refName) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(refName); + + const ref = yield GitUtil.getReference(repo, refName); + if (null === ref) { + return {}; + } + const result = {}; + const commit = yield repo.getCommit(ref.target()); + const tree = yield commit.getTree(); + const entries = tree.entries(); + const processEntry = co.wrap(function *(e) { + const blob = yield e.getBlob(); + result[e.name()] = blob.toString(); + }); + yield DoWorkQueue.doInParallel(entries, processEntry, maxParallel); + return result; +}); + +/** + * Return the result of transforming the specified `map` so that each of the + * (string) values are replaced by the result of JSON parsing them. + * + * @param {Object} map string to string + * @return {Object} map string to object + */ +exports.parseNotes = function (map) { + const result = {}; + for (let key in map) { + result[key] = JSON.parse(map[key]); + } + return result; +}; diff --git a/node/lib/util/destitch_util.js b/node/lib/util/destitch_util.js index c1d15f4f9..a137e1f52 100644 --- a/node/lib/util/destitch_util.js +++ b/node/lib/util/destitch_util.js @@ -35,6 +35,7 @@ const co = require("co"); const NodeGit = require("nodegit"); const path = require("path"); +const BulkNotesUtil = require("./bulk_notes_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); const StitchUtil = require("./stitch_util"); @@ -422,7 +423,9 @@ exports.recordLocalNotes = co.wrap(function *(repo, newCommits) { Object.keys(newCommits).forEach(sha => { content[sha] = JSON.stringify(newCommits[sha], null, 4); }); - yield StitchUtil.writeNotes(repo, exports.localReferenceNoteRef, content); + yield BulkNotesUtil.writeNotes(repo, + exports.localReferenceNoteRef, + content); }); /** diff --git a/node/lib/util/stitch_util.js b/node/lib/util/stitch_util.js index 66f9ec62f..5d2ff0635 100644 --- a/node/lib/util/stitch_util.js +++ b/node/lib/util/stitch_util.js @@ -34,8 +34,8 @@ const assert = require("chai").assert; const co = require("co"); const NodeGit = require("nodegit"); +const BulkNotesUtil = require("./bulk_notes_util"); const Commit = require("./commit"); -const ConfigUtil = require("./config_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -46,99 +46,6 @@ const UserError = require("./user_error"); const FILEMODE = NodeGit.TreeEntry.FILEMODE; -// TODO: the `writeNotes` and `readNotes` methods should be moved to a utility -// for notes, if they're needed elsewhwere. - -/** - * Write the specified `contents` to the note having the specified `refName` in - * the specified `repo`. - * - * Writing notes oneo-at-a-time is slow. This method let's you write them in - * bulk, far more efficiently. - * - * @param {NodeGit.Repository} repo - * @param {String} refName - * @param {Object} contents SHA to data - */ -exports.writeNotes = co.wrap(function *(repo, refName, contents) { - assert.instanceOf(repo, NodeGit.Repository); - assert.isString(refName); - assert.isObject(contents); - - if (0 === Object.keys(contents).length) { - // Nothing to do if no contents; no point in making an empty commit or - // in making clients check themselves. - return; // RETURN - } - - // We're going to directly write the tree/commit for a new note containing - // `contents`. - - let currentCommit = null; - let currentTree = null; - const parents = []; - const ref = yield GitUtil.getReference(repo, refName); - if (null !== ref) { - currentCommit = yield repo.getCommit(ref.target()); - parents.push(currentCommit); - currentTree = yield currentCommit.getTree(); - } - const odb = yield repo.odb(); - const changes = {}; - const ODB_BLOB = 3; - const BLOB = NodeGit.TreeEntry.FILEMODE.BLOB; - const writeBlob = co.wrap(function *(sha) { - const content = contents[sha]; - const blobId = yield odb.write(content, content.length, ODB_BLOB); - changes[sha] = new TreeUtil.Change(blobId, BLOB); - }); - yield DoWorkQueue.doInParallel(Object.keys(contents), writeBlob); - - const newTree = yield TreeUtil.writeTree(repo, currentTree, changes); - const sig = yield ConfigUtil.defaultSignature(repo); - const commit = yield NodeGit.Commit.create(repo, - null, - sig, - sig, - null, - "git-meta updating notes", - newTree, - parents.length, - parents); - yield NodeGit.Reference.create(repo, refName, commit, 1, "updated"); -}); - -/** - * Return the contents of the note having the specified `refName` in the - * specified `repo` or an empty object if no such note exists. - * - * Reading notes one-at-a-time is slow. This method let's you read them all at - * once for a given ref. - * - * @param {NodeGit.Repository} repo - * @param {String} refName - * @return {Object} sha to content - */ -exports.readNotes = co.wrap(function *(repo, refName) { - assert.instanceOf(repo, NodeGit.Repository); - assert.isString(refName); - - const ref = yield GitUtil.getReference(repo, refName); - if (null === ref) { - return {}; - } - const result = {}; - const commit = yield repo.getCommit(ref.target()); - const tree = yield commit.getTree(); - const entries = tree.entries(); - const processEntry = co.wrap(function *(e) { - const blob = yield e.getBlob(); - result[e.name()] = blob.toString(); - }); - yield DoWorkQueue.doInParallel(entries, processEntry); - return result; -}); - /** * @property {String} */ @@ -153,7 +60,8 @@ exports.whitelistNoteRef = "refs/notes/stitched/whitelist"; exports.readWhitelist = co.wrap(function *(repo) { assert.instanceOf(repo, NodeGit.Repository); - const notes = yield exports.readNotes(repo, exports.whitelistNoteRef); + const notes = + yield BulkNotesUtil.readNotes(repo, exports.whitelistNoteRef); return new Set(Object.keys(notes)); }); @@ -403,24 +311,9 @@ exports.writeSubmoduleChangeCache = co.wrap(function *(repo, changes) { } cache[sha] = JSON.stringify(cachedChanges, null, 4); } - yield exports.writeNotes(repo, exports.changeCacheRef, cache); + yield BulkNotesUtil.writeNotes(repo, exports.changeCacheRef, cache); }); -/** - * Return the result of transforming the specified `map` so that each of the - * (string) values are replaced by the result of JSON parsing them. - * - * @param {Object} map string to string - * @return {Object} map string to object - */ -exports.parseNotes = function (map) { - const result = {}; - for (let key in map) { - result[key] = JSON.parse(map[key]); - } - return result; -}; - /** * Read the cached list of submodule changes per commit in the specified * `repo`. @@ -431,8 +324,8 @@ exports.parseNotes = function (map) { exports.readSubmoduleChangeCache = co.wrap(function *(repo) { assert.instanceOf(repo, NodeGit.Repository); - const cached = yield exports.readNotes(repo, exports.changeCacheRef); - return exports.parseNotes(cached); + const cached = yield BulkNotesUtil.readNotes(repo, exports.changeCacheRef); + return BulkNotesUtil.parseNotes(cached); }); /** @@ -887,7 +780,8 @@ exports.readConvertedCommits = co.wrap(function *(repo) { // We use "" to indicate that a commit could not be converted. - const result = yield exports.readNotes(repo, exports.convertedNoteRef); + const result = + yield BulkNotesUtil.readNotes(repo, exports.convertedNoteRef); for (let key in result) { result[key] = exports.readConvertedContent(result[key]); } @@ -1107,12 +1001,12 @@ ${name}`; exports.makeReferenceNoteContent(sha, record.subCommits); } } - yield exports.writeNotes(repo, - exports.referenceNoteRef, - referenceNotes); - yield exports.writeNotes(repo, - exports.convertedNoteRef, - convertedNotes); + yield BulkNotesUtil.writeNotes(repo, + exports.referenceNoteRef, + referenceNotes); + yield BulkNotesUtil.writeNotes(repo, + exports.convertedNoteRef, + convertedNotes); records = {}; }); diff --git a/node/test/util/bulk_notes_util.js b/node/test/util/bulk_notes_util.js new file mode 100644 index 000000000..42dc02c8a --- /dev/null +++ b/node/test/util/bulk_notes_util.js @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const NodeGit = require("nodegit"); + +const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const BulkNotesUtil = require("../../lib/util/bulk_notes_util"); + +describe("BulkNotesUtil", function () { +describe("writeNotes", function () { + it("with a parent", co.wrap(function *() { + const ref = "refs/notes/foo/bar"; + const written = yield RepoASTTestUtil.createRepo(` +S:C2-1;H=2;N ${ref} 1=hello`); + const repo = written.repo; + const foo = yield repo.getHeadCommit(); + const fooSha = foo.id().tostrS(); + const first = (yield foo.getParents())[0]; + const firstSha = first.id().tostrS(); + const newNotes = {}; + newNotes[fooSha] = "foobar"; + yield BulkNotesUtil.writeNotes(repo, ref, newNotes); + const result = {}; + const shas = []; + yield NodeGit.Note.foreach(repo, ref, (_, sha) => { + shas.push(sha); + }); + yield shas.map(co.wrap(function *(sha) { + const note = yield NodeGit.Note.read(repo, ref, sha); + result[sha] = note.message(); + })); + const expected = newNotes; + newNotes[firstSha] = "hello"; + assert.deepEqual(result, expected); + })); + it("no parents", co.wrap(function *() { + const ref = "refs/notes/foo/bar"; + const written = yield RepoASTTestUtil.createRepo("S:C2-1;H=2"); + const repo = written.repo; + const foo = yield repo.getHeadCommit(); + const fooSha = foo.id().tostrS(); + const first = (yield foo.getParents())[0]; + const firstSha = first.id().tostrS(); + const newNotes = {}; + newNotes[firstSha] = "hello"; + newNotes[fooSha] = "foobar"; + yield BulkNotesUtil.writeNotes(repo, ref, newNotes); + const result = {}; + const shas = []; + yield NodeGit.Note.foreach(repo, ref, (_, sha) => { + shas.push(sha); + }); + yield shas.map(co.wrap(function *(sha) { + const note = yield NodeGit.Note.read(repo, ref, sha); + result[sha] = note.message(); + })); + const expected = newNotes; + assert.deepEqual(result, expected); + })); +}); +describe("readNotes", function () { + it("empty", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S:C2-1;Bfoo=2"); + const repo = written.repo; + const refName = "refs/notes/foo/bar"; + const result = yield BulkNotesUtil.readNotes(repo, refName); + assert.deepEqual(result, {}); + })); + it("breathing", co.wrap(function *() { + const written = yield RepoASTTestUtil.createRepo("S:C2-1;Bfoo=2"); + const repo = written.repo; + const head = yield repo.getHeadCommit(); + const headSha = head.id().tostrS(); + const foo = yield repo.getBranchCommit("foo"); + const fooSha = foo.id().tostrS(); + const refName = "refs/notes/foo/bar"; + const sig = repo.defaultSignature(); + yield NodeGit.Note.create(repo, refName, sig, sig, fooSha, "foo", 1); + yield NodeGit.Note.create(repo, refName, sig, sig, headSha, "bar", 1); + const result = yield BulkNotesUtil.readNotes(repo, refName); + const expected = {}; + expected[fooSha] = "foo"; + expected[headSha] = "bar"; + assert.deepEqual(result, expected); + })); +}); +describe("parseNotes", function () { + it("breathing", function () { + const obj = { foo: "bar" }; + const str = JSON.stringify(obj, null, 4); + const input = { yay: str }; + const result = BulkNotesUtil.parseNotes(input); + assert.deepEqual(result, { yay: obj }); + }); +}); +}); diff --git a/node/test/util/destitch_util.js b/node/test/util/destitch_util.js index 515eeca5e..b8f2dca8c 100644 --- a/node/test/util/destitch_util.js +++ b/node/test/util/destitch_util.js @@ -34,6 +34,7 @@ const assert = require("chai").assert; const co = require("co"); const NodeGit = require("nodegit"); +const BulkNotesUtil = require("../../lib/util/bulk_notes_util"); const DestitchUtil = require("../../lib/util/destitch_util"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); const StitchUtil = require("../../lib/util/stitch_util"); @@ -428,9 +429,9 @@ x=E:Cs.y.s-a a=baz;Cd.y-2 s=Sa:s.y.s;Bs.y.s=s.y.s;Bd.y=d.y; for (let sha in already) { already[sha] = JSON.stringify(already[sha], null, 4); } - yield StitchUtil.writeNotes(repo, - StitchUtil.referenceNoteRef, - already); + yield BulkNotesUtil.writeNotes(repo, + StitchUtil.referenceNoteRef, + already); const originalNewly = mapDestitched(revMap, c.newly); const newly = Object.assign({}, originalNewly); const result = yield DestitchUtil.destitchChain(repo, @@ -638,7 +639,7 @@ describe("recordLocalNotes", function () { subCommits: { s: headSha }, }; yield DestitchUtil.recordLocalNotes(repo, newCommits); - const notes = yield StitchUtil.readNotes( + const notes = yield BulkNotesUtil.readNotes( repo, DestitchUtil.localReferenceNoteRef); const expected = {}; @@ -708,9 +709,9 @@ b=E:Fcommits/s.x.s=s.x.s`, // can exercise this capability, but we'll remove them afterwards // so that we don't see a state change. - yield StitchUtil.writeNotes(repo, - StitchUtil.referenceNoteRef, - alreadyContent); + yield BulkNotesUtil.writeNotes(repo, + StitchUtil.referenceNoteRef, + alreadyContent); yield DestitchUtil.destitch(repo, c.commitish, c.remote, @@ -719,8 +720,9 @@ b=E:Fcommits/s.x.s=s.x.s`, // At this point, the only stored ones are those newly created. const localNotesRef = DestitchUtil.localReferenceNoteRef; - const localNotes = yield StitchUtil.readNotes(repo, localNotesRef); - const notes = StitchUtil.parseNotes(localNotes); + const localNotes = + yield BulkNotesUtil.readNotes(repo, localNotesRef); + const notes = BulkNotesUtil.parseNotes(localNotes); const commits = {}; for (let stitchedSha in notes) { const data = notes[stitchedSha]; diff --git a/node/test/util/stitch_util.js b/node/test/util/stitch_util.js index 8116c39c4..b04b2a72e 100644 --- a/node/test/util/stitch_util.js +++ b/node/test/util/stitch_util.js @@ -34,6 +34,7 @@ const assert = require("chai").assert; const co = require("co"); const NodeGit = require("nodegit"); +const BulkNotesUtil = require("../../lib/util/bulk_notes_util"); const RepoAST = require("../../lib/util/repo_ast"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); const RepoASTUtil = require("../../lib/util/repo_ast_util"); @@ -131,57 +132,6 @@ const getCommitChanges = co.wrap(function *(repo, commit) { }); describe("StitchUtil", function () { -describe("writeNotes", function () { - it("with a parent", co.wrap(function *() { - const ref = "refs/notes/foo/bar"; - const written = yield RepoASTTestUtil.createRepo(` -S:C2-1;H=2;N ${ref} 1=hello`); - const repo = written.repo; - const foo = yield repo.getHeadCommit(); - const fooSha = foo.id().tostrS(); - const first = (yield foo.getParents())[0]; - const firstSha = first.id().tostrS(); - const newNotes = {}; - newNotes[fooSha] = "foobar"; - yield StitchUtil.writeNotes(repo, ref, newNotes); - const result = {}; - const shas = []; - yield NodeGit.Note.foreach(repo, ref, (_, sha) => { - shas.push(sha); - }); - yield shas.map(co.wrap(function *(sha) { - const note = yield NodeGit.Note.read(repo, ref, sha); - result[sha] = note.message(); - })); - const expected = newNotes; - newNotes[firstSha] = "hello"; - assert.deepEqual(result, expected); - })); - it("no parents", co.wrap(function *() { - const ref = "refs/notes/foo/bar"; - const written = yield RepoASTTestUtil.createRepo("S:C2-1;H=2"); - const repo = written.repo; - const foo = yield repo.getHeadCommit(); - const fooSha = foo.id().tostrS(); - const first = (yield foo.getParents())[0]; - const firstSha = first.id().tostrS(); - const newNotes = {}; - newNotes[firstSha] = "hello"; - newNotes[fooSha] = "foobar"; - yield StitchUtil.writeNotes(repo, ref, newNotes); - const result = {}; - const shas = []; - yield NodeGit.Note.foreach(repo, ref, (_, sha) => { - shas.push(sha); - }); - yield shas.map(co.wrap(function *(sha) { - const note = yield NodeGit.Note.read(repo, ref, sha); - result[sha] = note.message(); - })); - const expected = newNotes; - assert.deepEqual(result, expected); - })); -}); describe("readWhitelist", function () { it("breathing", co.wrap(function *() { const written = yield RepoASTTestUtil.createRepo(` @@ -523,32 +473,6 @@ describe("listCommitsInOrder", function () { }); }); }); -describe("readNotes", function () { - it("empty", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo("S:C2-1;Bfoo=2"); - const repo = written.repo; - const refName = "refs/notes/foo/bar"; - const result = yield StitchUtil.readNotes(repo, refName); - assert.deepEqual(result, {}); - })); - it("breathing", co.wrap(function *() { - const written = yield RepoASTTestUtil.createRepo("S:C2-1;Bfoo=2"); - const repo = written.repo; - const head = yield repo.getHeadCommit(); - const headSha = head.id().tostrS(); - const foo = yield repo.getBranchCommit("foo"); - const fooSha = foo.id().tostrS(); - const refName = "refs/notes/foo/bar"; - const sig = repo.defaultSignature(); - yield NodeGit.Note.create(repo, refName, sig, sig, fooSha, "foo", 1); - yield NodeGit.Note.create(repo, refName, sig, sig, headSha, "bar", 1); - const result = yield StitchUtil.readNotes(repo, refName); - const expected = {}; - expected[fooSha] = "foo"; - expected[headSha] = "bar"; - assert.deepEqual(result, expected); - })); -}); describe("refMapper", function () { const Commit = RepoAST.Commit; const cases = { @@ -790,15 +714,6 @@ describe("writeSubmoduleChangeCache", function () { }); })); }); -describe("parseNotes", function () { - it("breathing", function () { - const obj = { foo: "bar" }; - const str = JSON.stringify(obj, null, 4); - const input = { yay: str }; - const result = StitchUtil.parseNotes(input); - assert.deepEqual(result, { yay: obj }); - }); -}); describe("readSubmoduleChangeCache", function () { it("breathing", co.wrap(function *() { const written = yield RepoASTTestUtil.createRepo("S:C2-1;H=2"); @@ -1485,7 +1400,9 @@ describe("listCommitsToStitch", function () { const headSha = head.id().tostrS(); const notes = {}; notes[headSha] = StitchUtil.makeConvertedNoteContent(null); - yield StitchUtil.writeNotes(repo, StitchUtil.convertedNoteRef, notes); + yield BulkNotesUtil.writeNotes(repo, + StitchUtil.convertedNoteRef, + notes); const getConv = StitchUtil.makeGetConvertedCommit(repo, {}); const result = yield StitchUtil.listCommitsToStitch(repo, head, getConv); @@ -1517,7 +1434,9 @@ describe("listCommitsToStitch", function () { const twoSha = written.oldCommitMap["2"]; const notes = {}; notes[twoSha] = StitchUtil.makeConvertedNoteContent(twoSha); - yield StitchUtil.writeNotes(repo, StitchUtil.convertedNoteRef, notes); + yield BulkNotesUtil.writeNotes(repo, + StitchUtil.convertedNoteRef, + notes); const getConv = StitchUtil.makeGetConvertedCommit(repo, {}); const result = yield StitchUtil.listCommitsToStitch(repo, head, getConv); From 96b0c1bdcf3f36f4e8211cd8adce565009787f64 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 27 Sep 2018 11:28:47 -0400 Subject: [PATCH 214/402] Shard bulk note writes, can now read sharded notes --- node/lib/util/bulk_notes_util.js | 41 ++++++++++++++++++++----------- node/test/util/bulk_notes_util.js | 22 +++++++++++++++++ 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/node/lib/util/bulk_notes_util.js b/node/lib/util/bulk_notes_util.js index 63a45031f..a4cfad2fc 100644 --- a/node/lib/util/bulk_notes_util.js +++ b/node/lib/util/bulk_notes_util.js @@ -33,18 +33,34 @@ const assert = require("chai").assert; const co = require("co"); const NodeGit = require("nodegit"); +const path = require("path"); const ConfigUtil = require("./config_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); const TreeUtil = require("./tree_util"); -// This constant defines the maximum number of simple, multi-threaded parallel -// operations we'll perform. We allow the user to configure the number of -// parallel operations that we must shell out for, but this value is just to -// prevent us from running out of JavaScript heap. - -const maxParallel = 1000; +/** + * Return a sharded path for the specified `sha`, inserting a "/" between every + * second character. + * + * @param {String} sha + * @return {String} + */ +exports.shardSha = function (sha) { + const size = 2; + let result = ""; + while ("" !== sha) { + const next = sha.substr(0, size); + if ("" !== result) { + result = path.join(result, next); + } else { + result = next; + } + sha = sha.substr(size); + } + return result; +}; /** * Write the specified `contents` to the note having the specified `refName` in @@ -71,12 +87,11 @@ exports.writeNotes = co.wrap(function *(repo, refName, contents) { // We're going to directly write the tree/commit for a new note containing // `contents`. - let currentCommit = null; let currentTree = null; const parents = []; const ref = yield GitUtil.getReference(repo, refName); if (null !== ref) { - currentCommit = yield repo.getCommit(ref.target()); + const currentCommit = yield repo.getCommit(ref.target()); parents.push(currentCommit); currentTree = yield currentCommit.getTree(); } @@ -87,7 +102,8 @@ exports.writeNotes = co.wrap(function *(repo, refName, contents) { const writeBlob = co.wrap(function *(sha) { const content = contents[sha]; const blobId = yield odb.write(content, content.length, ODB_BLOB); - changes[sha] = new TreeUtil.Change(blobId, BLOB); + const sharded = exports.shardSha(sha); + changes[sharded] = new TreeUtil.Change(blobId, BLOB); }); yield DoWorkQueue.doInParallel(Object.keys(contents), writeBlob); @@ -155,12 +171,7 @@ exports.readNotes = co.wrap(function *(repo, refName) { const result = {}; const commit = yield repo.getCommit(ref.target()); const tree = yield commit.getTree(); - const entries = tree.entries(); - const processEntry = co.wrap(function *(e) { - const blob = yield e.getBlob(); - result[e.name()] = blob.toString(); - }); - yield DoWorkQueue.doInParallel(entries, processEntry, maxParallel); + yield processTree(result, repo, tree, ""); return result; }); diff --git a/node/test/util/bulk_notes_util.js b/node/test/util/bulk_notes_util.js index 42dc02c8a..608404c6b 100644 --- a/node/test/util/bulk_notes_util.js +++ b/node/test/util/bulk_notes_util.js @@ -33,11 +33,18 @@ const assert = require("chai").assert; const co = require("co"); const NodeGit = require("nodegit"); +const path = require("path"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); const BulkNotesUtil = require("../../lib/util/bulk_notes_util"); describe("BulkNotesUtil", function () { +describe("shardSha", function () { + it("breathing", function () { + assert.equal(BulkNotesUtil.shardSha("aabbbb"), + path.join("aa", "bb", "bb")); + }); +}); describe("writeNotes", function () { it("with a parent", co.wrap(function *() { const ref = "refs/notes/foo/bar"; @@ -114,6 +121,21 @@ describe("readNotes", function () { expected[headSha] = "bar"; assert.deepEqual(result, expected); })); + it("sharded, from `writeNotes`", co.wrap(function *() { + const ref = "refs/notes/foo/bar"; + const written = yield RepoASTTestUtil.createRepo("S:C2-1;H=2"); + const repo = written.repo; + const foo = yield repo.getHeadCommit(); + const fooSha = foo.id().tostrS(); + const first = (yield foo.getParents())[0]; + const firstSha = first.id().tostrS(); + const newNotes = {}; + newNotes[firstSha] = "hello"; + newNotes[fooSha] = "foobar"; + yield BulkNotesUtil.writeNotes(repo, ref, newNotes); + const result = yield BulkNotesUtil.readNotes(repo, ref); + assert.deepEqual(result, newNotes); + })); }); describe("parseNotes", function () { it("breathing", function () { From b977f1a5b03c04266a0a905b92f7cc6f7f5efe79 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 27 Sep 2018 16:13:08 -0400 Subject: [PATCH 215/402] stop over-sharding --- node/lib/util/bulk_notes_util.js | 18 ++++-------------- node/test/util/bulk_notes_util.js | 4 ++-- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/node/lib/util/bulk_notes_util.js b/node/lib/util/bulk_notes_util.js index a4cfad2fc..5d8b18e7d 100644 --- a/node/lib/util/bulk_notes_util.js +++ b/node/lib/util/bulk_notes_util.js @@ -41,25 +41,15 @@ const GitUtil = require("./git_util"); const TreeUtil = require("./tree_util"); /** - * Return a sharded path for the specified `sha`, inserting a "/" between every - * second character. + * Return a sharded path for the specified `sha`, e.g. + * "aabbffffffffffffffff" becomes: "aa/bb/ffffffffffffffff". The behavior is + * undefined unless `sha` contains at least five characters. * * @param {String} sha * @return {String} */ exports.shardSha = function (sha) { - const size = 2; - let result = ""; - while ("" !== sha) { - const next = sha.substr(0, size); - if ("" !== result) { - result = path.join(result, next); - } else { - result = next; - } - sha = sha.substr(size); - } - return result; + return path.join(sha.substr(0, 2), sha.substr(2, 2), sha.substr(4)); }; /** diff --git a/node/test/util/bulk_notes_util.js b/node/test/util/bulk_notes_util.js index 608404c6b..3362b06e1 100644 --- a/node/test/util/bulk_notes_util.js +++ b/node/test/util/bulk_notes_util.js @@ -41,8 +41,8 @@ const BulkNotesUtil = require("../../lib/util/bulk_notes_util"); describe("BulkNotesUtil", function () { describe("shardSha", function () { it("breathing", function () { - assert.equal(BulkNotesUtil.shardSha("aabbbb"), - path.join("aa", "bb", "bb")); + assert.equal(BulkNotesUtil.shardSha("aabbffffffffffffffff"), + path.join("aa", "bb", "ffffffffffffffff")); }); }); describe("writeNotes", function () { From d5527b69cb42b72aa5a22e5ab34e415647749be9 Mon Sep 17 00:00:00 2001 From: blevz Date: Tue, 2 Oct 2018 18:33:26 -0400 Subject: [PATCH 216/402] Add stash apply to help text --- node/lib/cmd/stash.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/lib/cmd/stash.js b/node/lib/cmd/stash.js index 3edd90da5..c1f3ec706 100644 --- a/node/lib/cmd/stash.js +++ b/node/lib/cmd/stash.js @@ -64,8 +64,8 @@ exports.configureParser = function (parser) { parser.addArgument("type", { help: ` 'save' to save a stash, 'pop' to restore, 'list' to show stashes, 'drop' to \ -discard a stash; 'save' is -default`, +discard a stash, 'apply' to apply a change without popping from stashes; \ +'save' is default`, type: "string", nargs: "?", defaultValue: "save", From 71f0b0f09e816303d33e92c5d58de182426bc7f0 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 4 Oct 2018 14:10:52 -0400 Subject: [PATCH 217/402] Fix `getCurrentRepo` to recognize non-sub gitlinks So that it will do the right thing in a worktree. --- node/lib/util/git_util.js | 18 +++++++++++++++--- node/test/util/git_util.js | 16 ++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index f6fa41e00..a39e31079 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -57,9 +57,21 @@ const UserError = require("./user_error"); * @return {String} */ function getContainingGitDir(dir) { - const gitDir = path.join(dir, ".git"); - if (fs.existsSync(gitDir) && fs.statSync(gitDir).isDirectory()) { - return dir; // RETURN + const gitPath = path.join(dir, ".git"); + if (fs.existsSync(gitPath)) { + if (fs.statSync(gitPath).isDirectory()) { + return dir; // RETURN + } + + // If `.git` is a file, it is a git link. If the link is to a submodule + // it will be relative. If it's not relative, and therefore not a + // submodule, we stop with this directory. + + const content = fs.readFileSync(gitPath, "utf8"); + const parts = content.split(" "); + if (1 < parts.length && parts[1].startsWith("/")) { + return dir; + } } const base = path.dirname(dir); diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index 9fb6053ab..bbbc872a5 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -35,7 +35,6 @@ const co = require("co"); const fs = require("fs-promise"); const mkdirp = require("mkdirp"); const NodeGit = require("nodegit"); -const os = require("os"); const path = require("path"); const GitUtil = require("../../lib/util/git_util"); @@ -402,12 +401,12 @@ describe("GitUtil", function () { // - simple case // - one deep - it("failure", function () { - const tempdir = os.tmpdir(); + it("failure", co.wrap(function *() { + const tempdir = yield TestUtil.makeTempDir(); process.chdir(tempdir); const result = GitUtil.getRootGitDirectory(); assert.isNull(result); - }); + })); it("successes", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); @@ -422,6 +421,15 @@ describe("GitUtil", function () { const subRoot = GitUtil.getRootGitDirectory(workdir); assert(yield TestUtil.isSameRealPath(workdir, subRoot), "trivial"); })); + it("with a non-submodule link", co.wrap(function *() { + const tempdir = yield TestUtil.makeTempDir(); + process.chdir(tempdir); + const gitLink = path.join(tempdir, ".git"); + yield fs.writeFile(gitLink, "gitdir: /foo/bar"); + const result = GitUtil.getRootGitDirectory(); + assert.isNotNull(result); + assert(yield TestUtil.isSameRealPath(tempdir, result), result); + })); }); describe("getCurrentRepo", function () { From 38fb3024c5a7e4f95f992d6f5f5efcc72408e86c Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Thu, 4 Oct 2018 13:30:16 -0400 Subject: [PATCH 218/402] Use normal routine to find repo for destitch The normal git-meta way isn't needed (we're not in a meta-repo) and doesn't work if you're using worktrees. --- node/lib/destitch.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/node/lib/destitch.js b/node/lib/destitch.js index c2941b692..fefc87209 100755 --- a/node/lib/destitch.js +++ b/node/lib/destitch.js @@ -32,9 +32,9 @@ const ArgumentParser = require("argparse").ArgumentParser; const co = require("co"); +const NodeGit = require("nodegit"); const DestitchUtil = require("./util/destitch_util"); -const GitUtil = require("./util/git_util"); const UserError = require("./util/user_error"); const description = `\ @@ -65,7 +65,8 @@ parser.addArgument(["commitish"], { co(function *() { const args = parser.parseArgs(); try { - const repo = yield GitUtil.getCurrentRepo(); + const location = yield NodeGit.Repository.discover(".", 0, ""); + const repo = yield NodeGit.Repository.open(location); yield DestitchUtil.destitch(repo, args.commitish, args.remote, From c213d50edf318719b6d4b7c4c2bfd5c414495e39 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 4 Oct 2018 18:05:12 -0400 Subject: [PATCH 219/402] optimize synthetic branch check --- node/lib/util/submodule_change.js | 7 ++++++ node/lib/util/synthetic_branch_util.js | 30 +++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/node/lib/util/submodule_change.js b/node/lib/util/submodule_change.js index 32e86fb7e..0e1a4152a 100644 --- a/node/lib/util/submodule_change.js +++ b/node/lib/util/submodule_change.js @@ -81,6 +81,13 @@ class SubmoduleChange { get newSha() { return this.d_newSha; } + + /** + * True if the submodule has been deleted in this change. + */ + get deleted() { + return this.d_newSha === null; + } } module.exports = SubmoduleChange; diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 9231cf381..2b8b64921 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -157,6 +157,28 @@ function* checkSubmodule(repo, cfg, metaCommit, submoduleEntry, url) { } } +/** + * Return a list of submodules changed or added in between `commit` + * and `parent`. Exclude deleted submodules. + * + * If parent is null, return null. + */ +function* computeChangedSubmodules(repo, commit, parent) { + if (parent === null) { + return null; + } + const changed = []; + const changes = yield SubmoduleUtil.getSubmoduleChanges(repo, commit, + parent, true); + for (const sub of Object.keys(changes)) { + const change = changes[sub]; + if (!change.deleted) { + changed.push(sub); + } + } + return changed; +} + function* checkSubmodules(repo, commit) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); @@ -166,9 +188,15 @@ function* checkSubmodules(repo, commit) { yield ConfigUtil.getConfigString( config, "gitmeta.skipsyntheticrefpattern")) || ""; const cfg = new SyntheticBranchConfig(whitelistPattern); + + const parent = yield GitUtil.getParentCommit(repo, commit); + const names = yield computeChangedSubmodules(repo, + commit, + parent); + const submodules = yield SubmoduleUtil.getSubmodulesForCommit(repo, commit, - null); + names); const getChanges = SubmoduleUtil.getSubmoduleChanges; const changes = yield getChanges(repo, commit, null, true); const allChanges = [ From abce63fc037827653a0c2a11d70ea51ecc6731ec Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Tue, 16 Oct 2018 00:41:45 -0400 Subject: [PATCH 220/402] Modules are lazily loaded, however some managed to sneak in without being noticed causing some additional slowness. --- node/lib/cmd/add_submodule.js | 4 ++-- node/lib/cmd/checkout.js | 4 ++-- node/lib/cmd/cherry_pick.js | 2 +- node/lib/cmd/commit.js | 3 ++- node/lib/cmd/forward.js | 6 +++--- node/lib/cmd/merge.js | 2 +- node/lib/cmd/open.js | 2 +- node/lib/cmd/reset.js | 2 +- 8 files changed, 13 insertions(+), 12 deletions(-) diff --git a/node/lib/cmd/add_submodule.js b/node/lib/cmd/add_submodule.js index fe0a867b7..b9e73124c 100644 --- a/node/lib/cmd/add_submodule.js +++ b/node/lib/cmd/add_submodule.js @@ -31,7 +31,6 @@ "use strict"; const co = require("co"); -const Hook = require("../util/hook"); /** * This module contains methods for implementing the `new` command. @@ -79,8 +78,9 @@ exports.executeableSubcommand = co.wrap(function *(args) { const fs = require("fs-promise"); const path = require("path"); - const GitUtil = require("../util/git_util"); const AddSubmodule = require("../util/add_submodule"); + const GitUtil = require("../util/git_util"); + const Hook = require("../util/hook"); const UserError = require("../util/user_error"); if (null !== args.branch && null === args.import_from) { diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index 66db59e88..8c3a69450 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -30,8 +30,6 @@ */ const co = require("co"); -const Hook = require("../util/hook"); -const UserError = require("../../lib/util/user_error"); /** * This module contains the entrypoint for the `checkout` command. @@ -131,6 +129,8 @@ exports.executeableSubcommand = co.wrap(function *(args) { const Checkout = require("../util/checkout"); const GitUtil = require("../util/git_util"); + const Hook = require("../util/hook"); + const UserError = require("../../lib/util/user_error"); let newBranch = null; const newBranchNameArr = args["new branch name"]; diff --git a/node/lib/cmd/cherry_pick.js b/node/lib/cmd/cherry_pick.js index c9acc245c..6ba5d4547 100644 --- a/node/lib/cmd/cherry_pick.js +++ b/node/lib/cmd/cherry_pick.js @@ -31,7 +31,6 @@ "use strict"; const co = require("co"); -const Hook = require("../util/hook"); /** * This module contains methods for implementing the `cherry-pick` command. @@ -89,6 +88,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { const CherryPickUtil = require("../util/cherry_pick_util"); const GitUtil = require("../util/git_util"); + const Hook = require("../util/hook"); const UserError = require("../util/user_error"); const repo = yield GitUtil.getCurrentRepo(); diff --git a/node/lib/cmd/commit.js b/node/lib/cmd/commit.js index edf64af00..03fd6ab8b 100644 --- a/node/lib/cmd/commit.js +++ b/node/lib/cmd/commit.js @@ -31,7 +31,6 @@ "use strict"; const co = require("co"); -const Hook = require("../util/hook"); /** * this module is the entrypoint for the `commit` command. @@ -109,6 +108,7 @@ the URL of submodules).`, const doCommit = co.wrap(function *(args) { const Commit = require("../util/commit"); const GitUtil = require("../util/git_util"); + const Hook = require("../util/hook"); const repo = yield GitUtil.getCurrentRepo(); const cwd = process.cwd(); @@ -126,6 +126,7 @@ const doCommit = co.wrap(function *(args) { const doAmend = co.wrap(function *(args) { const Commit = require("../util/commit"); const GitUtil = require("../util/git_util"); + const Hook = require("../util/hook"); const UserError = require("../util/user_error"); const usingPaths = 0 !== args.file.length; diff --git a/node/lib/cmd/forward.js b/node/lib/cmd/forward.js index f161fef30..35b7b5834 100644 --- a/node/lib/cmd/forward.js +++ b/node/lib/cmd/forward.js @@ -30,9 +30,7 @@ */ "use strict"; -const ArgParse = require("argparse"); -const assert = require("chai").assert; -const co = require("co"); +const co = require("co"); /** * Return an object to be used in configuring the help of the main git-meta @@ -46,6 +44,8 @@ const co = require("co"); * @return {Function} executeableSubcommand function to invoke command */ exports.makeModule = function (name) { + const ArgParse = require("argparse"); + const assert = require("chai").assert; function configureParser(parser) { parser.addArgument(["args"], { diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index b694995d1..3ac14aa1b 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -31,7 +31,6 @@ "use strict"; const co = require("co"); -const Hook = require("../util/hook"); /** * This module contains methods for implementing the `merge` command. @@ -110,6 +109,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { const MergeUtil = require("../util/merge_util"); const GitUtil = require("../util/git_util"); + const Hook = require("../util/hook"); const UserError = require("../util/user_error"); const MODE = MergeUtil.MODE; diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 9777621a3..a06f978b8 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -35,7 +35,6 @@ */ const co = require("co"); -const Hook = require("../util/hook"); /** * help text for the `open` command @@ -81,6 +80,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { const DoWorkQueue = require("../util/do_work_queue"); const GitUtil = require("../util/git_util"); + const Hook = require("../util/hook"); const Open = require("../util/open"); const SparseCheckoutUtil = require("../util/sparse_checkout_util"); const SubmoduleConfigUtil = require("../util/submodule_config_util"); diff --git a/node/lib/cmd/reset.js b/node/lib/cmd/reset.js index 4f63ead52..22a08d20b 100644 --- a/node/lib/cmd/reset.js +++ b/node/lib/cmd/reset.js @@ -32,7 +32,6 @@ const ArgParse = require("argparse"); const co = require("co"); -const Hook = require("../util/hook"); /** * This submodule provides the entrypoint for the `reset` command. @@ -131,6 +130,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { const path = require("path"); const GitUtil = require("../util/git_util"); + const Hook = require("../util/hook"); const Reset = require("../util/reset"); const UserError = require("../util/user_error"); const StatusUtil = require("../util/status_util"); From 0b793357bf3497a0789cb221a3243dee95f488e1 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Tue, 16 Oct 2018 00:39:05 -0400 Subject: [PATCH 221/402] Wait until completion of argparse to load NodeGit. Loading NodeGit causes an additional several hundred ms to argparse output and validation of flags. Instead, lazily load NodeGit after it has completed to speed up fatals and help calls. --- node/lib/git-meta.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js index f69ec8fa3..3e3ebffaf 100755 --- a/node/lib/git-meta.js +++ b/node/lib/git-meta.js @@ -36,7 +36,6 @@ */ const ArgumentParser = require("argparse").ArgumentParser; -const NodeGit = require("nodegit"); const add = require("./cmd/add"); const addSubmodule = require("./cmd/add_submodule"); @@ -63,11 +62,6 @@ const submodule = require("./cmd/submodule"); const UserError = require("./util/user_error"); const version = require("./cmd/version"); -// see https://github.com/nodegit/nodegit/issues/827 -- this is required -// to prevent random hard crashes with e.g. parallelism in index operations. -// Eventually, this will be nodegit's default. -NodeGit.setThreadSafetyStatus(NodeGit.THREAD_SAFETY.ENABLED_FOR_ASYNC_ONLY); - /** * Configure the specified `parser` to include the command having the specified * `commandName` implemented in the specified `module`. @@ -87,6 +81,12 @@ function configureSubcommand(parser, commandName, module) { module.configureParser(subParser); subParser.setDefaults({ func: function (args) { + const NodeGit = require("nodegit"); + // see https://github.com/nodegit/nodegit/issues/827 -- this is required + // to prevent random hard crashes with e.g. parallelism in index operations. + // Eventually, this will be nodegit's default. + NodeGit.setThreadSafetyStatus(NodeGit.THREAD_SAFETY.ENABLED_FOR_ASYNC_ONLY); + module.executeableSubcommand(args) .catch(function (error) { From d9d26c6c5d3abbc078d1fddd4d60a92c9a1ffd49 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Tue, 16 Oct 2018 01:06:58 -0400 Subject: [PATCH 222/402] As an optimization to git meta version, skip importing NodeGit. --- node/lib/git-meta.js | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js index 3e3ebffaf..783b5ec54 100755 --- a/node/lib/git-meta.js +++ b/node/lib/git-meta.js @@ -69,11 +69,9 @@ const version = require("./cmd/version"); * @param {ArgumentParser} parser * @param {String} commandName * @param {Object} module - * @param {Function} module.configureParser - * @param {Function} module.executeableSubcommand - * @param {String} module.helpText + * @param {Boolean} skipNodeGit */ -function configureSubcommand(parser, commandName, module) { +function configureSubcommand(parser, commandName, module, skipNodeGit) { const subParser = parser.addParser(commandName, { help: module.helpText, description: module.description, @@ -81,11 +79,17 @@ function configureSubcommand(parser, commandName, module) { module.configureParser(subParser); subParser.setDefaults({ func: function (args) { - const NodeGit = require("nodegit"); - // see https://github.com/nodegit/nodegit/issues/827 -- this is required - // to prevent random hard crashes with e.g. parallelism in index operations. - // Eventually, this will be nodegit's default. - NodeGit.setThreadSafetyStatus(NodeGit.THREAD_SAFETY.ENABLED_FOR_ASYNC_ONLY); + // This optimization allows to skip importing NodeGit + // if not required (eg. version). + if (!skipNodeGit) { + const NodeGit = require("nodegit"); + // see https://github.com/nodegit/nodegit/issues/827 -- this is + // required to prevent random hard crashes with e.g. + // parallelism in index operations. Eventually, this will be + // nodegit's default. + NodeGit.setThreadSafetyStatus( + NodeGit.THREAD_SAFETY.ENABLED_FOR_ASYNC_ONLY); + } module.executeableSubcommand(args) .catch(function (error) { @@ -147,11 +151,16 @@ const commands = { "version": version, }; +// These optimized commands do not require NodeGit, and can skip importing it. +const optimized = { + "version": true, +}; + // Configure the parser with commands in alphabetical order. Object.keys(commands).sort().forEach(name => { const cmd = commands[name]; - configureSubcommand(subParser, name, cmd); + configureSubcommand(subParser, name, cmd, (null !== optimized[name])); }); const blacklist = new Set([ From f77c23ff19c39503b52b476fe1e543c82a23a0df Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Tue, 16 Oct 2018 01:04:12 -0400 Subject: [PATCH 223/402] Optimize git meta root and forward command. Both the forward command and root command currently load NodeGit for no specific reason. Removing this loading and breaking out non-NodeGit utility methods makes these commands significantly faster. --- node/lib/cmd/forward.js | 4 +- node/lib/cmd/root.js | 6 +-- node/lib/git-meta.js | 1 + node/lib/util/git_util.js | 53 ++------------------ node/lib/util/git_util_fast.js | 87 +++++++++++++++++++++++++++++++++ node/test/util/git_util.js | 45 ----------------- node/test/util/git_util_fast.js | 86 ++++++++++++++++++++++++++++++++ 7 files changed, 183 insertions(+), 99 deletions(-) create mode 100644 node/lib/util/git_util_fast.js create mode 100644 node/test/util/git_util_fast.js diff --git a/node/lib/cmd/forward.js b/node/lib/cmd/forward.js index 35b7b5834..ffbc0ee7c 100644 --- a/node/lib/cmd/forward.js +++ b/node/lib/cmd/forward.js @@ -77,14 +77,14 @@ ${helpText} See 'git ${name} --help' for more information.`, exports.execute = co.wrap(function *(name, args) { const ChildProcess = require("child-process-promise"); - const GitUtil = require("../util/git_util"); + const GitUtilFast = require("../util/git_util_fast"); if (name === "diff") { args.splice(0, 0, "--submodule=diff"); } const gitArgs = [ "-C", - GitUtil.getRootGitDirectory(), + GitUtilFast.getRootGitDirectory(), name, ].concat(args); try { diff --git a/node/lib/cmd/root.js b/node/lib/cmd/root.js index bb2542902..e28b49c51 100644 --- a/node/lib/cmd/root.js +++ b/node/lib/cmd/root.js @@ -67,10 +67,10 @@ Print the relative path between current directory and root. E.g., \ exports.executeableSubcommand = co.wrap(function *(args) { const path = require("path"); - const GitUtil = require("../util/git_util"); - const UserError = require("../util/user_error"); + const GitUtilFast = require("../util/git_util_fast"); + const UserError = require("../util/user_error"); - const root = GitUtil.getRootGitDirectory(); + const root = GitUtilFast.getRootGitDirectory(); if (null === root) { throw new UserError("No root repo found."); } diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js index 783b5ec54..a31cf8606 100755 --- a/node/lib/git-meta.js +++ b/node/lib/git-meta.js @@ -153,6 +153,7 @@ const commands = { // These optimized commands do not require NodeGit, and can skip importing it. const optimized = { + "root": true, "version": true, }; diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index a39e31079..02e3f9fd7 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -31,7 +31,8 @@ "use strict"; /** - * This module contains common git utility methods. + * This module contains common git utility methods that require NodeGit. + * Otherwise, put them in git_util_fast to optimize their load time. */ const assert = require("chai").assert; @@ -45,44 +46,9 @@ const path = require("path"); const ConfigUtil = require("./config_util"); const DoWorkQueue = require("./do_work_queue"); const Hook = require("./hook"); +const GitUtilFast = require("./git_util_fast"); const UserError = require("./user_error"); -/** - * If the directory identified by the specified `dir` contains a ".git" - * directory, return it. Otherwise, return the first parent directory of `dir` - * containing a `.git` directory. If no such directory exists, return `None`. - * - * @private - * @param {String} dir - * @return {String} - */ -function getContainingGitDir(dir) { - const gitPath = path.join(dir, ".git"); - if (fs.existsSync(gitPath)) { - if (fs.statSync(gitPath).isDirectory()) { - return dir; // RETURN - } - - // If `.git` is a file, it is a git link. If the link is to a submodule - // it will be relative. If it's not relative, and therefore not a - // submodule, we stop with this directory. - - const content = fs.readFileSync(gitPath, "utf8"); - const parts = content.split(" "); - if (1 < parts.length && parts[1].startsWith("/")) { - return dir; - } - } - - const base = path.dirname(dir); - - if ("" === base || "/" === base) { - return null; // RETURN - } - - return getContainingGitDir(base); -} - /** * Create a branch having the specified `branchName` in the specified `repo` * pointing to the current head. @@ -330,17 +296,6 @@ exports.findRemoteBranch = co.wrap(function *(repo, remoteName, branchName) { return yield exports.findBranch(repo, shorthand); }); - -/** - * Return the root of the repository in which the current working directory - * resides, or null if the working directory contains no git repository. - * - * @return {String|null} - */ -exports.getRootGitDirectory = function () { - return getContainingGitDir(process.cwd()); -}; - /** * Return the current repository (as located from the current working * directory) or throw a `UserError` exception if no git repository can be @@ -350,7 +305,7 @@ exports.getRootGitDirectory = function () { * @return {NodeGit.Repository} */ exports.getCurrentRepo = function () { - const path = exports.getRootGitDirectory(); + const path = GitUtilFast.getRootGitDirectory(); if (null === path) { throw new UserError( `Could not find Git directory from ${colors.red(process.cwd())}.`); diff --git a/node/lib/util/git_util_fast.js b/node/lib/util/git_util_fast.js new file mode 100644 index 000000000..43c07f981 --- /dev/null +++ b/node/lib/util/git_util_fast.js @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +/** + * This module contains common git utility methods that do not require NodeGit + * to accomplish. This is an optimization since loading NodeGit results in + * unsatisfactory performance in the CLI. + */ + +const fs = require("fs-promise"); +const path = require("path"); +// DO NOT REQUIRE NODEGIT + +/** + * If the directory identified by the specified `dir` contains a ".git" + * directory, return it. Otherwise, return the first parent directory of `dir` + * containing a `.git` directory. If no such directory exists, return `None`. + * + * @private + * @param {String} dir + * @return {String} + */ +function getContainingGitDir(dir) { + const gitPath = path.join(dir, ".git"); + if (fs.existsSync(gitPath)) { + if (fs.statSync(gitPath).isDirectory()) { + return dir; // RETURN + } + + // If `.git` is a file, it is a git link. If the link is to a submodule + // it will be relative. If it's not relative, and therefore not a + // submodule, we stop with this directory. + + const content = fs.readFileSync(gitPath, "utf8"); + const parts = content.split(" "); + if (1 < parts.length && parts[1].startsWith("/")) { + return dir; + } + } + + const base = path.dirname(dir); + + if ("" === base || "/" === base) { + return null; // RETURN + } + + return getContainingGitDir(base); +} + +/** + * Return the root of the repository in which the current working directory + * resides, or null if the working directory contains no git repository. + * + * @return {String|null} + */ +exports.getRootGitDirectory = function () { + return getContainingGitDir(process.cwd()); +}; \ No newline at end of file diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index bbbc872a5..35e0d5f44 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -387,51 +387,6 @@ describe("GitUtil", function () { }); }); - describe("getRootGitDirectory", function () { - let cwd; - before(function () { - cwd = process.cwd(); - }); - after(function () { - process.chdir(cwd); - }); - - // This method is recursive, so we will check just three cases: - // - failure case - // - simple case - // - one deep - - it("failure", co.wrap(function *() { - const tempdir = yield TestUtil.makeTempDir(); - process.chdir(tempdir); - const result = GitUtil.getRootGitDirectory(); - assert.isNull(result); - })); - - it("successes", co.wrap(function *() { - const repo = yield TestUtil.createSimpleRepository(); - const workdir = repo.workdir(); - process.chdir(workdir); - const repoRoot = GitUtil.getRootGitDirectory(workdir); - assert(yield TestUtil.isSameRealPath(workdir, repoRoot), - "trivial"); - const subdir = path.join(workdir, "sub"); - yield fs.mkdir(subdir); - process.chdir(subdir); - const subRoot = GitUtil.getRootGitDirectory(workdir); - assert(yield TestUtil.isSameRealPath(workdir, subRoot), "trivial"); - })); - it("with a non-submodule link", co.wrap(function *() { - const tempdir = yield TestUtil.makeTempDir(); - process.chdir(tempdir); - const gitLink = path.join(tempdir, ".git"); - yield fs.writeFile(gitLink, "gitdir: /foo/bar"); - const result = GitUtil.getRootGitDirectory(); - assert.isNotNull(result); - assert(yield TestUtil.isSameRealPath(tempdir, result), result); - })); - }); - describe("getCurrentRepo", function () { let cwd; diff --git a/node/test/util/git_util_fast.js b/node/test/util/git_util_fast.js new file mode 100644 index 000000000..0204340ac --- /dev/null +++ b/node/test/util/git_util_fast.js @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2018, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const fs = require("fs-promise"); +const path = require("path"); + +const GitUtilFast = require("../../lib/util/git_util_fast"); +const TestUtil = require("../../lib/util/test_util"); + +describe("GitUtilFast", function () { + describe("getRootGitDirectory", function () { + let cwd; + before(function () { + cwd = process.cwd(); + }); + after(function () { + process.chdir(cwd); + }); + + // This method is recursive, so we will check just three cases: + // - failure case + // - simple case + // - one deep + + it("failure", co.wrap(function *() { + const tempdir = yield TestUtil.makeTempDir(); + process.chdir(tempdir); + const result = GitUtilFast.getRootGitDirectory(); + assert.isNull(result); + })); + + it("successes", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const workdir = repo.workdir(); + process.chdir(workdir); + const repoRoot = GitUtilFast.getRootGitDirectory(workdir); + assert(yield TestUtil.isSameRealPath(workdir, repoRoot), + "trivial"); + const subdir = path.join(workdir, "sub"); + yield fs.mkdir(subdir); + process.chdir(subdir); + const subRoot = GitUtilFast.getRootGitDirectory(workdir); + assert(yield TestUtil.isSameRealPath(workdir, subRoot), "trivial"); + })); + it("with a non-submodule link", co.wrap(function *() { + const tempdir = yield TestUtil.makeTempDir(); + process.chdir(tempdir); + const gitLink = path.join(tempdir, ".git"); + yield fs.writeFile(gitLink, "gitdir: /foo/bar"); + const result = GitUtilFast.getRootGitDirectory(); + assert.isNotNull(result); + assert(yield TestUtil.isSameRealPath(tempdir, result), result); + })); + }); +}); From ae4f3e4bef743f2647cac1ac9b937b03caca43f4 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Tue, 16 Oct 2018 00:22:48 -0700 Subject: [PATCH 224/402] Remove dead code. --- node/lib/cmd/forward.js | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/node/lib/cmd/forward.js b/node/lib/cmd/forward.js index ffbc0ee7c..c55c8e16f 100644 --- a/node/lib/cmd/forward.js +++ b/node/lib/cmd/forward.js @@ -32,42 +32,6 @@ const co = require("co"); -/** - * Return an object to be used in configuring the help of the main git-meta - * parwser. - * - * @param {String} name - * @return {Object} - * @return {String} return.helpText help description - * @return {String} return.description detailed description - * @return {Function} configureParser set up parser for this command - * @return {Function} executeableSubcommand function to invoke command - */ -exports.makeModule = function (name) { - const ArgParse = require("argparse"); - const assert = require("chai").assert; - - function configureParser(parser) { - parser.addArgument(["args"], { - type: "string", - help: `Arguments to pass to 'git ${name}'.`, - nargs: ArgParse.Const.REMAINDER, - }); - } - - const helpText = `\ -Invoke 'git -C $(git meta root) ${name}' with all arguments.`; - return { - helpText: helpText, - description: `\ -${helpText} See 'git ${name} --help' for more information.`, - configureParser: configureParser, - executeableSubcommand: function () { - assert(false, "should never get here"); - }, - }; -}; - /** * Forward the specified `args` to the Git command having the specified `name`. * From ea7782aa2fdd1310eac736841d024f4d041eacef Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Tue, 16 Oct 2018 15:27:58 -0700 Subject: [PATCH 225/402] Do not pass -C to git unless in a git meta clone. --- node/lib/cmd/forward.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/node/lib/cmd/forward.js b/node/lib/cmd/forward.js index c55c8e16f..514627b3d 100644 --- a/node/lib/cmd/forward.js +++ b/node/lib/cmd/forward.js @@ -46,11 +46,15 @@ exports.execute = co.wrap(function *(name, args) { if (name === "diff") { args.splice(0, 0, "--submodule=diff"); } - const gitArgs = [ - "-C", - GitUtilFast.getRootGitDirectory(), - name, - ].concat(args); + const gitArgs = []; + + const rootDir = GitUtilFast.getRootGitDirectory(); + if (rootDir) { + gitArgs.push("-C", rootDir); + } + gitArgs.push(name); + gitArgs.concat(args); + try { yield ChildProcess.spawn("git", gitArgs, { stdio: "inherit", From 4975c4b77e3cda9362dcbc04fb3b64dd7c7433e4 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Thu, 18 Oct 2018 10:09:04 -0400 Subject: [PATCH 226/402] Use concat properly. --- node/lib/cmd/forward.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/node/lib/cmd/forward.js b/node/lib/cmd/forward.js index 514627b3d..48f131cab 100644 --- a/node/lib/cmd/forward.js +++ b/node/lib/cmd/forward.js @@ -53,10 +53,9 @@ exports.execute = co.wrap(function *(name, args) { gitArgs.push("-C", rootDir); } gitArgs.push(name); - gitArgs.concat(args); try { - yield ChildProcess.spawn("git", gitArgs, { + yield ChildProcess.spawn("git", gitArgs.concat(args), { stdio: "inherit", }); } From 9e49f32ca8f7b2e762f0d9f1ecf70d2092c680c0 Mon Sep 17 00:00:00 2001 From: shijing Date: Mon, 22 Oct 2018 14:37:33 -0400 Subject: [PATCH 227/402] #678 fix git meta creates empty commits issue (#679) * #678 fix git meta creates empty commits issue * Address @novalis's comments --- node/lib/util/cherry_pick_util.js | 12 +++++++++--- node/lib/util/submodule_rebase_util.js | 5 +++++ node/test/util/cherry_pick_util.js | 11 +++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 3c94171a9..d1fae8bd4 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -370,6 +370,7 @@ exports.computeChanges = co.wrap(function *(repo, index, targetCommit) { * @return {Object} * @return {Object} return.commits map from name to map from new to old ids * @return {Object} return.conflicts map from name to commit causing conflict + * @returns {Object} return.ffwds map from name to if ffwd happend */ exports.pickSubs = co.wrap(function *(metaRepo, opener, metaIndex, subs) { assert.instanceOf(metaRepo, NodeGit.Repository); @@ -379,6 +380,7 @@ exports.pickSubs = co.wrap(function *(metaRepo, opener, metaIndex, subs) { const result = { commits: {}, conflicts: {}, + ffwds: {}, }; const fetcher = yield opener.fetcher(); const pickSub = co.wrap(function *(name) { @@ -400,6 +402,7 @@ ${colors.green(commitText)}.`); newCommit, oldCommit); result.commits[name] = rewriteResult.commits; + result.ffwds[name] = rewriteResult.ffwd; yield metaIndex.addByPath(name); if (null !== rewriteResult.conflictedCommit) { result.conflicts[name] = rewriteResult.conflictedCommit; @@ -544,9 +547,12 @@ exports.rewriteCommit = co.wrap(function *(repo, commit) { newMetaCommit: null, }; yield SparseCheckoutUtil.writeMetaIndex(repo, index); - if ("" === errorMessage && - (0 !== Object.keys(changes.simpleChanges).length || - 0 !== Object.keys(picks.commits).length)) { + const nChanges = Object.keys(picks.commits) + .map(name => Object.keys( + picks.commits[name]).length + picks.ffwds[name] ? 1 : 0 + ).reduce((acc, len) => acc + len, 0); + if ("" === errorMessage && + (0 !== Object.keys(changes.simpleChanges).length || 0 !== nChanges)) { result.newMetaCommit = yield SubmoduleRebaseUtil.makeCommit(repo, commit); } diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index 20c082888..379ce4043 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -157,6 +157,7 @@ exports.callNext = co.wrap(function *(rebase) { * @return {Object} * @return {Object} return.commits * @return {String|null} return.conflictedCommit + * @returns {Boolean} return.ffwd */ exports.processRebase = co.wrap(function *(repo, rebase, op) { assert.instanceOf(repo, NodeGit.Repository); @@ -167,6 +168,7 @@ exports.processRebase = co.wrap(function *(repo, rebase, op) { const result = { commits: {}, conflictedCommit: null, + ffwd: false, }; const signature = yield ConfigUtil.defaultSignature(repo); while (null !== op) { @@ -206,6 +208,7 @@ exports.processRebase = co.wrap(function *(repo, rebase, op) { * @return {Object} * @return {Object} return.commits new sha to original sha * @return {String|null} return.conflictedCommit error message if failed + * @return {Boolean} return.ffwd true if fast-forwarded */ exports.rewriteCommits = co.wrap(function *(repo, branch, upstream) { assert.instanceOf(repo, NodeGit.Repository); @@ -221,6 +224,7 @@ exports.rewriteCommits = co.wrap(function *(repo, branch, upstream) { const result = { commits: {}, conflictedCommit: null, + ffwd: false, }; // If we're up-to-date with the commit to be rebased onto, return @@ -252,6 +256,7 @@ exports.rewriteCommits = co.wrap(function *(repo, branch, upstream) { if (null === upstream) { if (yield NodeGit.Graph.descendantOf(repo, branchSha, headSha)) { yield GitUtil.setHeadHard(repo, branch); + result.ffwd = true; return result; // RETURN } } diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index d7faba1ea..524493c4c 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -790,6 +790,13 @@ a=Ax:Cz-y;Cy-x;Bfoo=z| x=S:C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2;Os H=x`, expected: "x=E:C9-2 s=Sa:zs;Bmaster=9;Os Czs-x z=z!H=zs", }, + "skip duplicated cherry picks": { + input: ` +a=Ax:Cz-y;Cy-x;Bfoo=z| +x=S:C8-3 s=Sa:z;C3-2 s=Sa:y;C2-1 s=Sa:x;Bfoo=8;Bmaster=2;Os H=x`, + expected: "x=E:C9-2 s=Sa:zs;Bmaster=9;Os Czs-x z=z!H=zs", + duplicate: true, + }, "nothing to commit": { input: "a=B|x=S:C2-1;C8-1 ;Bmaster=2;B8=8", }, @@ -834,6 +841,10 @@ Submodule ${colors.red("s")} is conflicted. const eightCommit = yield x.getCommit(eightCommitSha); const result = yield CherryPickUtil.cherryPick(x, eightCommit); + if (c.duplicate) { + const res = yield CherryPickUtil.cherryPick(x, eightCommit); + assert.isNull(res.newMetaCommit); + } assert.equal(result.errorMessage, c.errorMessage || null); return mapCommits(maps, result); From 62e5ed2d110b5bd4a95185963ccc1440421d3303 Mon Sep 17 00:00:00 2001 From: Adam Bliss Date: Tue, 23 Oct 2018 09:43:12 -0400 Subject: [PATCH 228/402] Fix typo in architecture.md --- doc/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/architecture.md b/doc/architecture.md index f92cfbcd9..e984b2b9f 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -131,7 +131,7 @@ XML file. It does not generally try to provide a full suite of cross-repository operations (such as `rebase`, `cherry-pick`, etc.) and assumes the use of the Gerrit code review tool. -Git submodules come the close: they do provide the technical ability to solve +Git submodules come the closest: they do provide the technical ability to solve the problem, but are very difficult to use and lack some of the desired features. With git-meta, we build on top of Git submodules to provide the desired functionality by leveraging only existing Git commands. From 3b75aac45dfe650ceb1fe9ce5d0d9f989599dd9d Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Tue, 23 Oct 2018 15:01:11 -0400 Subject: [PATCH 229/402] Fix small typo. --- node/lib/util/submodule_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 66622ebcf..ac7acb996 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -441,7 +441,7 @@ Modification to meta-repo file ${colors.red(path)} is not supported.`); newFile.id().tostrS()); } else if (!allowMetaChanges && path !== modulesFileName) { throw new UserError(`\ -Addtion to meta-repo of file ${colors.red(path)} is not supported.`); +Addition to meta-repo of file ${colors.red(path)} is not supported.`); } } break; case DELTA.DELETED: { From 4c56ddd64e8658d4529c53c36446fc70412992e7 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Tue, 23 Oct 2018 17:16:50 -0400 Subject: [PATCH 230/402] New push logic that uses remote refs. --- node/lib/util/push.js | 159 +++++++++++++++++++++-------------------- node/test/util/push.js | 135 ++++++++++++++++++++++++++++++++-- 2 files changed, 213 insertions(+), 81 deletions(-) diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 6e929333a..5be5f17fa 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -46,14 +46,74 @@ const SubmoduleConfigUtil = require("./submodule_config_util"); const SyntheticBranchUtil = require("./synthetic_branch_util"); const UserError = require("./user_error"); +// This magic SHA represents an empty tree in Git. + +const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; + +/** + * For a given commit and remote, determine a reasonably close oid that has + * already been pushed. This does *not* do a fetch, but rather just compares + * against all available remote refs. To compare against all remotes, simply + * pass a "*" wildcard. + * + * This works by building a revwalk and removing all ancestors of remote refs + * for the given remote. It then reverses the result, and returns the first + * parent of the oldest, unpushed commit. This is not an absolute last pushed + * oid, but is a very good proxy to determine what to push. + * + * Returns a commit that exists in a remote ref, or null if no such commit + * exists. If the given commit exists in a remote ref, will return itself. + * + * @async + * @param {NodeGit.Repository} repo + * @param {String} remoteName + * @param {NodeGit.Commit} commit + */ +exports.getClosePushedCommit = co.wrap(function*(repo, remoteName, commit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(remoteName); + assert.instanceOf(commit, NodeGit.Commit); + + // Search for the first commit that is not an ancestor of a remote ref, ie. + // the first commit that is unpushed. First by topological, then resolving + // by timestamp. + + const walk = NodeGit.Revwalk.create(repo); + walk.sorting(NodeGit.Revwalk.SORT.TOPOLOGICAL, + NodeGit.Revwalk.SORT.TIME, + NodeGit.Revwalk.SORT.REVERSE); + walk.hideGlob(`refs/remotes/${remoteName}`); + walk.push(commit); + + // This occurs when walk.next() has no commits left -- in this case, + // when there are no unpushed commits. Return the passed commit as the + // last pushed commit. + + let firstNewOid; + try { + firstNewOid = yield walk.next(); + } catch (err) { + if (NodeGit.Error.CODE.ITEROVER === err.errno) { + return commit; + } + throw err; + } + + const firstNewCommit = yield repo.getCommit(firstNewOid); + + // If the first unpushed commit has no parents, then the entire set of + // commits to push is new. + + if (0 === firstNewCommit.parentcount()) { + return null; + } + return yield repo.getCommit(firstNewCommit.parentId(0)); +}); /** * For a given proposed push, return a map from submodule to sha, * excluding any submodules that the server likely already has. * - * Create a list of relevant branches, including any tracking branches - * associated with the one being pushed and the target branch. - * * Return in the map only those submodules that (1) exist locally, in * `.git/modules` and (2) are changed between `commit` and the merge base of * `commit` and each relevant branch. @@ -62,101 +122,46 @@ const UserError = require("./user_error"); * @param {NodeGit.Repository} repo * @param {String} remoteName * @param {String} source - * @param {String} target * @param {NodeGit.Commit} commit */ -exports.getPushMap = co.wrap(function*(repo, remoteName, source, target, +exports.getPushMap = co.wrap(function*(repo, remoteName, source, commit) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(remoteName); assert.isString(source); - assert.isString(target); assert.instanceOf(commit, NodeGit.Commit); - const submoduleSet = new Set(yield SubmoduleUtil.listOpenSubmodules(repo)); - for (const s of yield SubmoduleUtil.listAbsorbedSubmodules(repo)) { - submoduleSet.add(s); - } - const submodules = [...submoduleSet]; - const pushMap = yield SubmoduleUtil.getSubmoduleShasForCommit(repo, - submodules, - commit); - const bareRepos = {}; - for (const sub of Object.keys(pushMap)) { - const subBareRepo = yield SubmoduleUtil.getBareRepo(repo, sub); - bareRepos[sub] = subBareRepo; + const baseCommit = yield exports.getClosePushedCommit(repo, remoteName, + commit); + let baseTree; + if (null !== baseCommit) { + baseTree = yield baseCommit.getTree(); + } else { + baseTree = EMPTY_TREE; } - const trackingBranches = new Set(); - trackingBranches.add(`refs/remotes/${remoteName}/${target}`); - let tracking; - try { - const sourceBranch = yield repo.getReference(source); - tracking = yield GitUtil.getTrackingInfo(repo, sourceBranch); - } catch (e) { - // we have no local branch? maybe source is a sha. - tracking = null; - } + const tree = yield commit.getTree(); + const diff = yield NodeGit.Diff.treeToTree(repo, baseTree, tree, null); + const changes = SubmoduleUtil.getSubmoduleChangesFromDiff(diff, true); - if (tracking !== null) { - if (tracking.remoteName !== null) { - trackingBranches.add( - `refs/remotes/${tracking.remoteName}/${tracking.branchName}`); - } - if (tracking.pushRemoteName !== null) { - trackingBranches.add( - `refs/remotes/${tracking.pushRemoteName}/${target}`); + const pushMap = {}; + for (const path of Object.keys(changes)) { + const change = changes[path]; + if (!change.deleted) { + pushMap[path] = change.newSha; } } - // List all the merge bases for all tracking branches. - - const bases = []; - - yield Array.from(trackingBranches).map(co.wrap(function *(branch) { - let reference = null; - try { - reference = yield NodeGit.Reference.lookup(repo, branch); - } catch (e) { - // if this ref doesn't exist, OK - return; - } - const id = reference.target(); - const trackingCommit = yield NodeGit.Commit.lookup(repo, id); - const branchBases = yield GitUtil.mergeBases(repo, - trackingCommit, - commit); - bases.push(...branchBases); - })); - - // Then clear out all entries that weren't changed in each merge base. - - yield bases.map(co.wrap(function *(baseCommitSha) { - const baseCommit = yield repo.getCommit(baseCommitSha); - const changes = yield SubmoduleUtil.getSubmoduleChanges(repo, - commit, - baseCommit, - true); - // Remove any entry in `pushMap` that wasn't changed between `commit` - // and this merge base. - - for (let name in pushMap) { - if (!(name in changes)) { - delete pushMap[name]; - } - } - })); - // Make sure we have the commits we want to push. - for (const sub of Object.keys(pushMap)) { - const subRepo = bareRepos[sub]; + const subRepo = yield SubmoduleUtil.getBareRepo(repo, sub); try { yield subRepo.getCommit(pushMap[sub]); } catch (e) { delete pushMap[sub]; } } + return pushMap; }); @@ -205,7 +210,7 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { const commit = yield repo.getCommit(sha); // First, push the submodules. - const pushMap = yield exports.getPushMap(repo, remoteName, source, target, + const pushMap = yield exports.getPushMap(repo, `*`, source, commit); let errorMessage = ""; diff --git a/node/test/util/push.js b/node/test/util/push.js index 46bc10193..5e37b8be8 100644 --- a/node/test/util/push.js +++ b/node/test/util/push.js @@ -30,8 +30,9 @@ */ "use strict"; -const assert = require("chai").assert; -const co = require("co"); +const assert = require("chai").assert; +const co = require("co"); +const NodeGit = require("nodegit"); const GitUtil = require("../../lib/util/git_util"); const Push = require("../../lib/util/push"); @@ -298,7 +299,7 @@ x=S:B3=3;C2-1 s=Sa:1;Rorigin=a master=3;Rtarget=b;Bmaster=2 origin/master;Os`, } const pushMap = yield Push.getPushMap(repo, "origin", source, - "target", commit); + commit); const mappedPushMap = {}; for (const sub of Object.keys(pushMap)) { mappedPushMap[sub] = commitMap.commitMap[pushMap[sub]]; @@ -350,7 +351,7 @@ x=S:B3=3;C2-1 s=Sa:1;Rorigin=a master=3;Rtarget=b;Bmaster=2 origin/master;Os`, const sha = theirCommitMap["2"]; const commit = yield ourRepo.getCommit(sha); const pushMap = yield Push.getPushMap(ourRepo, "upstream", "2", - "target", commit); + commit); assert.deepEqual({}, pushMap); })); }); @@ -464,3 +465,129 @@ x=E:Rorigin=a foo=2`, })); }); }); + +describe("getClosePushedCommit", function () { + // 5 -> 4 -> 3 -> 1 + // -> 2 -> + const baseRepo = "x=S:C2-1;C3-1;C4-3,2;C5-4;Bmaster=5"; + + const cases = { + "no refs, no close pushed commit": { + source: "5", + remote: "origin", + refs: { + }, + expectedCommit: null, + }, + "no refs, with wildcard": { + source: "5", + remote: "*", + refs: { + }, + expectedCommit: null, + }, + "one refs, commit is pushed": { + source: "5", + remote: "*", + refs: { + "refs/remotes/origin/1": "5", + }, + expectedCommit: "5", + }, + "one refs, far commit": { + source: "5", + remote: "*", + refs: { + "refs/remotes/origin/1": "1", + }, + expectedCommit: "1", + }, + "one refs, close commit": { + source: "5", + remote: "*", + refs: { + "refs/remotes/origin/1": "2", + }, + expectedCommit: "1", + }, + "lots of refs, none matching": { + source: "5", + remote: "origin", + refs: { + "refs/remotes/foo/1": "1", + "refs/remotes/foo/2": "2", + "refs/remotes/bar/3": "3", + "refs/remotes/bar/4": "4", + }, + expectedCommit: null, + }, + "lots of refs, something matching": { + source: "5", + remote: "origin", + refs: { + "refs/remotes/origin/1": "1", + "refs/remotes/foo/2": "2", + "refs/remotes/bar/3": "3", + "refs/remotes/bar/4": "4", + }, + expectedCommit: "1", + }, + "lots of refs, something matching 2": { + source: "5", + remote: "origin", + refs: { + "refs/remotes/foo/1": "1", + "refs/remotes/foo/2": "2", + "refs/remotes/bar/3": "3", + "refs/remotes/origin/4": "4", + }, + expectedCommit: "4", + }, + "lots of refs, all matching": { + source: "5", + remote: "*", + refs: { + "refs/remotes/origin/1": "1", + "refs/remotes/origin/2": "2", + "refs/remotes/bar/3": "3", + "refs/remotes/bar/4": "4", + }, + expectedCommit: "4", + }, + "lots of refs, commit is pushed": { + source: "5", + remote: "*", + refs: { + "refs/remotes/origin/1": "1", + "refs/remotes/origin/2": "2", + "refs/remotes/bar/3": "3", + "refs/remotes/bar/4": "5", + }, + expectedCommit: "5", + }, + }; + + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const repos = yield RepoASTTestUtil.createMultiRepos(baseRepo); + const repo = repos.repos.x; + + for (const ref of Object.keys(c.refs)) { + yield NodeGit.Reference.create( + repo, ref, repos.reverseCommitMap[c.refs[ref]], 1, ""); + } + + const commit = yield repo.getCommit( + repos.reverseCommitMap[c.source]); + const actualCommit = yield Push.getClosePushedCommit( + repo, c.remote, commit); + if (null !== c.expectedCommit) { + assert.deepEqual(repos.reverseCommitMap[c.expectedCommit], + actualCommit.id().tostrS()); + } else { + assert.deepEqual(null, actualCommit); + } + })); + }); +}); \ No newline at end of file From 6959b3a8a181e39fdafdd7ff83d537341f03e894 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Tue, 23 Oct 2018 17:41:41 -0400 Subject: [PATCH 231/402] Remove dead code. --- node/lib/util/git_util.js | 97 -------------------------------------- node/test/util/git_util.js | 47 ------------------ 2 files changed, 144 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 02e3f9fd7..896c63753 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -44,7 +44,6 @@ const NodeGit = require("nodegit"); const path = require("path"); const ConfigUtil = require("./config_util"); -const DoWorkQueue = require("./do_work_queue"); const Hook = require("./hook"); const GitUtilFast = require("./git_util_fast"); const UserError = require("./user_error"); @@ -548,102 +547,6 @@ exports.fetchSha = co.wrap(function *(repo, url, sha) { return true; }); - - -/** - * Return a list the shas of commits in the history of the specified `commit` - * not present in the history of the specified `remote` in the specified - * `repo`. Note that this command does not do a *fetch*; the check is made - * against what commits are locally known. - * - * async - * @param {NodeGit.Repository} repo - * @param {String} remote - * @param {String} commit - * @return {NodeGit.Oid []} - */ -exports.listUnpushedCommits = co.wrap(function *(repo, remote, commit) { - // I wish there were a simpler way to do this. Our algorithm: - // 1. List all the refs for 'remote'. - // 2. Compute the list of different commits between each the head of each - // matching remote ref. - // 3. Return the shortest list. - // 4. If no matching refs return a list of all commits that are in the - // history of 'commit'. - - assert.instanceOf(repo, NodeGit.Repository); - assert.isString(remote); - assert.isString(commit); - - const refs = yield repo.getReferenceNames(NodeGit.Reference.TYPE.LISTALL); - - const commitId = NodeGit.Oid.fromString(commit); - - let bestResult = null; - - const regex = new RegExp(`^refs/remotes/${remote}/`); - - // The `fastWalk` method takes a max count for the number of items it will - // return. We should investigate why some time because I don't think it - // should be necessary. My guess is that they are creating a fixed size - // array to populate with the commits; an exponential growth algorithm - // like that used by `std::vector` would provide the same (amortized) - // performance. See http://www.nodegit.org/api/revwalk/#fastWalk. - // - // For now, I'm choosing the value 1000000 as something not big enough to - // blow out memory but more than large enough for any repo we're likely to - // encounter. - - const MAX_COMMIT_COUNT = 1000000; - - const checkRef = co.wrap(function *(name) { - - // If we've already matched the commit, no need to do any checking. - - if ([] === bestResult) { - return; // RETURN - } - - // Check to see if the name of the ref indicates that it is for - // 'remote'. - - const nameResult = regex.exec(name); - if (!nameResult) { - return; // RETURN - } - - const refHeadCommit = yield repo.getReferenceCommit(name); - const refHead = refHeadCommit.id(); - - // Use 'RevWalk' to generate the list of commits different between the - // head of the remote branch and our commit. - - let revWalk = repo.createRevWalk(); - revWalk.pushRange(`${refHead}..${commit}`); - const commitDiff = yield revWalk.fastWalk(MAX_COMMIT_COUNT); - - // If this list is shorter than the current best list (or there is no - // current best), store it as the best so far. - - if (null === bestResult || bestResult.length > commitDiff.length) { - bestResult = commitDiff; - } - }); - - yield DoWorkQueue.doInParallel(refs, checkRef); - - // If we found no results (no branches for 'remote', return a list - // containing 'commit' and all its history. - - if (null === bestResult) { - let revWalk = repo.createRevWalk(); - revWalk.push(commitId); - return yield revWalk.fastWalk(MAX_COMMIT_COUNT); // RETURN - } - - return bestResult; -}); - /** * Return true if the specified `source` commit is up-to-date with the * specified `target` commit in the specified `repo`. A commit is up-to-date diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index 35e0d5f44..9f0d18ca9 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -744,53 +744,6 @@ describe("GitUtil", function () { })); }); - describe("listUnpushedCommits", function () { - const cases = { - "no branches": { - input: "S:Rorigin=foo", - from: "1", - remote: "origin", - expected: ["1"], - }, - "up to date": { - input: "S:Rorigin=foo moo=1", - from: "1", - remote: "origin", - expected: [], - }, - "one not pushed": { - input: "S:C2-1;Bmaster=2;Rorigin=foo moo=1", - from: "2", - remote: "origin", - expected: ["2"], - }, - "two not pushed": { - input: "S:C3-2;C2-1;Bmaster=3;Rorigin=foo moo=1", - from: "3", - remote: "origin", - expected: ["2","3"], - }, - }; - Object.keys(cases).forEach(caseName => { - const c = cases[caseName]; - it(caseName, co.wrap(function *() { - const ast = ShorthandParserUtil.parseRepoShorthand(c.input); - const path = yield TestUtil.makeTempDir(); - const written = yield WriteRepoASTUtil.writeRAST(ast, path); - const fromSha = written.oldCommitMap[c.from]; - const unpushed = yield GitUtil.listUnpushedCommits( - written.repo, - c.remote, - fromSha); - const unpushedShas = unpushed.map(id => { - assert.instanceOf(id, NodeGit.Oid); - return written.commitMap[id.tostrS()]; - }); - assert.sameMembers(unpushedShas, c.expected); - })); - }); - }); - describe("isUpToDate", function () { const cases = { "trivial": { From a17cf27acd87c0ac241f8f872dcac47fd39426a3 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Wed, 31 Oct 2018 15:12:43 -0400 Subject: [PATCH 232/402] Repositories must be available (open or absorbed) if pushing. --- node/lib/util/push.js | 14 +++++++++++++- node/test/util/push.js | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 5be5f17fa..199bacf89 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -152,8 +152,20 @@ exports.getPushMap = co.wrap(function*(repo, remoteName, source, } } - // Make sure we have the commits we want to push. + const openSubmodules = yield SubmoduleUtil.listOpenSubmodules(repo); + const absorbedSubmodules = + yield SubmoduleUtil.listAbsorbedSubmodules(repo); + + const availableSubmodules = new Set([...openSubmodules, + ...absorbedSubmodules]); + + // Make sure we have the repositories and commits we want to push. for (const sub of Object.keys(pushMap)) { + if (!availableSubmodules.has(sub)) { + delete pushMap[sub]; + continue; + } + const subRepo = yield SubmoduleUtil.getBareRepo(repo, sub); try { yield subRepo.getCommit(pushMap[sub]); diff --git a/node/test/util/push.js b/node/test/util/push.js index 5e37b8be8..4ee86645e 100644 --- a/node/test/util/push.js +++ b/node/test/util/push.js @@ -33,6 +33,7 @@ const assert = require("chai").assert; const co = require("co"); const NodeGit = require("nodegit"); +const rimraf = require("rimraf"); const GitUtil = require("../../lib/util/git_util"); const Push = require("../../lib/util/push"); @@ -354,6 +355,45 @@ x=S:B3=3;C2-1 s=Sa:1;Rorigin=a master=3;Rtarget=b;Bmaster=2 origin/master;Os`, commit); assert.deepEqual({}, pushMap); })); + + it("local clone is missing directory", co.wrap(function*() { + const them = "sub=S:C8-1;Bmaster=8|x=S:C2-1 d=Ssub:8;Bmaster=2"; + const our = "sub=S:C7-1;Bmaster=7|x=S:C3-1 d=Ssub:7;Bmaster=3;Od"; + + const theirWritten = yield RepoASTTestUtil.createMultiRepos(them); + const theirRepo = theirWritten.repos.x; + + const ourWritten = yield RepoASTTestUtil.createMultiRepos(our); + const ourRepo = ourWritten.repos.x; + const ourCommitMap = ourWritten.reverseCommitMap; + + const config = yield ourRepo.config(); + yield config.setString("remote.upstream.url", theirRepo.path()); + + const sha = ourCommitMap["3"]; + const commit = yield ourRepo.getCommit(sha); + const pushMap = yield Push.getPushMap(ourRepo, "upstream", "2", + commit); + assert.deepEqual({"d": ourCommitMap["7"]}, pushMap); + + // "Delete" the codebase, but remain absorbed. + yield (new Promise(callback => { + return rimraf(ourWritten.repos.x.workdir() + "d", {}, + callback); + })); + const pushMapDel = yield Push.getPushMap(ourRepo, "upstream", "2", + commit); + assert.deepEqual({"d": ourCommitMap["7"]}, pushMapDel); + + // Remove the absorbed codebase. + yield (new Promise(callback => { + return rimraf(ourWritten.repos.x.path() + "modules/d", {}, + callback); + })); + const pushMapRm = yield Push.getPushMap(ourRepo, "upstream", "2", + commit); + assert.deepEqual({}, pushMapRm); + })); }); describe("push", function () { From 9358cf2e5d83a7f8540023795e3b0c4da541e298 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 6 Nov 2018 15:02:26 -0500 Subject: [PATCH 233/402] simplify handling of no-ff ffwd merges (and make them actually work) --- node/lib/util/merge_util.js | 74 ++++++++---------------------------- node/test/util/merge_util.js | 44 ++------------------- 2 files changed, 19 insertions(+), 99 deletions(-) diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 137f5c040..94f75bf64 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -98,77 +98,35 @@ const MODE = { exports.MODE = MODE; /** - * Perform a fast-forward merge in the specified `repo` to the specified - * `commit`. If `MODE.FORCE_COMMIT === mode`, generate a merge commit. When - * generating a merge commit, use the optionally specified `message`. The - * behavior is undefined unless `commit` is different from but descendant of - * the HEAD commit in `repo`. If a commit is generated, return its sha; - * otherwise, return null. + * Perform a fast-forward merge in the specified `repo` to the + * specified `commit`. When generating a merge commit, use the + * optionally specified `message`. The behavior is undefined unless + * `commit` is different from but descendant of the HEAD commit in + * `repo`. * * @param {NodeGit.Repository} repo * @param {MODE} mode * @param {NodeGit.Commit} commit * @param {String|null} message - * @return {String|null} + */ -exports.fastForwardMerge = co.wrap(function *(repo, mode, commit, message) { +exports.fastForwardMerge = co.wrap(function *(repo, commit, message) { assert.instanceOf(repo, NodeGit.Repository); - assert.isNumber(mode); assert.instanceOf(commit, NodeGit.Commit); assert.isString(message); // Remember the current branch; the checkoutCommit function will move it. const branch = yield repo.getCurrentBranch(); - let result = null; - let newHead; - if (MODE.FORCE_COMMIT !== mode) { - // If we're not generating a commit, we just need to checkout the one - // we're fast-forwarding to. - - yield Checkout.checkoutCommit(repo, commit, false); - newHead = commit; - } - else { - // Checkout the commit we're fast-forwarding to. - - const head = yield repo.getHeadCommit(); - yield Checkout.checkoutCommit(repo, commit, false); - - // Then, generate a new commit that has the previous HEAD and commit to - // merge as children. - - const sig = yield ConfigUtil.defaultSignature(repo); - const tree = yield commit.getTree(); - const id = yield NodeGit.Commit.create( - repo, - 0, - sig, - sig, - null, - Commit.ensureEolOnLastLine(message), - tree, - 2, - [head, commit]); - newHead = yield repo.getCommit(id); - result = newHead.id().tostrS(); - - // Move HEAD to point to the new commit. - - yield NodeGit.Reset.reset(repo, - newHead, - NodeGit.Reset.TYPE.HARD, - null, - branch.name()); - } + + yield Checkout.checkoutCommit(repo, commit, false); // If we were on a branch, make it current again. if (branch.isBranch()) { - yield branch.setTarget(newHead, "ffwd merge"); + yield branch.setTarget(commit, "ffwd merge"); yield repo.setHead(branch.name()); } - return result; }); /** @@ -382,15 +340,13 @@ running merge.`); throw new UserError(`The meta-repository cannot be fast-forwarded to \ ${colors.red(commitSha)}.`); } - else if (canFF) { + else if (canFF && MODE.FORCE_COMMIT !== mode) { console.log(`Fast-forwarding meta-repo to ${colors.green(commitSha)}.`); - - const sha = yield exports.fastForwardMerge(repo, - mode, - commit, - message); - result.metaCommit = sha; + result.metaCommit = commitSha; + yield exports.fastForwardMerge(repo, + commit, + message); return result; } diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index b8930b05f..b7e717eb9 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -67,36 +67,25 @@ function mapReturnedCommits(result, maps) { describe("MergeUtil", function () { describe("fastForwardMerge", function () { - const MODE = MergeUtil.MODE; const cases = { "simple": { initial: "a=B|x=S:C2-1 q=Sa:1;Bfoo=2", commit: "2", - mode: MODE.NORMAL, - expected: "x=E:Bmaster=2", - }, - "simple, FF_ONLY": { - initial: "a=B|x=S:C2-1 t/y=Sa:1;Bfoo=2", - commit: "2", - mode: MODE.FF_ONLY, expected: "x=E:Bmaster=2", }, "simple detached": { initial: "a=B|x=S:C2-1 u=Sa:1;Bfoo=2;*=", commit: "2", - mode: MODE.NORMAL, expected: "x=E:H=2", }, "with submodule": { initial: "a=B:Ca-1;Ba=a|x=U:C3-2 s=Sa:a;Bfoo=3", commit: "3", - mode: MODE.NORMAL, expected: "x=E:Bmaster=3", }, "with open submodule": { initial: "a=B:Ca-1;Ba=a|x=U:C3-2 s=Sa:a;Bfoo=3;Os", commit: "3", - mode: MODE.NORMAL, expected: "x=E:Bmaster=3;Os H=a", }, "with open submodule and change": { @@ -104,7 +93,6 @@ describe("MergeUtil", function () { a=B:Ca-1;Ba=a| x=U:C3-2 s=Sa:a;Bfoo=3;Os W README.md=3`, commit: "3", - mode: MODE.NORMAL, expected: "x=E:Bmaster=3;Os H=a!W README.md=3", }, "with open submodule and conflict": { @@ -112,23 +100,8 @@ x=U:C3-2 s=Sa:a;Bfoo=3;Os W README.md=3`, a=B:Ca-1;Ba=a| x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, commit: "3", - mode: MODE.NORMAL, fails: true, }, - "force commit": { - initial: "a=B|x=S:C2-1 z=Sa:1;Bfoo=2", - commit: "2", - mode: MODE.FORCE_COMMIT, - expected: "x=E:Chahaha\n#x-1,2 z=Sa:1;Bmaster=x", - message: "hahaha", - }, - "force commit, detached": { - initial: "a=B|x=S:C2-1 y=Sa:1;Bfoo=2;*=", - commit: "2", - mode: MODE.FORCE_COMMIT, - expected: "x=E:Chahaha\n#x-1,2 y=Sa:1;H=x", - message: "hahaha", - }, "ff merge adding submodule": { initial: "a=S|x=U:Bfoo=1;*=foo", commit: "2", @@ -144,20 +117,11 @@ x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, const physicalCommit = reverseCommitMap[c.commit]; const commit = yield x.getCommit(physicalCommit); const message = c.message || "message\n"; - const mode = c.mode || MODE.NORMAL; - const result = yield MergeUtil.fastForwardMerge(x, - mode, - commit, - message); - let newCommitMap = {}; - if (null !== result) { - assert.isString(result); - // If a new commit was generated, map it to "x". - - newCommitMap[result] = "x"; - } + yield MergeUtil.fastForwardMerge(x, + commit, + message); return { - commitMap: newCommitMap, + commitMap: {}, }; }); it(caseName, co.wrap(function *() { From f3c6d2a796a8643dbfcf498a7aabb87354b8f223 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 7 Nov 2018 01:10:34 -0500 Subject: [PATCH 234/402] restore thread safety --- node/lib/git-meta.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js index a31cf8606..4fb4bf010 100755 --- a/node/lib/git-meta.js +++ b/node/lib/git-meta.js @@ -161,7 +161,7 @@ const optimized = { Object.keys(commands).sort().forEach(name => { const cmd = commands[name]; - configureSubcommand(subParser, name, cmd, (null !== optimized[name])); + configureSubcommand(subParser, name, cmd, (undefined !== optimized[name])); }); const blacklist = new Set([ From bac168be369bbf9b8b9b9946da2cd3ff1db018a3 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Fri, 9 Nov 2018 15:48:13 -0500 Subject: [PATCH 235/402] Fix git-meta rm path overmatching issue --- node/lib/util/merge_util.js | 1 - node/lib/util/submodule_util.js | 10 +++++++++- node/test/util/submodule_util.js | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 94f75bf64..9bad9d686 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -38,7 +38,6 @@ const NodeGit = require("nodegit"); const Checkout = require("./checkout"); const CherryPickUtil = require("./cherry_pick_util"); -const Commit = require("./commit"); const ConfigUtil = require("./config_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index ac7acb996..7630ce55b 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -559,11 +559,19 @@ exports.getSubmodulesInPath = function (dir, indexSubNames) { if ("" === dir) { return indexSubNames; } + + // test if the short path a parent dir of the long path + const isParentDir = (short, long) => { + return long.startsWith(short) && ( + short[short.length-1] === "/" || + long.replace(short, "")[0] === "/" + ); + }; const result = []; for (const subPath of indexSubNames) { if (subPath === dir) { return [dir]; // RETURN - } else if (subPath.startsWith(dir)) { + } else if (isParentDir(dir, subPath)) { result.push(subPath); } } diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index d39fa85a3..2d97f6b24 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -781,6 +781,11 @@ describe("SubmoduleUtil", function () { indexSubNames: ["q/r", "q/s", "q/t/v", "z"], expected: ["q/r", "q/s", "q/t/v"], }, + "does not overmatch": { + dir: "q", + indexSubNames: ["q/r", "qr", "qr/"], + expected: ["q/r"], + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From 984180c4d8ae19171fc51c5dc1550e6aa145e2d4 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Fri, 9 Nov 2018 17:17:30 -0500 Subject: [PATCH 236/402] improve the check logic as per suggested --- node/lib/util/submodule_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 7630ce55b..a99df925e 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -564,7 +564,7 @@ exports.getSubmodulesInPath = function (dir, indexSubNames) { const isParentDir = (short, long) => { return long.startsWith(short) && ( short[short.length-1] === "/" || - long.replace(short, "")[0] === "/" + long[short.length] === "/" ); }; const result = []; From e7a0928311fde363aae930ba30942e22f02a47dd Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Fri, 28 Dec 2018 19:33:01 -0500 Subject: [PATCH 237/402] Add a 'merge-bare' subcommand that allows for merging under bare repository --- node/lib/cmd/merge_bare.js | 128 ++++++++++ node/lib/cmd/open.js | 6 +- node/lib/cmd/submodule.js | 2 +- node/lib/git-meta.js | 2 + node/lib/util/cherry_pick_util.js | 121 ++++++++-- node/lib/util/commit.js | 2 +- node/lib/util/merge_bare_util.js | 288 +++++++++++++++++++++++ node/lib/util/merge_util.js | 2 +- node/lib/util/open.js | 59 ++++- node/lib/util/reset.js | 2 +- node/lib/util/stash_util.js | 2 +- node/lib/util/submodule_change.js | 25 +- node/lib/util/submodule_config_util.js | 27 ++- node/lib/util/submodule_util.js | 7 +- node/test/util/cherry_pick_util.js | 21 +- node/test/util/merge_bare_util.js | 310 +++++++++++++++++++++++++ node/test/util/merge_util.js | 2 +- node/test/util/open.js | 21 +- node/test/util/reset.js | 4 +- node/test/util/stitch_util.js | 10 +- node/test/util/submodule_change.js | 2 +- node/test/util/submodule_util.js | 44 ++-- 22 files changed, 997 insertions(+), 90 deletions(-) create mode 100644 node/lib/cmd/merge_bare.js create mode 100644 node/lib/util/merge_bare_util.js create mode 100644 node/test/util/merge_bare_util.js diff --git a/node/lib/cmd/merge_bare.js b/node/lib/cmd/merge_bare.js new file mode 100644 index 000000000..24caa0f6a --- /dev/null +++ b/node/lib/cmd/merge_bare.js @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2019, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const co = require("co"); + +/** + * This module contains methods for implementing the `merge` command. + */ + +/** + * help text for the `merge` command + * @property {String} + */ +exports.helpText = `merge two commits with no need for a working directory, \ +print resulting merged commit sha`; + +/** + * description of the `merge` command + * @property {String} + */ +exports.description =` Merge changes from two commits. This command +can work with or without a working tree, resulting a dangling merged commit +that is not pointed by any refs. It will also abort if there are merge conflicts + between two commits.`; + +exports.configureParser = function (parser) { + parser.addArgument(["-m", "--message"], { + type: "string", + help: "commit message", + required: true, + }); + + parser.addArgument(["ourCommit"], { + type: "string", + help: "our side commitish to merge", + nargs: 1, + }); + parser.addArgument(["theirCommit"], { + type: "string", + help: "their side commitish to merge", + nargs: 1, + }); +}; + +/** + * Execute the `merge_bare` command according to the specified `args`. + * + * @async + * @param {Object} args + * @param {String} args.commit + */ +exports.executeableSubcommand = co.wrap(function *(args) { + // TODO: add applicable `git merge` options. + // TODO: For now, we will always create a merge commit. We should be able + // to control whether FF is allowed/required for meta and sub repos. + + const colors = require("colors"); + + const MergeBareUtil = require("../util/merge_bare_util"); + const GitUtil = require("../util/git_util"); + const Hook = require("../util/hook"); + const UserError = require("../util/user_error"); + + const repo = yield GitUtil.getCurrentRepo(); + + let ourCommitName = args.ourCommit; + let theirCommitName = args.theirCommit; + if (null === ourCommitName || null === theirCommitName) { + throw new UserError("Both two merge commits must be given."); + } + const ourCommitish = yield GitUtil.resolveCommitish(repo, ourCommitName); + if (null === ourCommitish) { + throw new UserError(`\ +Could not resolve ${colors.red(ourCommitName)} to a commit.`); + } + + const theirCommitish + = yield GitUtil.resolveCommitish(repo, theirCommitName); + if (null === theirCommitish) { + throw new UserError(`\ +Could not resolve ${colors.red(theirCommitName)} to a commit.`); + } + + const ourCommit = yield repo.getCommit(ourCommitish.id()); + const theirCommit = yield repo.getCommit(theirCommitish.id()); + const result = yield MergeBareUtil.merge(repo, + ourCommit, + theirCommit, + args.message); + if (null !== result.errorMessage) { + throw new UserError(result.errorMessage); + } + + // Run post-merge hook if merge successfully. + // Fixme: --squash is not supported yet, once supported, need to parse 0/1 + // as arg into the post-merge hook, 1 means it is a squash merge, 0 means + // not. + yield Hook.execHook(repo, "post-merge", ["0"]); +}); diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index a06f978b8..7f5ef9427 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -127,7 +127,11 @@ Opening ${colors.blue(name)} on ${colors.green(shas[index])}.`); // to open other (probably unaffected) repositories. try { - yield Open.openOnCommit(fetcher, name, shas[index], templatePath); + yield Open.openOnCommit(fetcher, + name, + shas[index], + templatePath, + false); subsOpenSuccessfully.push(name); } catch (e) { diff --git a/node/lib/cmd/submodule.js b/node/lib/cmd/submodule.js index 375fb04eb..248631df4 100644 --- a/node/lib/cmd/submodule.js +++ b/node/lib/cmd/submodule.js @@ -236,7 +236,7 @@ Could not find ${colors.red(metaCommittish)} in the meta-repo.`); const subName = paths[0]; const opener = new Open.Opener(repo, null); - const subRepo = yield opener.getSubrepo(subName); + const subRepo = yield opener.getSubrepo(subName, false); const metaCommit = yield repo.getCommit(metaAnnotated.id()); // Now that we have an open submodule, we can attempt to resolve diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js index 4fb4bf010..9588910a7 100755 --- a/node/lib/git-meta.js +++ b/node/lib/git-meta.js @@ -47,6 +47,7 @@ const Forward = require("./cmd/forward"); const include = require("./cmd/include"); const listFiles = require("./cmd/list_files"); const merge = require("./cmd/merge"); +const mergeBare = require("./cmd/merge_bare"); const open = require("./cmd/open"); const pull = require("./cmd/pull"); const push = require("./cmd/push"); @@ -135,6 +136,7 @@ const commands = { "include": include, "ls-files": listFiles, "merge": merge, + "merge-bare": mergeBare, "add-submodule": addSubmodule, "open": open, "pull": pull, diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index d1fae8bd4..0f76fd065 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -116,7 +116,7 @@ exports.changeSubmodules = co.wrap(function *(repo, yield rmrf(name); } else if (yield opener.isOpen(name)) { - const subRepo = yield opener.getSubrepo(name); + const subRepo = yield opener.getSubrepo(name, false); yield fetcher.fetchSha(subRepo, name, sub.sha); const commit = yield subRepo.getCommit(sub.sha); yield GitUtil.setHeadHard(subRepo, commit); @@ -137,6 +137,62 @@ exports.changeSubmodules = co.wrap(function *(repo, yield SubmoduleConfigUtil.writeUrls(repo, index, urls); }); +/** + * Similar to exports.changeSubmodules, but operates in bare repo. + * @param {NodeGit.Repository} repo + * @param {Open.Opener} opener + * @param {NodeGit.Index} index + * @param {Object} submodules name to Submodule + */ +exports.changeSubmodulesBare = co.wrap(function *(repo, + opener, + index, + submodules) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(opener, Open.Opener); + assert.instanceOf(index, NodeGit.Index); + assert.isObject(submodules); + if (0 === Object.keys(submodules).count) { + return; // RETURN + } + const urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + const changes = {}; + const fetcher = yield opener.fetcher(); + for (let name in submodules) { + const sub = submodules[name]; + if (null === sub) { + changes[name] = null; + delete urls[name]; + } + else if (!(yield opener.isClose(name))) { + const subRepo = yield opener.getSubrepo(name, true); + yield fetcher.fetchSha(subRepo, name, sub.sha); + const commit = yield subRepo.getCommit(sub.sha); + changes[name] = new TreeUtil.Change( + commit.id(), + NodeGit.TreeEntry.FILEMODE.COMMIT); + } else { + changes[name] = new TreeUtil.Change( + NodeGit.Oid.fromString(sub.sha), + NodeGit.TreeEntry.FILEMODE.COMMIT); + urls[name] = sub.url; + } + } + for (let name in changes) { + const change = changes[name]; + if (change !== null) { + const entry = new NodeGit.IndexEntry(); + entry.path = name; + entry.mode = NodeGit.TreeEntry.FILEMODE.COMMIT; + entry.id = change.id; + entry.flags = entry.flagsExtended = 0; + yield index.add(entry); + } + } + // write urls to the in-memory index + yield SubmoduleConfigUtil.writeUrls(repo, index, urls, true); +}); + /** * Return true if there are URL changes between the specified `commit` and * `baseCommit` in the specified `repo` and false otherwise. A URL change is @@ -234,37 +290,36 @@ const workAroundLibgit2MergeBug = co.wrap(function *(data, repo, name, /** * Determine how to apply the submodule changes introduced in the - * specified `targetCommit` to the commit on the head of the specified `repo` + * specified `srcCommit` to the commit `targetCommit` of the specified repo * as described in the specified in-memory `index`. Return an object * describing what changes to make, including which submodules cannot be * updated at all due to a conflicts, such as a change being introduced to a * submodule that does not exist in HEAD. Throw a `UserError` if non-submodule * changes are detected. The behavior is undefined if there is no merge base - * between HEAD and `commit`. - * - * Note that this method will cause conflicts in `index` to be cleaned up. - * + * between `srcCommit` and the `targetCommit`. * @param {NodeGit.Repository} repo * @param {NodeGit.Index} index + * @param {NodeGit.Commit} srcCommit * @param {NodeGit.Commit} targetCommit * @return {Object} return * @return {Object} return.changes from sub name to `SubmoduleChange` * @return {Object} return.simpleChanges from sub name to `Submodule` or null * @return {Object} return.conflicts from sub name to `Conflict` - */ -exports.computeChanges = co.wrap(function *(repo, index, targetCommit) { + * */ +exports.computeChangesBetweenTwoCommits = co.wrap(function *(repo, + index, + srcCommit, + targetCommit){ assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(index, NodeGit.Index); + assert.instanceOf(srcCommit, NodeGit.Commit); assert.instanceOf(targetCommit, NodeGit.Commit); - - const head = yield repo.getHeadCommit(); const conflicts = {}; const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit( repo, targetCommit); // Group together all parts of conflicted entries. - const conflictEntries = new Map(); // name -> normal, ours, theirs const entries = index.entries(); for (const entry of entries) { @@ -297,7 +352,7 @@ exports.computeChanges = co.wrap(function *(repo, index, targetCommit) { } const libgit2MergeBugData = { - head: head, + head: srcCommit, targetCommit: targetCommit }; for (const [name, entries] of conflictEntries) { @@ -312,7 +367,8 @@ exports.computeChanges = co.wrap(function *(repo, index, targetCommit) { COMMIT === ours.mode && COMMIT === theirs.mode) { changes[name] = new SubmoduleChange(ancestor.id.tostrS(), - theirs.id.tostrS()); + theirs.id.tostrS(), + ours.id.tostrS()); } else if (SubmoduleConfigUtil.modulesFileName !== name) { conflicts[name] = new Conflict(makeConflict(ancestor), makeConflict(ours), @@ -330,8 +386,8 @@ exports.computeChanges = co.wrap(function *(repo, index, targetCommit) { const simpleChanges = {}; const treeId = yield index.writeTreeTo(repo); const tree = yield NodeGit.Tree.lookup(repo, treeId); - const headTree = yield head.getTree(); - const diff = yield NodeGit.Diff.treeToTree(repo, headTree, tree, null); + const srcTree = yield srcCommit.getTree(); + const diff = yield NodeGit.Diff.treeToTree(repo, srcTree, tree, null); const treeChanges = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff, false); for (let name in treeChanges) { @@ -384,7 +440,7 @@ exports.pickSubs = co.wrap(function *(metaRepo, opener, metaIndex, subs) { }; const fetcher = yield opener.fetcher(); const pickSub = co.wrap(function *(name) { - const repo = yield opener.getSubrepo(name); + const repo = yield opener.getSubrepo(name, false); const change = subs[name]; const commitText = "(" + GitUtil.shortSha(change.oldSha) + ".." + GitUtil.shortSha(change.newSha) + "]"; @@ -412,6 +468,39 @@ ${colors.green(commitText)}.`); return result; }); +/** + * Determine how to apply the submodule changes introduced in the + * specified `targetCommit` to the commit on the head of the specified `repo` + * as described in the specified in-memory `index`. Return an object + * describing what changes to make, including which submodules cannot be + * updated at all due to a conflicts, such as a change being introduced to a + * submodule that does not exist in HEAD. Throw a `UserError` if non-submodule + * changes are detected. The behavior is undefined if there is no merge base + * between HEAD and `targetCommit`. + * + * Note that this method will cause conflicts in `index` to be cleaned up. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Index} index + * @param {NodeGit.Commit} targetCommit + * @return {Object} return + * @return {Object} return.changes from sub name to `SubmoduleChange` + * @return {Object} return.simpleChanges from sub name to `Submodule` or null + * @return {Object} return.conflicts from sub name to `Conflict` + */ +exports.computeChanges = co.wrap(function *(repo, index, targetCommit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(index, NodeGit.Index); + assert.instanceOf(targetCommit, NodeGit.Commit); + + const head = yield repo.getHeadCommit(); + const result = yield exports.computeChangesBetweenTwoCommits(repo, + index, + head, + targetCommit); + return result; +}); + /** * Write the specified `conflicts` to the specified `index` in the specified * `repo`. If `conflicts` is non-empty, return a non-empty string desribing diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index dec53fe53..d180fe01a 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -1021,7 +1021,7 @@ exports.getAmendStatus = co.wrap(function *(repo, options) { old = new Submodule(commit.url, commit.sha); } } - const getRepo = () => opener.getSubrepo(name); + const getRepo = () => opener.getSubrepo(name, false); const result = yield exports.getSubmoduleAmendStatus(currentSub, old, diff --git a/node/lib/util/merge_bare_util.js b/node/lib/util/merge_bare_util.js new file mode 100644 index 000000000..c30694338 --- /dev/null +++ b/node/lib/util/merge_bare_util.js @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2019, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const colors = require("colors"); +const NodeGit = require("nodegit"); + +const CherryPickUtil = require("./cherry_pick_util"); +const ConfigUtil = require("./config_util"); +const DoWorkQueue = require("./do_work_queue"); +const GitUtil = require("./git_util"); +const Open = require("./open"); +const UserError = require("./user_error"); + + +/** + * Update meta repo index and point the submodule to a commit sha + * + * @param {NodeGit.Index} index + * @param {String} subName + * @param {String} sha + */ +const addSubmoduleCommit = co.wrap(function *(index, subName, sha) { + assert.instanceOf(index, NodeGit.Index); + assert.isString(subName); + assert.isString(sha); + + const entry = new NodeGit.IndexEntry(); + entry.path = subName; + entry.mode = NodeGit.TreeEntry.FILEMODE.COMMIT; + entry.id = NodeGit.Oid.fromString(sha); + entry.flags = entry.flagsExtended = 0; + yield index.add(entry); +}); + +/** + * Merge in each submodule and update in memory index accordingly. + * + * @param {NodeGit.Repository} repo + * @param {Open.Opener} opener + * @param {NodeGit.Index} mergeIndex + * @param {Object} subs map from sub name to changes + * @param {String} message commit message + * */ +exports.mergeSubmoduleBare = co.wrap(function *(repo, + opener, + mergeIndex, + subs, + message) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(mergeIndex, NodeGit.Index); + assert.isObject(subs); + assert.isString(message); + + const result = { + conflicts: {}, + commits: {}, + }; + const sig = yield ConfigUtil.defaultSignature(repo); + const fetcher = yield opener.fetcher(); + + const mergeSubmodule = co.wrap(function *(name) { + + const subRepo = yield opener.getSubrepo(name, true); + const change = subs[name]; + + const theirSha = change.newSha; + const ourSha = change.ourSha; + yield fetcher.fetchSha(subRepo, name, theirSha); + yield fetcher.fetchSha(subRepo, name, ourSha); + const theirCommit = yield subRepo.getCommit(theirSha); + const ourCommit = yield subRepo.getCommit(ourSha); + + // No change if ours is up-to-date + if (yield NodeGit.Graph.descendantOf(subRepo, ourSha, theirSha)) { + return result; // RETURN + } + + // use their sha if it is a fast forward merge + if (yield NodeGit.Graph.descendantOf(subRepo, theirSha, ourSha)) { + yield addSubmoduleCommit(mergeIndex, name, theirSha); + return result; // RETURN + } + + console.log(`Submodule ${colors.blue(name)}: merging commit ` + + `${colors.green(theirSha)}.`); + + // Start the merge. + let subIndex = yield NodeGit.Merge.commits(subRepo, + ourCommit, + theirCommit, + null); + + // Abort if conflicted. + if (subIndex.hasConflicts()) { + result.conflicts[name] = theirSha; + return; // RETURN + } + + // Otherwise, finish off the merge. + const treeId = yield subIndex.writeTreeTo(subRepo); + const mergeCommit + = yield subRepo.createCommit(null, + sig, + sig, + message, + treeId, + [ourCommit, theirCommit]); + const mergeSha = mergeCommit.tostrS(); + result.commits[name] = mergeSha; + yield addSubmoduleCommit(mergeIndex, name, mergeSha); + }); + yield DoWorkQueue.doInParallel(Object.keys(subs), mergeSubmodule); + return result; +}); + +/** + * Return a formatted string indicating merge will abort for + * irresolvable conflicts. + */ +function formatConflictsMessage(conflicts) { + let errorMessage = "CONFLICT (content): \n"; + const names = Object.keys(conflicts).sort(); + for (let name of names) { + errorMessage += `Conflicting entries for submodule: ` + + `${colors.red(name)}\n`; + } + errorMessage += "Automatic merge failed\n"; + return errorMessage; +} + +/** + * Merge `theirCommit` into `ourCommit` in the specified `repo` with specific + * commitMessage. Return `null` if there are merge conflicts and the conflicts + * cannot be resolved automatically. Return an object describing merge commits + * otherwise. Use our commit as the merged commit if our commit is up to date, + * use theirs if this is a fast forward merge and return a new merge commit + * otherwise. Throw a `UserError` if there are no commits in common between + * `theirCommit` and `ourCommit`. + * + * @async + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} ourCommit + * @param {NodeGit.Commit} theirCommit + * @param {String} commitMessage + * @return {Object} + * @return {String|null} return.metaCommit + * @return {Object} return.submoduleCommits map from submodule to commit + * @return {String|null} return.errorMessage + */ +exports.merge = co.wrap(function *(repo, + ourCommit, + theirCommit, + commitMessage) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(ourCommit, NodeGit.Commit); + assert.instanceOf(theirCommit, NodeGit.Commit); + assert.isString(commitMessage); + + const baseCommit = yield GitUtil.getMergeBase(repo, ourCommit, theirCommit); + + if (null === baseCommit) { + throw new UserError(`No commits in common with `+ + `${colors.red(GitUtil.shortSha(ourCommit.id().tostrS()))} and ` + + `${colors.red(GitUtil.shortSha(theirCommit.id().tostrS()))}`); + } + + yield CherryPickUtil.ensureNoURLChanges(repo, ourCommit, theirCommit); + + const result = { + metaCommit: null, + submoduleCommits: {}, + errorMessage: null, + }; + + const ourCommitSha = ourCommit.id().tostrS(); + const theirCommitSha = theirCommit.id().tostrS(); + if (ourCommitSha === theirCommitSha) { + console.log(`Nothing to do for merging ${colors.green(ourCommitSha)}` + + `into itself.`); + result.metaCommit = ourCommitSha; + return result; + } + + const upToDate = yield NodeGit.Graph.descendantOf(repo, + ourCommitSha, + theirCommitSha); + + if (upToDate) { + console.log(`${colors.green(ourCommitSha)} is up-to-date.`); + result.metaCommit = ourCommitSha; + return result; // RETURN + } + + const canFF = yield NodeGit.Graph.descendantOf(repo, + theirCommitSha, + ourCommitSha); + + if (canFF) { + console.log(`Fast-forward merge: `+ + `${colors.green(theirCommitSha)} is a descendant of `+ + `${colors.green(ourCommitSha)}`); + result.metaCommit = theirCommitSha; + return result; // RETURN + } + + const sig = yield ConfigUtil.defaultSignature(repo); + + const changeIndex + = yield NodeGit.Merge.commits(repo, ourCommit, theirCommit, []); + const changes + = yield CherryPickUtil.computeChangesBetweenTwoCommits(repo, + changeIndex, + ourCommit, + theirCommit); + if (Object.keys(changes.conflicts).length > 0) { + result.errorMessage = formatConflictsMessage(changes.conflicts); + return result; // RETURN + } + const opener = new Open.Opener(repo, null); + + const makeMetaCommit = co.wrap(function *(indexToWrite) { + console.log(`Merging meta-repo commits ` + + `${colors.green(ourCommitSha)} and ` + + `${colors.green(theirCommitSha)}`); + + const id = yield indexToWrite.writeTreeTo(repo); + // And finally, commit it. + const metaCommit = yield repo.createCommit(null, + sig, + sig, + commitMessage, + id, + [ourCommit, theirCommit]); + result.metaCommit = metaCommit.tostrS(); + console.log(`Merge commit created at ` + + `${colors.green(result.metaCommit)}.`); + }); + + + yield CherryPickUtil.changeSubmodulesBare(repo, + opener, + changeIndex, + changes.simpleChanges); + const merges = yield exports.mergeSubmoduleBare(repo, + opener, + changeIndex, + changes.changes, + commitMessage); + if (Object.keys(merges.conflicts).length > 0) { + result.errorMessage = formatConflictsMessage(merges.conflicts); + } else { + yield makeMetaCommit(changeIndex); + result.submoduleCommits = merges.commits; + } + return result; + +}); diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 9bad9d686..81c1de35e 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -160,7 +160,7 @@ const mergeSubmodules = co.wrap(function *(repo, const sig = yield ConfigUtil.defaultSignature(repo); const fetcher = yield opener.fetcher(); const mergeSubmodule = co.wrap(function *(name) { - const subRepo = yield opener.getSubrepo(name); + const subRepo = yield opener.getSubrepo(name, false); const change = subs[name]; const fromSha = change.newSha; diff --git a/node/lib/util/open.js b/node/lib/util/open.js index 14f35f82d..c9b515e5b 100644 --- a/node/lib/util/open.js +++ b/node/lib/util/open.js @@ -60,15 +60,18 @@ const SubmoduleFetcher = require("./submodule_fetcher"); * @param {String} submoduleName * @param {String} submoduleSha * @param {String|null} templatePath + * @param {boolean} bare * @return {NodeGit.Repository} */ exports.openOnCommit = co.wrap(function *(fetcher, submoduleName, submoduleSha, - templatePath) { + templatePath, + bare) { assert.instanceOf(fetcher, SubmoduleFetcher); assert.isString(submoduleName); assert.isString(submoduleSha); + assert.isBoolean(bare); if (null !== templatePath) { assert.isString(templatePath); } @@ -83,12 +86,16 @@ exports.openOnCommit = co.wrap(function *(fetcher, metaRepo, submoduleName, submoduleUrl, - templatePath); + templatePath, + bare); // Turn off GC for the submodule const config = yield submoduleRepo.config(); config.setInt64("gc.auto", 0); + if (bare) { + return submoduleRepo; // RETURN + } // Fetch the needed sha. Close if the fetch fails; otherwise, the // repository ends up in a state where it things the submodule is open, but // it's actually not. @@ -151,10 +158,16 @@ Opener.prototype._initialize = co.wrap(function *() { this.d_commit = yield this.d_repo.getHeadCommit(); } this.d_subRepos = {}; - const openSubsList = yield SubmoduleUtil.listOpenSubmodules(this.d_repo); - this.d_openSubs = new Set(openSubsList); + if (!this.d_repo.isBare()) { + const openSubsList + = yield SubmoduleUtil.listOpenSubmodules(this.d_repo); + this.d_openSubs = new Set(openSubsList); + } + const absorbedSubsList + = yield SubmoduleUtil.listAbsorbedSubmodules(this.d_repo); + this.d_absorbedSubs = new Set(absorbedSubsList); this.d_templatePath = - yield SubmoduleConfigUtil.getTemplatePath(this.d_repo); + yield SubmoduleConfigUtil.getTemplatePath(this.d_repo); this.d_fetcher = new SubmoduleFetcher(this.d_repo, this.d_commit); this.d_initialized = true; this.d_tree = yield this.d_commit.getTree(); @@ -207,6 +220,21 @@ Opener.prototype.isOpen = co.wrap(function *(subName) { return this.d_openSubs.has(subName) || (subName in this.d_subRepos); }); +/** + * Return true if the submodule is neither opened nor half opened. + * + * @param {String} subName + * @return {Boolean} + */ +Opener.prototype.isClose = co.wrap(function *(subName) { + if (!this.d_initialized) { + yield this._initialize(); + } + return !this.d_absorbedSubs.has(subName) && + !this.d_openSubs.has(subName) && + !(subName in this.d_subRepos); +}); + /** * Return the repository for the specified `submoduleName`, opening it if * necessary. @@ -216,10 +244,11 @@ Opener.prototype.isOpen = co.wrap(function *(subName) { * is *unset*; since this operation is expensive, we cannot do it automatically * each time a submodule is opened. * - * @param {String} subName + * @param {String} subName + * @param {boolean} bare * @return {NodeGit.Repository} */ -Opener.prototype.getSubrepo = co.wrap(function *(subName) { +Opener.prototype.getSubrepo = co.wrap(function *(subName, bare) { if (!this.d_initialized) { yield this._initialize(); } @@ -227,7 +256,18 @@ Opener.prototype.getSubrepo = co.wrap(function *(subName) { if (undefined !== subRepo) { return subRepo; // it was found } - if (this.d_openSubs.has(subName)) { + if (bare) { + if (this.d_absorbedSubs.has(subName)) { + subRepo = yield SubmoduleUtil.getBareRepo(this.d_repo, subName); + } else { + subRepo = yield exports.openOnCommit(this.d_fetcher, + subName, + "", + this.d_templatePath, + true); + } + } + else if (this.d_openSubs.has(subName)) { subRepo = yield SubmoduleUtil.getRepo(this.d_repo, subName); } else { @@ -238,7 +278,8 @@ Opening ${colors.blue(subName)} on ${colors.green(sha)}.`); subRepo = yield exports.openOnCommit(this.d_fetcher, subName, sha, - this.d_templatePath); + this.d_templatePath, + false); } this.d_subRepos[subName] = subRepo; return subRepo; diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index f2b621ddb..bdfaf9f6a 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -219,7 +219,7 @@ exports.reset = co.wrap(function *(repo, commit, type) { // Open the submodule and fetch the sha of the commit to which we're // resetting in case we don't have it. - const subRepo = yield opener.getSubrepo(name); + const subRepo = yield opener.getSubrepo(name, false); let subCommitSha; diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 0343674b3..f34f11829 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -317,7 +317,7 @@ exports.apply = co.wrap(function *(repo, id, reinstateIndex) { return; // RETURN } - const subRepo = yield opener.getSubrepo(name); + const subRepo = yield opener.getSubrepo(name, false); // Try to get the comit for the stash; if it's missing, fail. diff --git a/node/lib/util/submodule_change.js b/node/lib/util/submodule_change.js index 0e1a4152a..9c4f574ba 100644 --- a/node/lib/util/submodule_change.js +++ b/node/lib/util/submodule_change.js @@ -45,11 +45,16 @@ class SubmoduleChange { * null `oldSha` implies that the submodule was added, a null `newSha` * implies that it was removed, and if neither is null, the submodule was * changed. + * + * UPDATE: the change used to contain only their sha, because default our + * sha can be obtained from head commit. An explicit field is added for + * cases when our sha is not necessarily the same as the head. * * @param {String | null} oldSha * @param {String | null} newSha + * @param {String | null} ourSha */ - constructor(oldSha, newSha) { + constructor(oldSha, newSha, ourSha) { assert.notEqual(oldSha, newSha); if (null !== oldSha) { assert.isString(oldSha); @@ -57,8 +62,14 @@ class SubmoduleChange { if (null !== newSha) { assert.isString(newSha); } + + if (null !== ourSha) { + assert.isString(ourSha); + } + this.d_oldSha = oldSha; this.d_newSha = newSha; + this.d_ourSha = ourSha; Object.freeze(this); } @@ -82,6 +93,18 @@ class SubmoduleChange { return this.d_newSha; } + /** + * This property represents the value of a sha to which a submodule change + * is applying. If it is null, then the change can only be applied to the + * current head. If it not null, it depends on users to choose to its value + * or head sha to apply submodule changes. + * + * @property {String | null} ourSha + */ + get ourSha() { + return this.d_ourSha; + } + /** * True if the submodule has been deleted in this change. */ diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index fd55d431f..14286e275 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -514,26 +514,31 @@ exports.getTemplatePath = co.wrap(function *(repo) { * @param {String} name * @param {String} url * @param {String|null} templatePath + * @param {Boolean} bare * @return {NodeGit.Repository} */ exports.initSubmoduleAndRepo = co.wrap(function *(repoUrl, metaRepo, name, url, - templatePath) { + templatePath, + bare=false) { if (null !== repoUrl) { assert.isString(repoUrl); } assert.instanceOf(metaRepo, NodeGit.Repository); assert.isString(name); assert.isString(url); + assert.isBoolean(bare); if (null !== templatePath) { assert.isString(templatePath); } // Update the `.git/config` file. - const repoPath = metaRepo.workdir(); + const repoPath = metaRepo.isBare ? + path.dirname(metaRepo.path()) : + metaRepo.workdir(); yield exports.initSubmodule(metaRepo.path(), name, url); // Then, initialize the repository. We pass `initExt` the right set of @@ -544,15 +549,17 @@ exports.initSubmoduleAndRepo = co.wrap(function *(repoUrl, const FLAGS = NodeGit.Repository.INIT_FLAG; // See if modules repo exists. - + let subRepo = null; try { - yield NodeGit.Repository.open(subRepoDir); + subRepo = bare ? + yield NodeGit.Repository.openBare(subRepoDir) : + yield NodeGit.Repository.open(subRepoDir); } catch (e) { // Or, make it if not. - yield NodeGit.Repository.initExt(subRepoDir, { - workdirPath: exports.computeRelativeWorkDir(name), + subRepo = yield NodeGit.Repository.initExt(subRepoDir, { + workdirPath: bare ? null : exports.computeRelativeWorkDir(name), flags: FLAGS.NO_DOTGIT_DIR | FLAGS.MKPATH | FLAGS.RELATIVE_GITLINK | (null === templatePath ? 0 : FLAGS.EXTERNAL_TEMPLATE), @@ -560,6 +567,9 @@ exports.initSubmoduleAndRepo = co.wrap(function *(repoUrl, }); } + if (bare) { + return subRepo; + } // Write out the .git file. Note that `initExt` configured to write a // relative .git directory will not write this file successfully if the @@ -635,7 +645,10 @@ exports.writeUrls = co.wrap(function *(repo, index, urls, cached) { assert.isBoolean(cached); } - const modulesPath = path.join(repo.workdir(), + const repoPath = repo.isBare ? + path.dirname(repo.path()) : + repo.workdir(); + const modulesPath = path.join(repoPath, exports.modulesFileName); const newConf = exports.writeConfigText(urls); if (newConf.length === 0) { diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index a99df925e..33606b090 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -427,7 +427,8 @@ exports.getSubmoduleChangesFromDiff = function (diff, allowMetaChanges) { if (COMMIT === newFile.mode()) { result[path] = new SubmoduleChange( delta.oldFile().id().tostrS(), - newFile.id().tostrS()); + newFile.id().tostrS(), + null); } else if (!allowMetaChanges && path !== modulesFileName) { throw new UserError(`\ Modification to meta-repo file ${colors.red(path)} is not supported.`); @@ -438,7 +439,8 @@ Modification to meta-repo file ${colors.red(path)} is not supported.`); const path = newFile.path(); if (COMMIT === newFile.mode()) { result[path] = new SubmoduleChange(null, - newFile.id().tostrS()); + newFile.id().tostrS(), + null); } else if (!allowMetaChanges && path !== modulesFileName) { throw new UserError(`\ Addition to meta-repo of file ${colors.red(path)} is not supported.`); @@ -449,6 +451,7 @@ Addition to meta-repo of file ${colors.red(path)} is not supported.`); const path = oldFile.path(); if (COMMIT === oldFile.mode()) { result[path] = new SubmoduleChange(oldFile.id().tostrS(), + null, null); } else if (!allowMetaChanges && path !== modulesFileName) { throw new UserError(`\ diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index 524493c4c..14cab55cc 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -252,7 +252,7 @@ describe("computeChangesFromIndex", function () { a=B:Ca-1;Cb-1;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;Ct-2 s=Sa:b;Bt=t;Bmaster=3`, changes: { - "s": new SubmoduleChange("1", "b"), + "s": new SubmoduleChange("1", "b", null), }, }, "addition": { @@ -360,7 +360,8 @@ x=U:C3-2 s=Sa:a;Ct-2 s;Bt=t;Bmaster=3`, for (let name in result.changes) { const change = result.changes[name]; changes[name] = new SubmoduleChange(commitMap[change.oldSha], - commitMap[change.newSha]); + commitMap[change.newSha], + null); } assert.deepEqual(changes, c.changes || {}); @@ -419,7 +420,8 @@ x=S:C2-1 s=Sa:1;C3-2 s=Sa:a;Ct-2 s=Sa:b;Bmaster=3;Bt=t` const change = result.changes.s; const expect = new SubmoduleChange(reverseCommitMap["1"], - reverseCommitMap.b); + reverseCommitMap.b, + reverseCommitMap.a); assert.deepEqual(expect, change); })); }); @@ -437,7 +439,7 @@ describe("pickSubs", function () { "pick a sub": { state: `a=B:Ca-1;Cb-1;Ba=a;Bb=b|x=U:C3-2 s=Sa:a;H=3`, subs: { - "s": new SubmoduleChange("1", "b"), + "s": new SubmoduleChange("1", "b", null), }, expected: `x=E:Os Cbs-a b=b!H=bs;I s=Sa:bs`, }, @@ -447,8 +449,8 @@ a=B:Ca-1;Caa-1;Cb-1;Cc-b;Ba=a;Bb=b;Bc=c;Baa=aa| x=U:C3-2 s=Sa:a,t/u=Sa:a;H=3 `, subs: { - "s": new SubmoduleChange("1", "aa"), - "t/u": new SubmoduleChange("1", "c"), + "s": new SubmoduleChange("1", "aa", null), + "t/u": new SubmoduleChange("1", "c", null), }, expected: ` x=E:Os Caas-a aa=aa!H=aas;Ot/u Cct/u-bt/u c=c!Cbt/u-a b=b!H=ct/u; @@ -457,7 +459,7 @@ x=E:Os Caas-a aa=aa!H=aas;Ot/u Cct/u-bt/u c=c!Cbt/u-a b=b!H=ct/u; "a conflict": { state: `a=B:Ca-1;Cb-1 a=foo;Ba=a;Bb=b|x=U:C3-2 s=Sa:a;H=3`, subs: { - "s": new SubmoduleChange("1", "b"), + "s": new SubmoduleChange("1", "b", null), }, conflicts: { "s": "b", @@ -475,7 +477,7 @@ foo a=B:Ca-1;Cb-1;Cc-b a=foo;Ba=a;Bb=b;Bc=c| x=U:C3-2 s=Sa:a;H=3`, subs: { - "s": new SubmoduleChange("1", "c"), + "s": new SubmoduleChange("1", "c", null), }, conflicts: { "s": "c", @@ -501,7 +503,8 @@ foo Object.keys(c.subs).forEach(name => { const change = c.subs[name]; subs[name] = new SubmoduleChange(reverseMap[change.oldSha], - reverseMap[change.newSha]); + reverseMap[change.newSha], + null); }); const opener = new Open.Opener(repo, null); const result = yield CherryPickUtil.pickSubs(repo, diff --git a/node/test/util/merge_bare_util.js b/node/test/util/merge_bare_util.js new file mode 100644 index 000000000..9443f8eb4 --- /dev/null +++ b/node/test/util/merge_bare_util.js @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2019, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const colors = require("colors"); + +const MergeBareUtil = require("../../lib//util/merge_bare_util"); +const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); + +describe("MergeBareUtil", function () { + describe("merge_with_all_cases", function () { + // Similar to tests of merge, but with no need for a working directory. + const cases = { + "3 way merge in bare": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=B:C2-1 s=Sa:1;C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + toCommit: "3", + }, + "fast forward": { + initial: "a=B|x=S:C2-1 s=Sa:1;Bfoo=2", + fromCommit: "2", + toCommit: "1", + parents: ["1"], + }, + "one merge_xxxx": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + toCommit: "3", + }, + "one merge with ancestor": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C5-4 t=Sa:b;C4-2 s=Sa:b;Bmaster=3;Bfoo=5`, + fromCommit: "5", + toCommit: "3", + }, + "non-ffmerge with trivial ffwd submodule change": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 t=Sa:b;C4-2 s=Sa:b;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + toCommit: "3", + }, + "sub is same": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:b,t=Sa:b;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + toCommit: "3", + }, + "sub is same, closed": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:b,t=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + toCommit: "3", + }, + "sub is behind": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:a;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + toCommit: "3", + }, + "sub is behind, closed": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:a;Bmaster=3;Bfoo=4`, + fromCommit: "4", + toCommit: "3", + }, + "non-ffmerge with ffwd submodule change": { + initial: ` +a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + toCommit: "3", + }, + "non-ffmerge with ffwd submodule change, closed": { + initial: ` +a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4`, + fromCommit: "4", + toCommit: "3", + }, + "non-ffmerge with deeper ffwd submodule change": { + initial: ` +a=Aa:Cb-a;Bb=b;Cc-b;Cd-c;Bd=d| +x=U:C3-2 s=Sa:b;C5-4 s=Sa:d;C4-2 s=Sa:c;Bmaster=3;Bfoo=5`, + fromCommit: "5", + toCommit: "3", + }, + "non-ffmerge with ffwd submodule change on lhs": { + initial: ` +a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| +x=U:C3-2 s=Sa:b;C4-2 q=Sa:a;Bmaster=3;Bfoo=4`, + fromCommit: "4", + toCommit: "3", + }, + "non-ffmerge with non-ffwd submodule change": { + initial: ` +a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4`, + fromCommit: "4", + toCommit: "3", + }, + "non-ffmerge with non-ffwd submodule change, sub already open": { + initial: ` +a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + toCommit: "3", + }, + "submodule commit is up-to-date": { + initial:` +a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:c;C4-2 s=Sa:b,t=Sa:a;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + toCommit: "3", + }, + "submodule commit is up-to-date, was not open": { + initial:` +a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:c;C4-2 s=Sa:b,t=Sa:a;Bmaster=3;Bfoo=4`, + fromCommit: "4", + toCommit: "3", + }, + "submodule commit is same": { + initial: ` +a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:c;C4-2 s=Sa:c,q=Sa:a;Bmaster=3;Bfoo=4`, + fromCommit: "4", + toCommit: "3", + }, + "added in merge_xxxxs": { + initial: `a=B|x=S:C2-1;C3-1 t=Sa:1;Bmaster=2;Bfoo=3`, + fromCommit: "2", + toCommit: "3", + }, + "added on both sides": { + initial: ` +a=B| +x=S:C2-1 s=Sa:1;C3-1 t=Sa:1;Bmaster=2;Bfoo=3`, + fromCommit: "2", + toCommit: "3", + }, + "conflicted add": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=S:C2-1 s=Sa:a;C3-1 s=Sa:b;Bmaster=2;Bfoo=3`, + fromCommit: "3", + toCommit: "2", + errorMessage: `\ +CONFLICT (content): +Conflicting entries for submodule: ${colors.red("s")} +Automatic merge failed +`, + }, + "conflict in submodule": { + initial: ` +a=B:Ca-1 README.md=8;Cb-1 README.md=9;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + toCommit: "3", + errorMessage: `\ +CONFLICT (content): +Conflicting entries for submodule: ${colors.red("s")} +Automatic merge failed +`, + }, + "new commit in sub in target branch but not in HEAD branch": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 t=Sa:1;C4-3 s=Sa:a;C5-3 t=Sa:b;Bmaster=4;Bfoo=5;Os;Ot`, + fromCommit: "5", + toCommit: "4", + }, + "new commit in sub in target branch but not in HEAD branch, closed" + : { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 t=Sa:1;C4-3 s=Sa:a;C5-3 t=Sa:b;Bmaster=4;Bfoo=5`, + fromCommit: "5", + toCommit: "4", + }, + "merge in a branch with a removed sub": { + initial: ` +a=B:Ca-1;Ba=a| +x=U:C3-2 t=Sa:1;C4-2 s;Bmaster=3;Bfoo=4`, + fromCommit: "4", + toCommit: "3", + }, + "merge to a branch with a removed sub": { + initial: ` +a=B:Ca-1;Ba=a| +x=U:C3-2 t=Sa:1;C4-2 s;Bmaster=4;Bfoo=3`, + fromCommit: "3", + toCommit: "4", + }, + "change with multiple merge bases": { + initial: ` +a=B:Ca-1;Ba=a| +x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; + C3-2 s=Sa:a; + C4-2 t=Sa:a; + Cl-3,4 s,t; + Ct-3,4 a=Sa:1,t=Sa:a; + Bmaster=l;Bfoo=t`, + fromCommit: "t", + toCommit: "l", + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + // expect no changes to the repo + const expected = "x=E"; + + const doMerge = co.wrap(function *(repos, maps) { + const upToDate = null === expected; + const x = repos.x; + const reverseCommitMap = maps.reverseCommitMap; + assert.property(reverseCommitMap, c.fromCommit); + const fromSha = reverseCommitMap[c.fromCommit]; + const fromCommit = yield x.getCommit(fromSha); + const toSha = reverseCommitMap[c.toCommit]; + const toCommit = yield x.getCommit(toSha); + + let message = c.message; + if (undefined === message) { + message = "message\n"; + } + const defaultEditor = function () {}; + const editMessage = c.editMessage || defaultEditor; + const result = yield MergeBareUtil.merge(x, + fromCommit, + toCommit, + message, + editMessage, + true); + const errorMessage = c.errorMessage || null; + assert.equal(result.errorMessage, errorMessage); + if (upToDate) { + assert.isNull(result.metaCommit); + return; // RETURN + } + + if (result.metaCommit) { + const parents = c.parents ? + c.parents.map(v => reverseCommitMap[v]) : + [fromSha, toSha]; + const mergedCommit + = yield x.getCommit(result.metaCommit); + const mergeParents + = yield mergedCommit.getParents(null, null); + const mergeParentShas + = new Set(mergeParents.map(c => c.sha())); + const parentsMatch = parents + .map(c => mergeParentShas.has(c)) + .reduce( (acc, curr) => acc && curr, true); + assert.isTrue( + parentsMatch, + "parents (" + mergeParentShas + ") " + + "of created meta commit do not match expected: " + + parents); + } + return {commitMap: {}}; + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + expected || {}, + doMerge, + c.fails); + })); + }); + }); +}); diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index b7e717eb9..de4e0b50a 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -133,7 +133,7 @@ x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, }); }); - describe("merge", function () { + describe("merge_xxx", function () { // Will do merge from repo `x`. A merge commit in the meta-repo will // be named `x`; any merge commits in the sub-repos will be given the // name of the sub-repo in which they are made. TODO: test for changes diff --git a/node/test/util/open.js b/node/test/util/open.js index cb84ecdb8..c88a3d5f9 100644 --- a/node/test/util/open.js +++ b/node/test/util/open.js @@ -83,7 +83,8 @@ describe("openOnCommit", function () { const result = yield Open.openOnCommit(fetcher, c.subName, commit, - null); + null, + false); assert.instanceOf(result, NodeGit.Repository); }); yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, @@ -103,8 +104,8 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos("a=B|x=U:Os"); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - const s1 = yield opener.getSubrepo("s"); - const s2 = yield opener.getSubrepo("s"); + const s1 = yield opener.getSubrepo("s", false); + const s2 = yield opener.getSubrepo("s", false); const base = yield SubmoduleUtil.getRepo(repo, "s"); assert.equal(s1, s2, "not re-opened"); assert.equal(s1.workdir(), base.workdir(), "right path"); @@ -113,8 +114,8 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos("a=B|x=U"); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - const s1 = yield opener.getSubrepo("s"); - const s2 = yield opener.getSubrepo("s"); + const s1 = yield opener.getSubrepo("s", false); + const s2 = yield opener.getSubrepo("s", false); const base = yield SubmoduleUtil.getRepo(repo, "s"); assert.equal(s1, s2, "not re-opened"); assert.equal(s1.workdir(), base.workdir(), "right path"); @@ -132,7 +133,7 @@ describe("openOnCommit", function () { const repo = w.repos.x; const commit = yield repo.getCommit(baseSha); const opener = new Open.Opener(repo, commit); - const s = yield opener.getSubrepo("s"); + const s = yield opener.getSubrepo("s", false); const head = yield s.getHeadCommit(); assert.equal(head.id().tostrS(), subSha); })); @@ -157,7 +158,7 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos(state); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - yield opener.getSubrepo("s"); + yield opener.getSubrepo("s", false); const open = yield opener.getOpenSubs(); assert.deepEqual(Array.from(open), []); })); @@ -182,7 +183,7 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos(state); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - yield opener.getSubrepo("s"); + yield opener.getSubrepo("s", false); const opened = yield opener.getOpenedSubs(); assert.deepEqual(opened, []); })); @@ -191,7 +192,7 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos(state); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - yield opener.getSubrepo("s"); + yield opener.getSubrepo("s", false); const opened = yield opener.getOpenedSubs(); assert.deepEqual(opened, ["s"]); })); @@ -208,7 +209,7 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos(state); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - const s = yield opener.getSubrepo("s"); + const s = yield opener.getSubrepo("s", false); const result = yield opener.isOpen("s"); assert.equal(true, result); const config = yield s.config(); diff --git a/node/test/util/reset.js b/node/test/util/reset.js index 2630a3051..f7df3e2b8 100644 --- a/node/test/util/reset.js +++ b/node/test/util/reset.js @@ -155,7 +155,7 @@ describe("reset", function () { const index = yield repo.index(); const commit = yield repo.getCommit(rev["3"]); const changes = { - s: new SubmoduleChange(rev["1"], null), + s: new SubmoduleChange(rev["1"], null, null), }; yield Reset.resetMetaRepo(repo, index, commit, changes); let exists = true; @@ -175,7 +175,7 @@ describe("reset", function () { const index = yield repo.index(); const commit = yield repo.getCommit(rev["2"]); const changes = { - s: new SubmoduleChange(null, rev["1"]), + s: new SubmoduleChange(null, rev["1"], null), }; yield Reset.resetMetaRepo(repo, index, commit, changes); let exists = true; diff --git a/node/test/util/stitch_util.js b/node/test/util/stitch_util.js index b04b2a72e..07eede973 100644 --- a/node/test/util/stitch_util.js +++ b/node/test/util/stitch_util.js @@ -695,10 +695,10 @@ describe("writeSubmoduleChangeCache", function () { const nextSha = next.id().tostrS(); const changes = {}; changes[headSha] = { - "foo/bar": new SubmoduleChange("1", "2") + "foo/bar": new SubmoduleChange("1", "2", null) }; changes[nextSha] = { - "baz/bam": new SubmoduleChange("3", "4") + "baz/bam": new SubmoduleChange("3", "4", null) }; yield StitchUtil.writeSubmoduleChangeCache(repo, changes); const refName = StitchUtil.changeCacheRef; @@ -724,10 +724,10 @@ describe("readSubmoduleChangeCache", function () { const nextSha = next.id().tostrS(); const changes = {}; changes[headSha] = { - "foo/bar": new SubmoduleChange("1", "2") + "foo/bar": new SubmoduleChange("1", "2", null) }; changes[nextSha] = { - "baz/bam": new SubmoduleChange("3", "4") + "baz/bam": new SubmoduleChange("3", "4", null) }; yield StitchUtil.writeSubmoduleChangeCache(repo, changes); const read = yield StitchUtil.readSubmoduleChangeCache(repo); @@ -1090,7 +1090,7 @@ describe("listSubmoduleChanges", function () { const parent = parents[0]; const firstSha = parent.id().tostrS(); expected[head.id().tostrS()] = { - "s": new SubmoduleChange(null, firstSha), + "s": new SubmoduleChange(null, firstSha, null), }; expected[firstSha] = {}; const result = diff --git a/node/test/util/submodule_change.js b/node/test/util/submodule_change.js index c55cb6e0b..bf4c9c8b3 100644 --- a/node/test/util/submodule_change.js +++ b/node/test/util/submodule_change.js @@ -36,7 +36,7 @@ const SubmoduleChange = require("../../lib/util/submodule_change"); describe("SubmoduleChange", function () { it("breathing", function () { - const change = new SubmoduleChange("old", "new"); + const change = new SubmoduleChange("old", "new", null); assert.equal(change.oldSha, "old"); assert.equal(change.newSha, "new"); }); diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index 2d97f6b24..cceed5d60 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -456,7 +456,7 @@ describe("SubmoduleUtil", function () { state: "S:C2-1 x=Sa:1;H=2", from: "2", result: { - "x": new SubmoduleChange(null, "1"), + "x": new SubmoduleChange(null, "1", null), }, allowMetaChanges: false, }, @@ -464,8 +464,8 @@ describe("SubmoduleUtil", function () { state: "S:C2-1 a=Sa:1,x=Sa:1;H=2", from: "2", result: { - a: new SubmoduleChange(null, "1"), - x: new SubmoduleChange(null, "1"), + a: new SubmoduleChange(null, "1", null), + x: new SubmoduleChange(null, "1", null), }, allowMetaChanges: true, }, @@ -473,7 +473,7 @@ describe("SubmoduleUtil", function () { state: "S:C3-2 a=Sa:2;C2-1 a=Sa:1,x=Sa:1;H=3", from: "3", result: { - a: new SubmoduleChange("1", "2"), + a: new SubmoduleChange("1", "2", null), }, allowMetaChanges: true, }, @@ -487,8 +487,8 @@ describe("SubmoduleUtil", function () { state: "S:C3-2 a=Sa:2,c=Sa:2;C2-1 a=Sa:1,x=Sa:1;H=3", from: "3", result: { - a: new SubmoduleChange("1", "2"), - c: new SubmoduleChange(null, "2"), + a: new SubmoduleChange("1", "2", null), + c: new SubmoduleChange(null, "2", null), }, allowMetaChanges: true, }, @@ -496,7 +496,7 @@ describe("SubmoduleUtil", function () { state: "S:C3-2 a;C2-1 a=Sa:1,x=Sa:1;H=3", from: "3", result: { - a: new SubmoduleChange("1", null), + a: new SubmoduleChange("1", null, null), }, allowMetaChanges: false, }, @@ -504,8 +504,8 @@ describe("SubmoduleUtil", function () { state: "S:C3-2 a,c=Sa:2;C2-1 a=Sa:1,x=Sa:1;H=3", from: "3", result: { - c: new SubmoduleChange(null, "2"), - a: new SubmoduleChange("1", null), + c: new SubmoduleChange(null, "2", null), + a: new SubmoduleChange("1", null, null), }, allowMetaChanges: true, }, @@ -560,7 +560,7 @@ describe("SubmoduleUtil", function () { assert.instanceOf(change, SubmoduleChange); const oldSha = change.oldSha && commitMap[change.oldSha]; const newSha = change.newSha && commitMap[change.newSha]; - changes[name] = new SubmoduleChange(oldSha, newSha); + changes[name] = new SubmoduleChange(oldSha, newSha, null); }); assert.deepEqual(changes, c.result); })); @@ -583,7 +583,7 @@ describe("SubmoduleUtil", function () { state: "S:C2-1 x=Sa:1;H=2", from: "2", result: { - "x": new SubmoduleChange(null, "1"), + "x": new SubmoduleChange(null, "1", null), }, allowMetaChanges: false, }, @@ -591,7 +591,7 @@ describe("SubmoduleUtil", function () { state: "S:C3-2 w=Sa:1;C2-1 x=Sa:1;H=3", from: "3", result: { - "w": new SubmoduleChange(null, "1"), + "w": new SubmoduleChange(null, "1", null), }, allowMetaChanges: false, }, @@ -600,8 +600,8 @@ describe("SubmoduleUtil", function () { from: "3", base: "1", result: { - "x": new SubmoduleChange(null, "1"), - "w": new SubmoduleChange(null, "1"), + "x": new SubmoduleChange(null, "1", null), + "w": new SubmoduleChange(null, "1", null), }, allowMetaChanges: false, }, @@ -652,7 +652,7 @@ describe("SubmoduleUtil", function () { assert.instanceOf(change, SubmoduleChange); const oldSha = change.oldSha && commitMap[change.oldSha]; const newSha = change.newSha && commitMap[change.newSha]; - changes[name] = new SubmoduleChange(oldSha, newSha); + changes[name] = new SubmoduleChange(oldSha, newSha, null); }); assert.deepEqual(changes, c.result); })); @@ -663,15 +663,15 @@ describe("SubmoduleUtil", function () { "one": { state: "S:C2-1 foo=Sa:1;H=2", commit: "2", - expected: { foo: new Submodule("a", "1") }, + expected: { foo: new Submodule("a", "1", null) }, names: null, }, "two": { state: "S:C2-1 foo=Sa:1,bar=Sa:1;H=2", commit: "2", expected: { - foo: new Submodule("a", "1"), - bar: new Submodule("a", "1"), + foo: new Submodule("a", "1", null), + bar: new Submodule("a", "1", null), }, names: null, }, @@ -691,14 +691,14 @@ describe("SubmoduleUtil", function () { state: "S:C2-1 foo=Sa:1,bar=Sa:1;H=2", commit: "2", expected: { - bar: new Submodule("a", "1"), + bar: new Submodule("a", "1", null), }, names: ["bar"], }, "from later commit": { state: "S:C2-1 x=S/a:1;C3-2 x=S/a:2;H=3", commit: "3", - expected: { x: new Submodule("/a", "2") }, + expected: { x: new Submodule("/a", "2", null) }, names: null, }, "none": { @@ -723,7 +723,9 @@ describe("SubmoduleUtil", function () { Object.keys(result).forEach((name) => { const resultSub = result[name]; const commit = written.commitMap[resultSub.sha]; - mappedResult[name] = new Submodule(resultSub.url, commit); + mappedResult[name] = new Submodule(resultSub.url, + commit, + null); }); assert.deepEqual(mappedResult, c.expected); })); From 6f77d4a196b3907ec860563141af0d062a1dcdfd Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Thu, 3 Jan 2019 10:37:39 -0500 Subject: [PATCH 238/402] Address @novalis's comment --- node/lib/util/submodule_change.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/node/lib/util/submodule_change.js b/node/lib/util/submodule_change.js index 9c4f574ba..3a3139756 100644 --- a/node/lib/util/submodule_change.js +++ b/node/lib/util/submodule_change.js @@ -44,11 +44,10 @@ class SubmoduleChange { * values. The behavior is undefined if `oldSha === newSha`. Note that a * null `oldSha` implies that the submodule was added, a null `newSha` * implies that it was removed, and if neither is null, the submodule was - * changed. - * - * UPDATE: the change used to contain only their sha, because default our - * sha can be obtained from head commit. An explicit field is added for - * cases when our sha is not necessarily the same as the head. + * changed. In a 3 way merge, `oldSha` tracks the merge base, `newSha` + * points to their commit and `ourSha` is usually the same as the HEAD. + * In case it is not or the HEAD sha is unavailable, we can explicitly + * point `ourSha` to a perticular target commit. * * @param {String | null} oldSha * @param {String | null} newSha From d6c140b6df36fc30628791f917b512205a210381 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Tue, 8 Jan 2019 14:17:32 -0500 Subject: [PATCH 239/402] Make the simple change logic clearer --- node/lib/util/cherry_pick_util.js | 29 ++++++++++++----------------- node/lib/util/merge_bare_util.js | 1 - node/lib/util/open.js | 10 +++++----- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 0f76fd065..34feb6302 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -140,16 +140,13 @@ exports.changeSubmodules = co.wrap(function *(repo, /** * Similar to exports.changeSubmodules, but operates in bare repo. * @param {NodeGit.Repository} repo - * @param {Open.Opener} opener - * @param {NodeGit.Index} index + * @param {NodeGit.Index} index meta repo's change index * @param {Object} submodules name to Submodule */ exports.changeSubmodulesBare = co.wrap(function *(repo, - opener, index, submodules) { assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(opener, Open.Opener); assert.instanceOf(index, NodeGit.Index); assert.isObject(submodules); if (0 === Object.keys(submodules).count) { @@ -157,26 +154,24 @@ exports.changeSubmodulesBare = co.wrap(function *(repo, } const urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); const changes = {}; - const fetcher = yield opener.fetcher(); for (let name in submodules) { + // Each sub object is either a {Submodule} object or null, it covers + // three types of change: addition, deletions and fast-forwards. + // (see {computeChangesBetweenTwoCommits}) + // In case of deletion, we remove its url from the urls array, update + // the .gitmodule file with `writeUrls` and skip adding the submodule + // to the meta index. + // In other case we bump the submodule sha to `sub.sha` by adding a + // new index entry to the meta index and add `sub.url` for updates. const sub = submodules[name]; if (null === sub) { changes[name] = null; delete urls[name]; + continue; } - else if (!(yield opener.isClose(name))) { - const subRepo = yield opener.getSubrepo(name, true); - yield fetcher.fetchSha(subRepo, name, sub.sha); - const commit = yield subRepo.getCommit(sub.sha); - changes[name] = new TreeUtil.Change( - commit.id(), + changes[name] = new TreeUtil.Change(NodeGit.Oid.fromString(sub.sha), NodeGit.TreeEntry.FILEMODE.COMMIT); - } else { - changes[name] = new TreeUtil.Change( - NodeGit.Oid.fromString(sub.sha), - NodeGit.TreeEntry.FILEMODE.COMMIT); - urls[name] = sub.url; - } + urls[name] = sub.url; } for (let name in changes) { const change = changes[name]; diff --git a/node/lib/util/merge_bare_util.js b/node/lib/util/merge_bare_util.js index c30694338..5a2eb8e54 100644 --- a/node/lib/util/merge_bare_util.js +++ b/node/lib/util/merge_bare_util.js @@ -269,7 +269,6 @@ exports.merge = co.wrap(function *(repo, yield CherryPickUtil.changeSubmodulesBare(repo, - opener, changeIndex, changes.simpleChanges); const merges = yield exports.mergeSubmoduleBare(repo, diff --git a/node/lib/util/open.js b/node/lib/util/open.js index c9b515e5b..63add574c 100644 --- a/node/lib/util/open.js +++ b/node/lib/util/open.js @@ -221,18 +221,18 @@ Opener.prototype.isOpen = co.wrap(function *(subName) { }); /** - * Return true if the submodule is neither opened nor half opened. + * Return true if the submodule is opened nor half opened. * * @param {String} subName * @return {Boolean} */ -Opener.prototype.isClose = co.wrap(function *(subName) { +Opener.prototype.isAtLeastHalfOpen = co.wrap(function *(subName) { if (!this.d_initialized) { yield this._initialize(); } - return !this.d_absorbedSubs.has(subName) && - !this.d_openSubs.has(subName) && - !(subName in this.d_subRepos); + return this.d_absorbedSubs.has(subName) || + this.d_openSubs.has(subName) || + (subName in this.d_subRepos); }); /** From eb6f352c44233c506bcbe245ef5fafbc996e40cf Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Thu, 17 Jan 2019 18:16:12 -0500 Subject: [PATCH 240/402] Address review comments, add --no-ff option, update unit tests accordingly --- node/lib/cmd/merge_bare.js | 18 ++-- node/lib/util/cherry_pick_util.js | 45 +++++---- node/lib/util/merge_bare_util.js | 45 ++++----- node/lib/util/submodule_change.js | 15 ++- node/test/util/merge_bare_util.js | 161 +++++++++++++++++------------- node/test/util/merge_util.js | 2 +- 6 files changed, 156 insertions(+), 130 deletions(-) diff --git a/node/lib/cmd/merge_bare.js b/node/lib/cmd/merge_bare.js index 24caa0f6a..a2fbc75ec 100644 --- a/node/lib/cmd/merge_bare.js +++ b/node/lib/cmd/merge_bare.js @@ -33,7 +33,7 @@ const co = require("co"); /** - * This module contains methods for implementing the `merge` command. + * This module contains methods for implementing the `merge-bare` command. */ /** @@ -69,6 +69,12 @@ exports.configureParser = function (parser) { help: "their side commitish to merge", nargs: 1, }); + + parser.addArgument(["--no-ff"], { + help: "create a merge commit even if fast-forwarding is possible", + action: "storeConst", + constant: true, + }); }; /** @@ -79,9 +85,6 @@ exports.configureParser = function (parser) { * @param {String} args.commit */ exports.executeableSubcommand = co.wrap(function *(args) { - // TODO: add applicable `git merge` options. - // TODO: For now, we will always create a merge commit. We should be able - // to control whether FF is allowed/required for meta and sub repos. const colors = require("colors"); @@ -91,11 +94,13 @@ exports.executeableSubcommand = co.wrap(function *(args) { const UserError = require("../util/user_error"); const repo = yield GitUtil.getCurrentRepo(); - + const mode = args.no_ff ? + MergeBareUtil.MODE.NORMAL : + MergeBareUtil.MODE.FORCE_COMMIT; let ourCommitName = args.ourCommit; let theirCommitName = args.theirCommit; if (null === ourCommitName || null === theirCommitName) { - throw new UserError("Both two merge commits must be given."); + throw new UserError("Two commits must be given."); } const ourCommitish = yield GitUtil.resolveCommitish(repo, ourCommitName); if (null === ourCommitish) { @@ -115,6 +120,7 @@ Could not resolve ${colors.red(theirCommitName)} to a commit.`); const result = yield MergeBareUtil.merge(repo, ourCommit, theirCommit, + mode, args.message); if (null !== result.errorMessage) { throw new UserError(result.errorMessage); diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 34feb6302..a10d86853 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -138,7 +138,32 @@ exports.changeSubmodules = co.wrap(function *(repo, }); /** - * Similar to exports.changeSubmodules, but operates in bare repo. +* Update meta repo index and point the submodule to a commit sha +* +* @param {NodeGit.Index} index +* @param {String} subName +* @param {String} sha +*/ +exports.addSubmoduleCommit = co.wrap(function *(index, subName, sha) { + assert.instanceOf(index, NodeGit.Index); + assert.isString(subName); + assert.isString(sha); + + const entry = new NodeGit.IndexEntry(); + entry.path = subName; + entry.mode = NodeGit.TreeEntry.FILEMODE.COMMIT; + entry.id = NodeGit.Oid.fromString(sha); + entry.flags = entry.flagsExtended = 0; + yield index.add(entry); +}); + +/** + * Similar to exports.changeSubmodules, but it: + * 1. operates in bare repo + * 2. does not make any changes to the working directory + * 3. only deals with simple changes like addition, deletions + * and fast-forwards + * * @param {NodeGit.Repository} repo * @param {NodeGit.Index} index meta repo's change index * @param {Object} submodules name to Submodule @@ -153,7 +178,6 @@ exports.changeSubmodulesBare = co.wrap(function *(repo, return; // RETURN } const urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); - const changes = {}; for (let name in submodules) { // Each sub object is either a {Submodule} object or null, it covers // three types of change: addition, deletions and fast-forwards. @@ -165,25 +189,12 @@ exports.changeSubmodulesBare = co.wrap(function *(repo, // new index entry to the meta index and add `sub.url` for updates. const sub = submodules[name]; if (null === sub) { - changes[name] = null; delete urls[name]; continue; } - changes[name] = new TreeUtil.Change(NodeGit.Oid.fromString(sub.sha), - NodeGit.TreeEntry.FILEMODE.COMMIT); + yield exports.addSubmoduleCommit(index, name, sub.sha); urls[name] = sub.url; } - for (let name in changes) { - const change = changes[name]; - if (change !== null) { - const entry = new NodeGit.IndexEntry(); - entry.path = name; - entry.mode = NodeGit.TreeEntry.FILEMODE.COMMIT; - entry.id = change.id; - entry.flags = entry.flagsExtended = 0; - yield index.add(entry); - } - } // write urls to the in-memory index yield SubmoduleConfigUtil.writeUrls(repo, index, urls, true); }); @@ -304,7 +315,7 @@ const workAroundLibgit2MergeBug = co.wrap(function *(data, repo, name, exports.computeChangesBetweenTwoCommits = co.wrap(function *(repo, index, srcCommit, - targetCommit){ + targetCommit) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(index, NodeGit.Index); assert.instanceOf(srcCommit, NodeGit.Commit); diff --git a/node/lib/util/merge_bare_util.js b/node/lib/util/merge_bare_util.js index 5a2eb8e54..ad5d1f38a 100644 --- a/node/lib/util/merge_bare_util.js +++ b/node/lib/util/merge_bare_util.js @@ -44,24 +44,15 @@ const UserError = require("./user_error"); /** - * Update meta repo index and point the submodule to a commit sha - * - * @param {NodeGit.Index} index - * @param {String} subName - * @param {String} sha + * @enum {MODE} + * Flags to describe what type of merge to do. */ -const addSubmoduleCommit = co.wrap(function *(index, subName, sha) { - assert.instanceOf(index, NodeGit.Index); - assert.isString(subName); - assert.isString(sha); - - const entry = new NodeGit.IndexEntry(); - entry.path = subName; - entry.mode = NodeGit.TreeEntry.FILEMODE.COMMIT; - entry.id = NodeGit.Oid.fromString(sha); - entry.flags = entry.flagsExtended = 0; - yield index.add(entry); -}); +const MODE = { + NORMAL : 0, // will do a fast-forward merge when possible + FORCE_COMMIT: 1, // will generate merge commit even could fast-forward +}; + +exports.MODE = MODE; /** * Merge in each submodule and update in memory index accordingly. @@ -101,14 +92,9 @@ exports.mergeSubmoduleBare = co.wrap(function *(repo, const theirCommit = yield subRepo.getCommit(theirSha); const ourCommit = yield subRepo.getCommit(ourSha); - // No change if ours is up-to-date - if (yield NodeGit.Graph.descendantOf(subRepo, ourSha, theirSha)) { - return result; // RETURN - } - - // use their sha if it is a fast forward merge - if (yield NodeGit.Graph.descendantOf(subRepo, theirSha, ourSha)) { - yield addSubmoduleCommit(mergeIndex, name, theirSha); + // Commit forwards or backwards are handled at meta repo level. + if ((yield NodeGit.Graph.descendantOf(subRepo, ourSha, theirSha)) || + (yield NodeGit.Graph.descendantOf(subRepo, theirSha, ourSha))) { return result; // RETURN } @@ -138,7 +124,7 @@ exports.mergeSubmoduleBare = co.wrap(function *(repo, [ourCommit, theirCommit]); const mergeSha = mergeCommit.tostrS(); result.commits[name] = mergeSha; - yield addSubmoduleCommit(mergeIndex, name, mergeSha); + yield CherryPickUtil.addSubmoduleCommit(mergeIndex, name, mergeSha); }); yield DoWorkQueue.doInParallel(Object.keys(subs), mergeSubmodule); return result; @@ -172,6 +158,7 @@ function formatConflictsMessage(conflicts) { * @param {NodeGit.Repository} repo * @param {NodeGit.Commit} ourCommit * @param {NodeGit.Commit} theirCommit + * @param {MODE} mode * @param {String} commitMessage * @return {Object} * @return {String|null} return.metaCommit @@ -181,16 +168,18 @@ function formatConflictsMessage(conflicts) { exports.merge = co.wrap(function *(repo, ourCommit, theirCommit, + mode, commitMessage) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(ourCommit, NodeGit.Commit); assert.instanceOf(theirCommit, NodeGit.Commit); + assert.isNumber(mode); assert.isString(commitMessage); const baseCommit = yield GitUtil.getMergeBase(repo, ourCommit, theirCommit); if (null === baseCommit) { - throw new UserError(`No commits in common with `+ + throw new UserError(`No commits in common between `+ `${colors.red(GitUtil.shortSha(ourCommit.id().tostrS()))} and ` + `${colors.red(GitUtil.shortSha(theirCommit.id().tostrS()))}`); } @@ -226,7 +215,7 @@ exports.merge = co.wrap(function *(repo, theirCommitSha, ourCommitSha); - if (canFF) { + if (canFF && MODE.FORCE_COMMIT !== mode) { console.log(`Fast-forward merge: `+ `${colors.green(theirCommitSha)} is a descendant of `+ `${colors.green(ourCommitSha)}`); diff --git a/node/lib/util/submodule_change.js b/node/lib/util/submodule_change.js index 3a3139756..5790d577e 100644 --- a/node/lib/util/submodule_change.js +++ b/node/lib/util/submodule_change.js @@ -35,7 +35,8 @@ const assert = require("chai").assert; /** * @class SubmoduleChanges.Change * - * This class represents a sha change to a submodule. + * This class describes the changes from `oldSha` to `newSha` as well as + * the destination that the changes will be appliedd: `ourSha` */ class SubmoduleChange { @@ -44,14 +45,12 @@ class SubmoduleChange { * values. The behavior is undefined if `oldSha === newSha`. Note that a * null `oldSha` implies that the submodule was added, a null `newSha` * implies that it was removed, and if neither is null, the submodule was - * changed. In a 3 way merge, `oldSha` tracks the merge base, `newSha` - * points to their commit and `ourSha` is usually the same as the HEAD. - * In case it is not or the HEAD sha is unavailable, we can explicitly - * point `ourSha` to a perticular target commit. + * changed. In a 3 way merge, `oldSha` is the merge base, `newSha` is the + * right side of the merge and `ourSha` is the left side. * - * @param {String | null} oldSha - * @param {String | null} newSha - * @param {String | null} ourSha + * @param {String | null} oldSha sha from which changes are computed + * @param {String | null} newSha sha to which changes are computed + * @param {String | null} ourSha sha against which changes will be applied */ constructor(oldSha, newSha, ourSha) { assert.notEqual(oldSha, newSha); diff --git a/node/test/util/merge_bare_util.js b/node/test/util/merge_bare_util.js index 9443f8eb4..0b1f82d17 100644 --- a/node/test/util/merge_bare_util.js +++ b/node/test/util/merge_bare_util.js @@ -40,150 +40,173 @@ const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); describe("MergeBareUtil", function () { describe("merge_with_all_cases", function () { // Similar to tests of merge, but with no need for a working directory. + const MODE = MergeBareUtil.MODE; const cases = { "3 way merge in bare": { initial: ` a=B:Ca-1;Cb-1;Ba=a;Bb=b| x=B:C2-1 s=Sa:1;C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, - "fast forward": { + "fast forward in normal mode": { initial: "a=B|x=S:C2-1 s=Sa:1;Bfoo=2", - fromCommit: "2", - toCommit: "1", + theirCommit: "2", + ourCommit: "1", + mode: MODE.NORMAL, parents: ["1"], }, - "one merge_xxxx": { + "fast forward in no-ff mode": { + initial: "a=B|x=S:C2-1 s=Sa:1;Bfoo=2", + theirCommit: "2", + ourCommit: "1", + parents: ["1", "2"], + }, + "one merge": { initial: ` a=B:Ca-1;Cb-1;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "one merge with ancestor": { initial: ` a=B:Ca-1;Cb-1;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;C5-4 t=Sa:b;C4-2 s=Sa:b;Bmaster=3;Bfoo=5`, - fromCommit: "5", - toCommit: "3", + theirCommit: "5", + ourCommit: "3", }, "non-ffmerge with trivial ffwd submodule change": { initial: ` a=Aa:Cb-a;Bb=b| x=U:C3-2 t=Sa:b;C4-2 s=Sa:b;Bmaster=3;Bfoo=4;Os`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "sub is same": { initial: ` a=Aa:Cb-a;Bb=b| x=U:C3-2 s=Sa:b;C4-2 s=Sa:b,t=Sa:b;Bmaster=3;Bfoo=4;Os`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "sub is same, closed": { initial: ` a=Aa:Cb-a;Bb=b| x=U:C3-2 s=Sa:b;C4-2 s=Sa:b,t=Sa:b;Bmaster=3;Bfoo=4`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "sub is behind": { initial: ` a=Aa:Cb-a;Bb=b| x=U:C3-2 s=Sa:b;C4-2 s=Sa:a;Bmaster=3;Bfoo=4;Os`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "sub is behind, closed": { initial: ` a=Aa:Cb-a;Bb=b| x=U:C3-2 s=Sa:b;C4-2 s=Sa:a;Bmaster=3;Bfoo=4`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "non-ffmerge with ffwd submodule change": { initial: ` a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "non-ffmerge with ffwd submodule change, closed": { initial: ` a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "non-ffmerge with deeper ffwd submodule change": { initial: ` a=Aa:Cb-a;Bb=b;Cc-b;Cd-c;Bd=d| x=U:C3-2 s=Sa:b;C5-4 s=Sa:d;C4-2 s=Sa:c;Bmaster=3;Bfoo=5`, - fromCommit: "5", - toCommit: "3", + theirCommit: "5", + ourCommit: "3", }, "non-ffmerge with ffwd submodule change on lhs": { initial: ` a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| x=U:C3-2 s=Sa:b;C4-2 q=Sa:a;Bmaster=3;Bfoo=4`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "non-ffmerge with non-ffwd submodule change": { initial: ` a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c| x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "non-ffmerge with non-ffwd submodule change, sub already open": { initial: ` a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c| x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "submodule commit is up-to-date": { initial:` a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| x=U:C3-2 s=Sa:c;C4-2 s=Sa:b,t=Sa:a;Bmaster=3;Bfoo=4;Os`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "submodule commit is up-to-date, was not open": { initial:` a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| x=U:C3-2 s=Sa:c;C4-2 s=Sa:b,t=Sa:a;Bmaster=3;Bfoo=4`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "submodule commit is same": { initial: ` a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| x=U:C3-2 s=Sa:c;C4-2 s=Sa:c,q=Sa:a;Bmaster=3;Bfoo=4`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", + }, + "submodule commit backwards": { + initial:` +a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:c;C4-2 s=Sa:b;Bmaster=3;Bfoo=4;Os`, + theirCommit: "4", + ourCommit: "3", }, - "added in merge_xxxxs": { + "submodule commit forwards": { + initial:` +a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, + theirCommit: "4", + ourCommit: "3", + }, + + "added in merge": { initial: `a=B|x=S:C2-1;C3-1 t=Sa:1;Bmaster=2;Bfoo=3`, - fromCommit: "2", - toCommit: "3", + theirCommit: "3", + ourCommit: "2", }, "added on both sides": { initial: ` a=B| x=S:C2-1 s=Sa:1;C3-1 t=Sa:1;Bmaster=2;Bfoo=3`, - fromCommit: "2", - toCommit: "3", + theirCommit: "2", + ourCommit: "3", }, "conflicted add": { initial: ` a=B:Ca-1;Cb-1;Ba=a;Bb=b| x=S:C2-1 s=Sa:a;C3-1 s=Sa:b;Bmaster=2;Bfoo=3`, - fromCommit: "3", - toCommit: "2", + theirCommit: "3", + ourCommit: "2", errorMessage: `\ CONFLICT (content): Conflicting entries for submodule: ${colors.red("s")} @@ -194,8 +217,8 @@ Automatic merge failed initial: ` a=B:Ca-1 README.md=8;Cb-1 README.md=9;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", errorMessage: `\ CONFLICT (content): Conflicting entries for submodule: ${colors.red("s")} @@ -206,30 +229,30 @@ Automatic merge failed initial: ` a=B:Ca-1;Cb-1;Ba=a;Bb=b| x=U:C3-2 t=Sa:1;C4-3 s=Sa:a;C5-3 t=Sa:b;Bmaster=4;Bfoo=5;Os;Ot`, - fromCommit: "5", - toCommit: "4", + theirCommit: "5", + ourCommit: "4", }, "new commit in sub in target branch but not in HEAD branch, closed" : { initial: ` a=B:Ca-1;Cb-1;Ba=a;Bb=b| x=U:C3-2 t=Sa:1;C4-3 s=Sa:a;C5-3 t=Sa:b;Bmaster=4;Bfoo=5`, - fromCommit: "5", - toCommit: "4", + theirCommit: "5", + ourCommit: "4", }, "merge in a branch with a removed sub": { initial: ` a=B:Ca-1;Ba=a| x=U:C3-2 t=Sa:1;C4-2 s;Bmaster=3;Bfoo=4`, - fromCommit: "4", - toCommit: "3", + theirCommit: "4", + ourCommit: "3", }, "merge to a branch with a removed sub": { initial: ` a=B:Ca-1;Ba=a| x=U:C3-2 t=Sa:1;C4-2 s;Bmaster=4;Bfoo=3`, - fromCommit: "3", - toCommit: "4", + theirCommit: "3", + ourCommit: "4", }, "change with multiple merge bases": { initial: ` @@ -240,8 +263,8 @@ x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; Cl-3,4 s,t; Ct-3,4 a=Sa:1,t=Sa:a; Bmaster=l;Bfoo=t`, - fromCommit: "t", - toCommit: "l", + theirCommit: "t", + ourCommit: "l", }, }; Object.keys(cases).forEach(caseName => { @@ -254,24 +277,22 @@ x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; const upToDate = null === expected; const x = repos.x; const reverseCommitMap = maps.reverseCommitMap; - assert.property(reverseCommitMap, c.fromCommit); - const fromSha = reverseCommitMap[c.fromCommit]; - const fromCommit = yield x.getCommit(fromSha); - const toSha = reverseCommitMap[c.toCommit]; - const toCommit = yield x.getCommit(toSha); + assert.property(reverseCommitMap, c.theirCommit); + const theirSha = reverseCommitMap[c.theirCommit]; + const theirCommit = yield x.getCommit(theirSha); + const ourSha = reverseCommitMap[c.ourCommit]; + const ourCommit = yield x.getCommit(ourSha); let message = c.message; if (undefined === message) { message = "message\n"; } - const defaultEditor = function () {}; - const editMessage = c.editMessage || defaultEditor; + const mode = !("mode" in c) ? MODE.FORCE_COMMIT : c.mode; const result = yield MergeBareUtil.merge(x, - fromCommit, - toCommit, - message, - editMessage, - true); + ourCommit, + theirCommit, + mode, + message); const errorMessage = c.errorMessage || null; assert.equal(result.errorMessage, errorMessage); if (upToDate) { @@ -282,7 +303,7 @@ x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; if (result.metaCommit) { const parents = c.parents ? c.parents.map(v => reverseCommitMap[v]) : - [fromSha, toSha]; + [theirSha, ourSha]; const mergedCommit = yield x.getCommit(result.metaCommit); const mergeParents diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index de4e0b50a..b7e717eb9 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -133,7 +133,7 @@ x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, }); }); - describe("merge_xxx", function () { + describe("merge", function () { // Will do merge from repo `x`. A merge commit in the meta-repo will // be named `x`; any merge commits in the sub-repos will be given the // name of the sub-repo in which they are made. TODO: test for changes From a745816076a95fcf136a9d55e9f09806638e64a0 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Mon, 4 Feb 2019 21:31:18 -0500 Subject: [PATCH 241/402] Allow normal merge to operates on bare and half opened repos --- node/lib/cmd/merge.js | 20 +- node/lib/cmd/merge_bare.js | 23 +- node/lib/cmd/submodule.js | 3 +- node/lib/util/add_submodule.js | 3 +- node/lib/util/cherry_pick_util.js | 7 +- node/lib/util/commit.js | 4 +- node/lib/util/merge_bare_util.js | 276 ------- node/lib/util/merge_common.js | 325 ++++++++ node/lib/util/merge_util.js | 716 +++++++++++------- node/lib/util/open.js | 156 +++- node/lib/util/reset.js | 4 +- node/lib/util/stash_util.js | 4 +- node/lib/util/status_util.js | 27 +- node/lib/util/submodule_config_util.js | 2 +- node/lib/util/submodule_util.js | 8 +- node/lib/util/write_repo_ast_util.js | 3 +- .../{merge_bare_util.js => merge_bare.js} | 22 +- node/test/util/merge_full_open.js | 427 +++++++++++ node/test/util/merge_util.js | 27 +- node/test/util/open.js | 19 +- node/test/util/read_repo_ast_util.js | 6 +- node/test/util/submodule_config_util.js | 9 +- 22 files changed, 1466 insertions(+), 625 deletions(-) delete mode 100644 node/lib/util/merge_bare_util.js create mode 100644 node/lib/util/merge_common.js rename node/test/util/{merge_bare_util.js => merge_bare.js} (93%) create mode 100644 node/test/util/merge_full_open.js diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index 3ac14aa1b..34b1e19f7 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -107,12 +107,14 @@ exports.executeableSubcommand = co.wrap(function *(args) { const colors = require("colors"); - const MergeUtil = require("../util/merge_util"); - const GitUtil = require("../util/git_util"); - const Hook = require("../util/hook"); - const UserError = require("../util/user_error"); + const MergeUtil = require("../util/merge_util"); + const MergeCommon = require("../util/merge_common"); + const GitUtil = require("../util/git_util"); + const Hook = require("../util/hook"); + const Open = require("../util/open"); + const UserError = require("../util/user_error"); - const MODE = MergeUtil.MODE; + const MODE = MergeCommon.MODE; let mode = MODE.NORMAL; if (args.ff + args.continue + args.abort + args.no_ff + args.ff_only > 1) { @@ -167,8 +169,12 @@ Merge of '${commitName}' return GitUtil.editMessage(repo, message); }; const commit = yield repo.getCommit(commitish.id()); - const result = - yield MergeUtil.merge(repo, commit, mode, args.message, editMessage); + const result = yield MergeUtil.merge(repo, + commit, + mode, + Open.SUB_OPEN_OPTION.ALLOW_BARE, + args.message, + editMessage); if (null !== result.errorMessage) { throw new UserError(result.errorMessage); } diff --git a/node/lib/cmd/merge_bare.js b/node/lib/cmd/merge_bare.js index a2fbc75ec..ac4ac20d8 100644 --- a/node/lib/cmd/merge_bare.js +++ b/node/lib/cmd/merge_bare.js @@ -88,17 +88,19 @@ exports.executeableSubcommand = co.wrap(function *(args) { const colors = require("colors"); - const MergeBareUtil = require("../util/merge_bare_util"); + const MergeUtil = require("../util/merge_util"); + const MergeCommon = require("../util/merge_common"); const GitUtil = require("../util/git_util"); const Hook = require("../util/hook"); + const Open = require("../util/open"); const UserError = require("../util/user_error"); const repo = yield GitUtil.getCurrentRepo(); const mode = args.no_ff ? - MergeBareUtil.MODE.NORMAL : - MergeBareUtil.MODE.FORCE_COMMIT; - let ourCommitName = args.ourCommit; - let theirCommitName = args.theirCommit; + MergeCommon.MODE.NORMAL : + MergeCommon.MODE.FORCE_COMMIT; + let ourCommitName = args.ourCommit[0]; + let theirCommitName = args.theirCommit[0]; if (null === ourCommitName || null === theirCommitName) { throw new UserError("Two commits must be given."); } @@ -117,11 +119,12 @@ Could not resolve ${colors.red(theirCommitName)} to a commit.`); const ourCommit = yield repo.getCommit(ourCommitish.id()); const theirCommit = yield repo.getCommit(theirCommitish.id()); - const result = yield MergeBareUtil.merge(repo, - ourCommit, - theirCommit, - mode, - args.message); + const result = yield MergeUtil.merge(repo, + ourCommit, + theirCommit, + mode, + Open.SUB_OPEN_OPTION.FORCE_BARE, + args.message); if (null !== result.errorMessage) { throw new UserError(result.errorMessage); } diff --git a/node/lib/cmd/submodule.js b/node/lib/cmd/submodule.js index 248631df4..dc583d45f 100644 --- a/node/lib/cmd/submodule.js +++ b/node/lib/cmd/submodule.js @@ -236,7 +236,8 @@ Could not find ${colors.red(metaCommittish)} in the meta-repo.`); const subName = paths[0]; const opener = new Open.Opener(repo, null); - const subRepo = yield opener.getSubrepo(subName, false); + const subRepo = yield opener.getSubrepo(subName, + Open.SUB_OPEN_OPTION.FORCE_OPEN); const metaCommit = yield repo.getCommit(metaAnnotated.id()); // Now that we have an open submodule, we can attempt to resolve diff --git a/node/lib/util/add_submodule.js b/node/lib/util/add_submodule.js index 8377c7c04..da44be91a 100644 --- a/node/lib/util/add_submodule.js +++ b/node/lib/util/add_submodule.js @@ -75,7 +75,8 @@ exports.addSubmodule = co.wrap(function *(repo, url, filename, importArg) { repo, filename, url, - templatePath); + templatePath, + false); if (null === importArg) { return subRepo; // RETURN } diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index a10d86853..c7a64c216 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -116,7 +116,9 @@ exports.changeSubmodules = co.wrap(function *(repo, yield rmrf(name); } else if (yield opener.isOpen(name)) { - const subRepo = yield opener.getSubrepo(name, false); + const subRepo = + yield opener.getSubrepo(name, + Open.SUB_OPEN_OPTION.FORCE_OPEN); yield fetcher.fetchSha(subRepo, name, sub.sha); const commit = yield subRepo.getCommit(sub.sha); yield GitUtil.setHeadHard(subRepo, commit); @@ -446,7 +448,8 @@ exports.pickSubs = co.wrap(function *(metaRepo, opener, metaIndex, subs) { }; const fetcher = yield opener.fetcher(); const pickSub = co.wrap(function *(name) { - const repo = yield opener.getSubrepo(name, false); + const repo = yield opener.getSubrepo(name, + Open.SUB_OPEN_OPTION.FORCE_OPEN); const change = subs[name]; const commitText = "(" + GitUtil.shortSha(change.oldSha) + ".." + GitUtil.shortSha(change.newSha) + "]"; diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index d180fe01a..a37e7e1b2 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -1021,7 +1021,9 @@ exports.getAmendStatus = co.wrap(function *(repo, options) { old = new Submodule(commit.url, commit.sha); } } - const getRepo = () => opener.getSubrepo(name, false); + const getRepo = + () => opener.getSubrepo(name, + Open.SUB_OPEN_OPTION.FORCE_OPEN); const result = yield exports.getSubmoduleAmendStatus(currentSub, old, diff --git a/node/lib/util/merge_bare_util.js b/node/lib/util/merge_bare_util.js deleted file mode 100644 index ad5d1f38a..000000000 --- a/node/lib/util/merge_bare_util.js +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (c) 2019, Two Sigma Open Source - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of git-meta nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -"use strict"; - -const assert = require("chai").assert; -const co = require("co"); -const colors = require("colors"); -const NodeGit = require("nodegit"); - -const CherryPickUtil = require("./cherry_pick_util"); -const ConfigUtil = require("./config_util"); -const DoWorkQueue = require("./do_work_queue"); -const GitUtil = require("./git_util"); -const Open = require("./open"); -const UserError = require("./user_error"); - - -/** - * @enum {MODE} - * Flags to describe what type of merge to do. - */ -const MODE = { - NORMAL : 0, // will do a fast-forward merge when possible - FORCE_COMMIT: 1, // will generate merge commit even could fast-forward -}; - -exports.MODE = MODE; - -/** - * Merge in each submodule and update in memory index accordingly. - * - * @param {NodeGit.Repository} repo - * @param {Open.Opener} opener - * @param {NodeGit.Index} mergeIndex - * @param {Object} subs map from sub name to changes - * @param {String} message commit message - * */ -exports.mergeSubmoduleBare = co.wrap(function *(repo, - opener, - mergeIndex, - subs, - message) { - assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(mergeIndex, NodeGit.Index); - assert.isObject(subs); - assert.isString(message); - - const result = { - conflicts: {}, - commits: {}, - }; - const sig = yield ConfigUtil.defaultSignature(repo); - const fetcher = yield opener.fetcher(); - - const mergeSubmodule = co.wrap(function *(name) { - - const subRepo = yield opener.getSubrepo(name, true); - const change = subs[name]; - - const theirSha = change.newSha; - const ourSha = change.ourSha; - yield fetcher.fetchSha(subRepo, name, theirSha); - yield fetcher.fetchSha(subRepo, name, ourSha); - const theirCommit = yield subRepo.getCommit(theirSha); - const ourCommit = yield subRepo.getCommit(ourSha); - - // Commit forwards or backwards are handled at meta repo level. - if ((yield NodeGit.Graph.descendantOf(subRepo, ourSha, theirSha)) || - (yield NodeGit.Graph.descendantOf(subRepo, theirSha, ourSha))) { - return result; // RETURN - } - - console.log(`Submodule ${colors.blue(name)}: merging commit ` + - `${colors.green(theirSha)}.`); - - // Start the merge. - let subIndex = yield NodeGit.Merge.commits(subRepo, - ourCommit, - theirCommit, - null); - - // Abort if conflicted. - if (subIndex.hasConflicts()) { - result.conflicts[name] = theirSha; - return; // RETURN - } - - // Otherwise, finish off the merge. - const treeId = yield subIndex.writeTreeTo(subRepo); - const mergeCommit - = yield subRepo.createCommit(null, - sig, - sig, - message, - treeId, - [ourCommit, theirCommit]); - const mergeSha = mergeCommit.tostrS(); - result.commits[name] = mergeSha; - yield CherryPickUtil.addSubmoduleCommit(mergeIndex, name, mergeSha); - }); - yield DoWorkQueue.doInParallel(Object.keys(subs), mergeSubmodule); - return result; -}); - -/** - * Return a formatted string indicating merge will abort for - * irresolvable conflicts. - */ -function formatConflictsMessage(conflicts) { - let errorMessage = "CONFLICT (content): \n"; - const names = Object.keys(conflicts).sort(); - for (let name of names) { - errorMessage += `Conflicting entries for submodule: ` + - `${colors.red(name)}\n`; - } - errorMessage += "Automatic merge failed\n"; - return errorMessage; -} - -/** - * Merge `theirCommit` into `ourCommit` in the specified `repo` with specific - * commitMessage. Return `null` if there are merge conflicts and the conflicts - * cannot be resolved automatically. Return an object describing merge commits - * otherwise. Use our commit as the merged commit if our commit is up to date, - * use theirs if this is a fast forward merge and return a new merge commit - * otherwise. Throw a `UserError` if there are no commits in common between - * `theirCommit` and `ourCommit`. - * - * @async - * @param {NodeGit.Repository} repo - * @param {NodeGit.Commit} ourCommit - * @param {NodeGit.Commit} theirCommit - * @param {MODE} mode - * @param {String} commitMessage - * @return {Object} - * @return {String|null} return.metaCommit - * @return {Object} return.submoduleCommits map from submodule to commit - * @return {String|null} return.errorMessage - */ -exports.merge = co.wrap(function *(repo, - ourCommit, - theirCommit, - mode, - commitMessage) { - assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(ourCommit, NodeGit.Commit); - assert.instanceOf(theirCommit, NodeGit.Commit); - assert.isNumber(mode); - assert.isString(commitMessage); - - const baseCommit = yield GitUtil.getMergeBase(repo, ourCommit, theirCommit); - - if (null === baseCommit) { - throw new UserError(`No commits in common between `+ - `${colors.red(GitUtil.shortSha(ourCommit.id().tostrS()))} and ` + - `${colors.red(GitUtil.shortSha(theirCommit.id().tostrS()))}`); - } - - yield CherryPickUtil.ensureNoURLChanges(repo, ourCommit, theirCommit); - - const result = { - metaCommit: null, - submoduleCommits: {}, - errorMessage: null, - }; - - const ourCommitSha = ourCommit.id().tostrS(); - const theirCommitSha = theirCommit.id().tostrS(); - if (ourCommitSha === theirCommitSha) { - console.log(`Nothing to do for merging ${colors.green(ourCommitSha)}` + - `into itself.`); - result.metaCommit = ourCommitSha; - return result; - } - - const upToDate = yield NodeGit.Graph.descendantOf(repo, - ourCommitSha, - theirCommitSha); - - if (upToDate) { - console.log(`${colors.green(ourCommitSha)} is up-to-date.`); - result.metaCommit = ourCommitSha; - return result; // RETURN - } - - const canFF = yield NodeGit.Graph.descendantOf(repo, - theirCommitSha, - ourCommitSha); - - if (canFF && MODE.FORCE_COMMIT !== mode) { - console.log(`Fast-forward merge: `+ - `${colors.green(theirCommitSha)} is a descendant of `+ - `${colors.green(ourCommitSha)}`); - result.metaCommit = theirCommitSha; - return result; // RETURN - } - - const sig = yield ConfigUtil.defaultSignature(repo); - - const changeIndex - = yield NodeGit.Merge.commits(repo, ourCommit, theirCommit, []); - const changes - = yield CherryPickUtil.computeChangesBetweenTwoCommits(repo, - changeIndex, - ourCommit, - theirCommit); - if (Object.keys(changes.conflicts).length > 0) { - result.errorMessage = formatConflictsMessage(changes.conflicts); - return result; // RETURN - } - const opener = new Open.Opener(repo, null); - - const makeMetaCommit = co.wrap(function *(indexToWrite) { - console.log(`Merging meta-repo commits ` + - `${colors.green(ourCommitSha)} and ` + - `${colors.green(theirCommitSha)}`); - - const id = yield indexToWrite.writeTreeTo(repo); - // And finally, commit it. - const metaCommit = yield repo.createCommit(null, - sig, - sig, - commitMessage, - id, - [ourCommit, theirCommit]); - result.metaCommit = metaCommit.tostrS(); - console.log(`Merge commit created at ` + - `${colors.green(result.metaCommit)}.`); - }); - - - yield CherryPickUtil.changeSubmodulesBare(repo, - changeIndex, - changes.simpleChanges); - const merges = yield exports.mergeSubmoduleBare(repo, - opener, - changeIndex, - changes.changes, - commitMessage); - if (Object.keys(merges.conflicts).length > 0) { - result.errorMessage = formatConflictsMessage(merges.conflicts); - } else { - yield makeMetaCommit(changeIndex); - result.submoduleCommits = merges.commits; - } - return result; - -}); diff --git a/node/lib/util/merge_common.js b/node/lib/util/merge_common.js new file mode 100644 index 000000000..520e68d2d --- /dev/null +++ b/node/lib/util/merge_common.js @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2019, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const CherryPickUtil = require("./cherry_pick_util"); +const co = require("co"); +const ConfigUtil = require("./config_util"); +const GitUtil = require("./git_util"); +const NodeGit = require("nodegit"); +const Open = require("./open"); +const UserError = require("./user_error"); + +/** + * @enum {MODE} + * Flags to describe what type of merge to do. + */ +const MODE = { + NORMAL : 0, // will do a fast-forward merge when possible + FF_ONLY : 1, // will fail unless fast-forward merge is possible + FORCE_COMMIT: 2, // will generate merge commit even could fast-forward +}; + +exports.MODE = MODE; + +/** + * @class MergeContext + * A class that manages the necessary objects for merging. + */ +class MergeContext { + /** + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit|null} ourCommit + * @param {NodeGit.Commit} theirCommit + * @param {MergeCommon.MODE} mode + * @param {Open.SUB_OPEN_OPTION} openOption + * @param {String|null} commitMessage + * @param {() -> Promise(String)} editMessage + */ + constructor(metaRepo, + ourCommit, + theirCommit, + mode, + openOption, + commitMessage, + editMessage) { + assert.instanceOf(metaRepo, NodeGit.Repository); + if (null !== ourCommit) { + assert.instanceOf(ourCommit, NodeGit.Commit); + } + assert.instanceOf(theirCommit, NodeGit.Commit); + assert.isNumber(mode); + assert.isNumber(openOption); + if (null !== commitMessage) { + assert.isString(commitMessage); + } + assert.isFunction(editMessage); + this.d_metaRepo = metaRepo; + this.d_ourCommit = ourCommit; + this.d_theirCommit = theirCommit; + this.d_mode = mode; + this.d_openOption = openOption; + this.d_commitMessage = commitMessage; + this.d_editMessage = editMessage; + this.d_opener = new Open.Opener(metaRepo, null); + this.d_changeIndex = null; + this.d_changes = null; + this.d_conflictsMessage = ""; + } + + /** + * @property {Boolean} forceBare if working directory is disabled + */ + get forceBare() { + return Open.SUB_OPEN_OPTION.FORCE_BARE === this.d_openOption; + } + + /** + * @property {NodeGit.Repository} + */ + get metaRepo() { + return this.d_metaRepo; + } + + /** + * @property {Opener} + */ + get opener() { + return this.d_opener; + } + + /** + * @property {NodeGit.Commit} + */ + get theirCommit() { + return this.d_theirCommit; + } + + /** + * @property {Open.SUB_OPEN_OPTION} + */ + get openOption() { + return this.d_openOption; + } + + /** + * @property {MODE} + */ + get mode() { + return this.d_mode; + } + + /** + * Reference to update when creating the merge commit + * @property {String | null} + */ + get refToUpdate() { + return this.forceBare ? null : "HEAD"; + } +} + +/** + * @async + * @return {Object} return from sub name to `SubmoduleChange` + * @return {Object} return.simpleChanges from sub name to `Submodule` + * @return {Object} return.changes from sub name to `Submodule` + * @return {Object} return.conflicts from sub name to `Conflict` + */ +MergeContext.prototype.getChanges = co.wrap(function *() { + if (null === this.d_changes) { + this.d_changes = + yield CherryPickUtil.computeChangesBetweenTwoCommits( + this.d_metaRepo, + yield this.getChangeIndex(), + yield this.getOurCommit(), + this.d_theirCommit); + } + return this.d_changes; +}); + +/** + * @async + * @return {NodeGit.Commit} return left side merge commit + */ +MergeContext.prototype.getOurCommit = co.wrap(function *() { + if (null !== this.d_ourCommit) { + return this.d_ourCommit; + } + if (this.forceBare) { + throw new UserError("Left side merge commit is undefined!"); + } + this.d_ourCommit = yield this.d_metaRepo.getHeadCommit(); + return this.d_ourCommit; +}); + +/** + * return an index object that contains the merge changes and whose tree + * representation will be flushed to disk. + * @async + * @return {NodeGit.Index} + */ +MergeContext.prototype.getIndexToWrite = co.wrap(function *() { + return this.forceBare ? + yield this.getChangeIndex() : + yield this.d_metaRepo.index(); +}); + +/** + * in memeory index object by merging `ourCommit` and `theirCommit` + * @return {NodeGit.Index} + */ +MergeContext.prototype.getChangeIndex = co.wrap(function *() { + if (null !== this.d_changeIndex) { + return this.d_changeIndex; + } + this.d_changeIndex = yield NodeGit.Merge.commits(this.d_metaRepo, + yield this.getOurCommit(), + this.d_theirCommit, + []); + return this.d_changeIndex; +}); + +/** + * Return the previously set/built commit message, or use the callback to + * build commit messsage. Once built, the commit message will be cached. + * + * @async + * @return {String} commit message + */ +MergeContext.prototype.getCommitMessage = co.wrap(function *() { + const message = (null === this.d_commitMessage) ? + GitUtil.stripMessage(yield this.d_editMessage()) : + this.d_commitMessage; + if ("" === message) { + console.log("Empty commit message."); + } + return message; +}); + +/** + * @async + * @returns {NodeGit.Signature} + */ +MergeContext.prototype.getSig = co.wrap(function *() { + return yield ConfigUtil.defaultSignature(this.d_metaRepo); +}); + +/** + * @async + * @returns {SubmoduleFetcher} + */ +MergeContext.prototype.getFetcher = co.wrap(function *() { + return yield this.d_opener.fetcher(); +}); + +exports.MergeContext = MergeContext; + +/** + * A class that tracks result from merging steps. + */ +class MergeStepResult { + + /** + * @param {String | null} infoMessage message to display to user + * @param {String | null} errorMessage message signifies a fatal error + * @param {String | null} finishSha commit sha indicating end of merge + * @param {Object} submoduleCommits map from submodule to commit + */ + constructor(infoMessage, errorMessage, finishSha, submoduleCommits) { + this.d_infoMessage = infoMessage; + this.d_errorMessage = errorMessage; + this.d_finishSha = finishSha; + this.d_submoduleCommits = submoduleCommits; + } + + /** + * @property {String|null} + */ + get errorMessage() { + return this.d_errorMessage; + } + + /** + * @property {String|null} + */ + get infoMessage() { + return this.d_infoMessage; + } + + /** + * @property {String|null} + */ + get finishSha() { + return this.d_finishSha; + } + + /** + * @property {Object} map from submodule to commit + */ + get submoduleCommits() { + if (null === this.d_submoduleCommits) { + return {}; + } + return this.d_submoduleCommits; + } + + /** + * @static + * @return {MergeStepResult} empty result object + */ + static empty() { + return new MergeStepResult(null, null, null, {}); + } + + /** + * A merge result that signifies we need to abort current merging process. + * + * @static + * @param {MergeStepResult} msg error message + */ + static error(msg) { + return new MergeStepResult(null, msg, null, {}); + } + /** + * A merge result that does not have any submodule commit. Only a finishing + * sha at the meta repo level will be returned. + * + * @static + * @param {String} infoMessage + * @param {String} finishSha meta repo commit sha + */ + static justMeta(infoMessage, finishSha) { + return new MergeStepResult(infoMessage, null, finishSha, {}); + } +} + +exports.MergeStepResult = MergeStepResult; diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 81c1de35e..46ea55097 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -41,18 +41,25 @@ const CherryPickUtil = require("./cherry_pick_util"); const ConfigUtil = require("./config_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); +const MergeCommon = require("./merge_common"); const Open = require("./open"); const RepoStatus = require("./repo_status"); const SequencerState = require("./sequencer_state"); const SequencerStateUtil = require("./sequencer_state_util"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const StatusUtil = require("./status_util"); +const SubmoduleChange = require("./submodule_change"); +const SubmoduleFetcher = require("./submodule_fetcher"); const SubmoduleRebaseUtil = require("./submodule_rebase_util"); const SubmoduleUtil = require("./submodule_util"); const UserError = require("./user_error"); -const CommitAndRef = SequencerState.CommitAndRef; -const MERGE = SequencerState.TYPE.MERGE; +const CommitAndRef = SequencerState.CommitAndRef; +const MERGE = SequencerState.TYPE.MERGE; +const MergeContext = MergeCommon.MergeContext; +const MergeStepResult = MergeCommon.MergeStepResult; +const MODE = MergeCommon.MODE; +const SUB_OPEN_OPTION = Open.SUB_OPEN_OPTION; /** * If there is a sequencer with a merge in the specified `path` return it, @@ -85,34 +92,39 @@ const checkForMerge = co.wrap(function *(path) { }); /** - * @enum {MODE} - * Flags to describe what type of merge to do. + * Return a formatted string indicating merge will abort for + * irresolvable conflicts. + * + * @param {Object} conflicts map from name to commit causing conflict + * @return {String} conflict message */ -const MODE = { - NORMAL : 0, // will do a fast-forward merge when possible - FF_ONLY : 1, // will fail unless fast-forward merge is possible - FORCE_COMMIT: 2, // will generate merge commit even could fast-forward +exports.formatConflictsMessage = function(conflicts) { + if (0 === Object.keys(conflicts).length) { + return ""; + } + let errorMessage = "CONFLICT (content): \n"; + const names = Object.keys(conflicts).sort(); + for (let name of names) { + errorMessage += `Conflicting entries for submodule: ` + + `${colors.red(name)}\n`; + } + errorMessage += "Automatic merge failed\n"; + return errorMessage; }; -exports.MODE = MODE; /** * Perform a fast-forward merge in the specified `repo` to the - * specified `commit`. When generating a merge commit, use the - * optionally specified `message`. The behavior is undefined unless - * `commit` is different from but descendant of the HEAD commit in - * `repo`. + * specified `commit`. The behavior is undefined unless `commit` + * is different from but descendant of the HEAD commit in `repo`. * * @param {NodeGit.Repository} repo - * @param {MODE} mode + * @param {MergeCommon.MODE} mode * @param {NodeGit.Commit} commit - * @param {String|null} message - */ -exports.fastForwardMerge = co.wrap(function *(repo, commit, message) { +exports.fastForwardMerge = co.wrap(function *(repo, commit) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); - assert.isString(message); // Remember the current branch; the checkoutCommit function will move it. @@ -129,294 +141,492 @@ exports.fastForwardMerge = co.wrap(function *(repo, commit, message) { }); /** - * Merge the specified `subs` in the specified `repo` having the specified - * `index`. Stage submodule commits in `metaRepo`. Return an object - * describing any commits that were generated and conflicted commits. Use the - * specified `opener` to acces submodule repos. Use the specified `message` to - * write commit messages. - * @param {NodeGit.Repository} metaRepo - * @param {Open.Opener} opener - * @param {NodeGit.Index} metaIndex - * @param {Object} subs map from name to SubmoduleChange + * Write tree representation of the index to the disk, create a commit + * from the tree and update reference if needed. + * + * @async + * @param {NodeGit.Repository} repo + * @param {NodeGit.Index} indexToWrite + * @param {NodeGit.Commit | null} ourCommit + * @param {NodeGit.Commit} theirCommit + * @param {String} commitMessage + * @param {String | null} refToUpdate * @return {Object} - * @return {Object} return.commits map from name to map from new to old ids - * @return {Object} return.conflicts map from name to commit causing conflict + * @return {String|null} return.infoMessage informative message + * @return {String|null} return.metaCommit in case no further merge operation + * is required, this is the merge commit. */ -const mergeSubmodules = co.wrap(function *(repo, - opener, - index, - subs, - message) { - assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(opener, Open.Opener); - assert.instanceOf(index, NodeGit.Index); - assert.isObject(subs); - assert.isString(message); - - const result = { - conflicts: {}, - commits: {}, - }; +exports.makeMetaCommit = co.wrap(function *(repo, + indexToWrite, + ourCommit, + theirCommit, + commitMessage, + refToUpdate) { + const id = yield indexToWrite.writeTreeTo(repo); const sig = yield ConfigUtil.defaultSignature(repo); - const fetcher = yield opener.fetcher(); - const mergeSubmodule = co.wrap(function *(name) { - const subRepo = yield opener.getSubrepo(name, false); - const change = subs[name]; - const fromSha = change.newSha; - yield fetcher.fetchSha(subRepo, name, fromSha); - const subHead = yield subRepo.getHeadCommit(); - const headSha = subHead.id().tostrS(); - const fromCommit = yield subRepo.getCommit(fromSha); + const metaCommit = yield repo.createCommit(refToUpdate, + sig, + sig, + commitMessage, + id, + [ourCommit, theirCommit]); + const commitSha = metaCommit.tostrS(); + return { + metaCommit: commitSha, + infoMessage: `Merge commit created at ` + + `${colors.green(commitSha)}.`, + }; +}); - // See if up-to-date +/** + * Merge the specified `subName` and update the in memeory `metaindex`. + * + * @async + * @param {NodeGit.Index} metaIndex index of the meta repo + * @param {String} subName submodule name + * @param {SubmoduleChange} change specifies the commits to merge + * @param {String} message commit message + * @param {SubmoduleFetcher} fetcher helper to fetch commits in the sub + * @param {NodeGit.Signature} sig default signature + * @param {Open.Opener} opener helper to open a sub + * @param {SUB_OPEN_OPTION} openOption option to open a sub + * @return {Object} + * @return {String|null} return.mergeSha + * @return {String|null} return.conflictSha + */ +exports.mergeSubmodule = co.wrap(function *(metaIndex, + subName, + change, + message, + opener, + fetcher, + sig, + openOption) { + assert.instanceOf(metaIndex, NodeGit.Index); + assert.isString(subName); + assert.instanceOf(change, SubmoduleChange); + assert.isString(message); + assert.instanceOf(opener, Open.Opener); + assert.instanceOf(fetcher, SubmoduleFetcher); + assert.instanceOf(sig, NodeGit.Signature); + assert.isNumber(openOption); + + let subRepo = yield opener.getSubrepo(subName, openOption); + + const isHalfOpened = yield opener.isHalfOpened(subName); + const forceBare = openOption === SUB_OPEN_OPTION.FORCE_BARE; + const theirSha = change.newSha; + yield fetcher.fetchSha(subRepo, subName, theirSha); + if (null !== change.ourSha) { + yield fetcher.fetchSha(subRepo, subName, change.ourSha); + } + const theirCommit = yield subRepo.getCommit(theirSha); - if (yield NodeGit.Graph.descendantOf(subRepo, headSha, fromSha)) { - return; // RETURN - } + const ourSha = change.ourSha; + const ourCommit = yield subRepo.getCommit(ourSha); + + const result = { + mergeSha: null, + conflictSha: null, + }; - // See if can fast-forward + // See if up-to-date + if (yield NodeGit.Graph.descendantOf(subRepo, ourSha, theirSha)) { + return result; // RETURN + } - if (yield NodeGit.Graph.descendantOf(subRepo, fromSha, headSha)) { - yield GitUtil.setHeadHard(subRepo, fromCommit); - yield index.addByPath(name); - return result; // RETURN + // See if can fast-forward and update HEAD if the submodule is opened. + if (yield NodeGit.Graph.descendantOf(subRepo, theirSha, ourSha)) { + if (isHalfOpened) { + yield CherryPickUtil.addSubmoduleCommit(metaIndex, + subName, + theirSha); + } else { + yield GitUtil.setHeadHard(subRepo, theirCommit); + yield metaIndex.addByPath(subName); } + return result; // RETURN + } - console.log(`Submodule ${colors.blue(name)}: merging commit \ -${colors.green(fromSha)}.`); - - // Start the merge. - - let subIndex = yield NodeGit.Merge.commits(subRepo, - subHead, - fromCommit, - null); + console.log(`Submodule ${colors.blue(subName)}: merging commit \ +${colors.green(theirSha)}.`); + // Start the merge. + let subIndex = yield NodeGit.Merge.commits(subRepo, + ourCommit, + theirCommit, + null); + if (!isHalfOpened) { yield NodeGit.Checkout.index(subRepo, subIndex, { checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, - }); - - // Abort if conflicted. + }); + } - if (subIndex.hasConflicts()) { - const seq = new SequencerState({ - type: MERGE, - originalHead: new CommitAndRef(subHead.id().tostrS(), null), - target: new CommitAndRef(fromSha, null), - currentCommit: 0, - commits: [fromSha], - message: message, + // handle conflicts: + // 1. if force bare, bubble up conflicts and direct return + // 2. if this is interactive merge and bare is allowed, open submodule, + // record conflicts and then bubble up the conflicts. + // 3. if bare is not allowed, record conflicts and bubble up conflicts + if (subIndex.hasConflicts()) { + if (forceBare) { + result.conflictSha = theirSha; + return result; + } + // fully open the submodule if conflict for manual resolution + if (isHalfOpened) { + opener.clearAbsorbedCache(subName); + subRepo = yield opener.getSubrepo(subName, + SUB_OPEN_OPTION.FORCE_OPEN); + yield NodeGit.Checkout.index(subRepo, subIndex, { + checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, }); - yield SequencerStateUtil.writeSequencerState(subRepo.path(), seq); - result.conflicts[name] = fromSha; - return; // RETURN } + const seq = new SequencerState({ + type: MERGE, + originalHead: new CommitAndRef(ourCommit.id().tostrS(), null), + target: new CommitAndRef(theirSha, null), + currentCommit: 0, + commits: [theirSha], + message: message, + }); + yield SequencerStateUtil.writeSequencerState(subRepo.path(), seq); + result.conflictSha = theirSha; + return result; // RETURN + } - // Otherwise, finish off the merge. - + // Otherwise, finish off the merge. + if (!isHalfOpened) { subIndex = yield subRepo.index(); - const treeId = yield subIndex.writeTreeTo(subRepo); - const mergeCommit = yield subRepo.createCommit("HEAD", - sig, - sig, - message, - treeId, - [subHead, fromCommit]); - result.commits[name] = mergeCommit.tostrS(); + } + const refToUpdate = isHalfOpened ? null : "HEAD"; + const treeId = yield subIndex.writeTreeTo(subRepo); + const mergeCommit = yield subRepo.createCommit(refToUpdate, + sig, + sig, + message, + treeId, + [ourCommit, theirCommit]); + const mergeSha = mergeCommit.tostrS(); + result.mergeSha = mergeSha; + if (isHalfOpened) { + yield CherryPickUtil.addSubmoduleCommit(metaIndex, subName, mergeSha); + } else { + yield metaIndex.addByPath(subName); // Clean up the conflict for this submodule and stage our change. - - yield index.addByPath(name); - yield index.conflictRemove(name); - }); - yield DoWorkQueue.doInParallel(Object.keys(subs), mergeSubmodule); + yield metaIndex.conflictRemove(subName); + } return result; }); /** - * Merge the specified `commit` in the specified `repo` having the specified - * `status`, using the specified `mode` to control whether or not a merge - * commit will be generated. Return `null` if the repository is up-to-date, or - * an object describing generated commits otherwise. If the optionally - * specified `commitMessage` is provided, use it as the commit message for any - * generated merge commit; otherwise, use the specified `editMessage` promise - * to request a message. Throw a `UserError` exception if a fast-forward merge - * is requested and cannot be completed. Throw a `UserError` if there are - * conflicts, or if local modifications prevent the merge from happening. - * Throw a `UserError` if there are no commits in common between `commit` and - * the HEAD commit of `repo`. - * + * Perform preparation work before merge, including + * 1. locate merge base + * 2. ensureNoURLChanges see also {CherryPickUtil.ensureNoURLChanges} + * 3. check if working dir is clean (non-bare repo) + * 3. check if two merging commits are the same or if their commit + * is an ancestor of ours, both cases are no-op. + * * @async - * @param {NodeGit.Repository} repo - * @param {NodeGit.Commit} commit - * @param {MODE} mode - * @param {String|null} commitMessage - * @param {() -> Promise(String)} editMessage - * @return {Object} - * @return {String|null} return.metaCommit - * @return {Object} return.submoduleCommits map from submodule to commit - * @return {String|null} return.errorMessage + * @param {MergeContext} context + * @return {MergeStepResult} */ -exports.merge = co.wrap(function *(repo, - commit, - mode, - commitMessage, - editMessage) { - assert.instanceOf(repo, NodeGit.Repository); - assert.isNumber(mode); - assert.instanceOf(commit, NodeGit.Commit); - if (null !== commitMessage) { - assert.isString(commitMessage); - } - assert.isFunction(editMessage); +const mergeStepPrepare = co.wrap(function *(context) { + assert.instanceOf(context, MergeContext); - const head = yield repo.getHeadCommit(); - const baseCommit = yield GitUtil.getMergeBase(repo, commit, head); + let errorMessage = null; + let infoMessage = null; + + const forceBare = context.forceBare; + const metaRepo = context.metaRepo; + const ourCommit = yield context.getOurCommit(); + const ourCommitSha = ourCommit.id().tostrS(); + const theirCommit = context.theirCommit; + const theirCommitSha = theirCommit.id().tostrS(); + + const baseCommit = + yield GitUtil.getMergeBase(metaRepo, theirCommit, ourCommit); if (null === baseCommit) { - throw new UserError(`\ -No commits in common with \ -${colors.red(GitUtil.shortSha(commit.id().tostrS()))}`); + errorMessage = "No commits in common with" + + `${colors.red(GitUtil.shortSha(ourCommitSha))} and ` + + `${colors.red(GitUtil.shortSha(theirCommitSha))}`; + return MergeStepResult.error(errorMessage); // RETURN } - yield CherryPickUtil.ensureNoURLChanges(repo, commit, baseCommit); - - const result = { - metaCommit: null, - submoduleCommits: {}, - errorMessage: null, - }; + yield CherryPickUtil.ensureNoURLChanges(metaRepo, theirCommit, baseCommit); - const status = yield StatusUtil.getRepoStatus(repo); - StatusUtil.ensureReady(status); - if (!status.isDeepClean(false)) { - // TODO: Git will refuse to run if there are staged changes, but will - // attempt a merge if there are just workdir changes. We should - // support this in the future, but it basically requires us to dry-run - // the merges in all the submodules. - - throw new UserError(`\ -The repository has uncommitted changes. Please stash or commit them before -running merge.`); + if (!forceBare) { + const status = yield StatusUtil.getRepoStatus(metaRepo); + const statusError = StatusUtil.checkReadiness(status); + if (null !== statusError) { + return MergeStepResult.error(statusError); // RETURN + } + if (!status.isDeepClean(false)) { + errorMessage = "The repository has uncommitted changes. "+ + "Please stash or commit them before running merge."; + return MergeStepResult.error(errorMessage); // RETURN + } } - const commitSha = commit.id().tostrS(); - - if (head.id().tostrS() === commit.id().tostrS()) { - console.log("Nothing to do."); - return result; + if (ourCommitSha === theirCommitSha) { + infoMessage = "Nothing to do."; + return MergeStepResult.justMeta(infoMessage, theirCommit); // RETURN } - const upToDate = yield NodeGit.Graph.descendantOf(repo, - head.id().tostrS(), - commitSha); + const upToDate = yield NodeGit.Graph.descendantOf(metaRepo, + ourCommitSha, + theirCommitSha); if (upToDate) { - console.log("Up-to-date."); - return result; + return MergeStepResult.justMeta(infoMessage, ourCommitSha); // RETURN } + return MergeStepResult.empty(); +}); - const canFF = yield NodeGit.Graph.descendantOf(repo, - commitSha, - head.id().tostrS()); - let message = ""; - if (!canFF || MODE.FORCE_COMMIT === mode) { - if (null === commitMessage) { - const raw = yield editMessage(); - message = GitUtil.stripMessage(raw); - if ("" === message) { - console.log("Empty commit message."); - return result; - } - } - else { - message = commitMessage; +/** + * Perform a fast-forward merge in the specified `repo` to the + * specified `commit`. When generating a merge commit, use the + * optionally specified `message`. The behavior is undefined unless + * `commit` is different from but descendant of the HEAD commit in + * `repo`. + * + * @async + * @param {MergeContext} content + * @return {MergeStepResult} + */ +const mergeStepFF = co.wrap(function *(context) { + assert.instanceOf(context, MergeContext); + + const forceBare = context.forceBare; + const metaRepo = context.metaRepo; + const mode = context.mode; + const ourCommit = yield context.getOurCommit(); + const ourCommitSha = ourCommit.id().tostrS(); + const theirCommit = context.theirCommit; + const theirCommitSha = theirCommit.id().tostrS(); + + let errorMessage = null; + let infoMessage = null; + + const canFF = yield NodeGit.Graph.descendantOf(metaRepo, + theirCommitSha, + ourCommitSha); + if (MODE.FF_ONLY === mode && !canFF) { + errorMessage = "The meta-repository cannot be fast-forwarded " + + `to ${colors.red(theirCommitSha)}.`; + return MergeStepResult.error(errorMessage); // RETURN + } else if (canFF && MODE.FORCE_COMMIT !== mode) { + infoMessage = `Fast-forwarding meta repo from `+ + `${colors.green(ourCommitSha)} to `+ + `${colors.green(theirCommitSha)}`; + if (!forceBare) { + yield exports.fastForwardMerge(metaRepo, theirCommit); } + return MergeStepResult.justMeta(infoMessage, theirCommitSha); // RETURN } + return MergeStepResult.empty(); +}); - if (MODE.FF_ONLY === mode && !canFF) { - throw new UserError(`The meta-repository cannot be fast-forwarded to \ -${colors.red(commitSha)}.`); - } - else if (canFF && MODE.FORCE_COMMIT !== mode) { - console.log(`Fast-forwarding meta-repo to ${colors.green(commitSha)}.`); - - result.metaCommit = commitSha; - yield exports.fastForwardMerge(repo, - commit, - message); - return result; +/** + * @async + * @param {MergeContext} context + * @return {MergeStepResult} + */ +const mergeStepMergeSubmodules = co.wrap(function *(context) { + assert.instanceOf(context, MergeContext); + + const changes = yield context.getChanges(); + const fetcher = yield context.getFetcher(); + const forceBare = context.forceBare; + const index = yield context.getIndexToWrite(); + const opener = context.opener; + const openOption = context.openOption; + const ourCommit = yield context.getOurCommit(); + const ourCommitSha = ourCommit.id().tostrS(); + const refToUpdate = context.refToUpdate; + const repo = context.metaRepo; + const sig = yield context.getSig(); + const theirCommit = context.theirCommit; + const theirCommitSha = theirCommit.id().tostrS(); + + let conflictMessage = ""; + // abort merge if conflicted under FROCE_BARE mode + if (forceBare && Object.keys(changes.conflicts).length > 0) { + conflictMessage = exports.formatConflictsMessage(changes.conflicts); + return MergeStepResult.error(conflictMessage); // RETURN } - const sig = yield ConfigUtil.defaultSignature(repo); - - const changeIndex = yield NodeGit.Merge.commits(repo, head, commit, []); - const changes = - yield CherryPickUtil.computeChanges(repo, changeIndex, commit); - const index = yield repo.index(); - const opener = new Open.Opener(repo, null); - - // Perform simple changes that don't require picks -- addition, deletions, - // and fast-forwards. - - yield CherryPickUtil.changeSubmodules(repo, - opener, - index, - changes.simpleChanges); - - // Render any conflicts - - let errorMessage = - yield CherryPickUtil.writeConflicts(repo, index, changes.conflicts); - - // Then do the submodule merges - - const merges = - yield mergeSubmodules(repo, opener, index, changes.changes, message); - result.submoduleCommits = merges.commits; - const conflicts = merges.conflicts; + // deal with simple changes + if (forceBare) { + yield CherryPickUtil.changeSubmodulesBare(repo, + index, + changes.simpleChanges); + } else { + yield CherryPickUtil.changeSubmodules(repo, + opener, + index, + changes.simpleChanges); + } - yield CherryPickUtil.closeSubs(opener, merges); + const message = yield context.getCommitMessage(); + if ("" === message) { + return MergeStepResult.empty(); + } - Object.keys(conflicts).sort().forEach(name => { - errorMessage += SubmoduleRebaseUtil.subConflictErrorMessage(name); + const merges = { + conflicts: {}, + commits: {}, + }; + const mergeSubmoduleRunner = co.wrap(function *(subName) { + const subResult = + yield exports.mergeSubmodule(index, + subName, + changes.changes[subName], + message, + opener, + fetcher, + sig, + openOption); + if (null !== subResult.mergeSha) { + merges.commits[subName] = subResult.mergeSha; + } + if (null !== subResult.conflictSha) { + merges.conflicts[subName] = subResult.conflictSha; + } }); - - // We must write the index here or the staging we've done earlier will go - // away. - yield SparseCheckoutUtil.writeMetaIndex(repo, index); - - if ("" !== errorMessage) { - // We're about to fail due to conflict. First, record that there is a - // merge in progress so that we can continue or abort it later. - // TODO: some day when we make use of it, write the ref name for HEAD - - const seq = new SequencerState({ - type: MERGE, - originalHead: new CommitAndRef(head.id().tostrS(), null), - target: new CommitAndRef(commit.id().tostrS(), null), - currentCommit: 0, - commits: [commit.id().tostrS()], - message: message, - }); - yield SequencerStateUtil.writeSequencerState(repo.path(), seq); - result.errorMessage = errorMessage; + yield DoWorkQueue.doInParallel(Object.keys(changes.changes), + mergeSubmoduleRunner); + // Render any conflicts + if (forceBare) { + conflictMessage = exports.formatConflictsMessage(merges.conflicts); } else { + conflictMessage = + yield CherryPickUtil.writeConflicts(repo, + index, + changes.conflicts); + /// + Object.keys(merges.conflicts).sort().forEach(name => { + conflictMessage += + SubmoduleRebaseUtil.subConflictErrorMessage(name); + }); + } - console.log(`Merging meta-repo commit ${colors.green(commitSha)}.`); + // finishing merge for interactive merges + // 1. close unnecessaried opened submodules + // 2. write the index to the meta repo or the staging we've done earlier + // will go away + if (!forceBare) { + yield CherryPickUtil.closeSubs(opener, merges); + yield SparseCheckoutUtil.writeMetaIndex(repo, index); + } - const id = yield index.writeTreeTo(repo); + if ("" !== conflictMessage) { + // For interactive merge, record that there is a merge in progress so + // that we can continue or abort it later + if (!forceBare) { + const seq = new SequencerState({ + type: MERGE, + originalHead: new CommitAndRef(ourCommitSha, null), + target: new CommitAndRef(theirCommitSha, null), + currentCommit: 0, + commits: [theirCommitSha], + message: message, + }); + yield SequencerStateUtil.writeSequencerState(repo.path(), seq); + } + return MergeStepResult.error(conflictMessage); + } - // And finally, commit it. + let infoMessage = `Merging meta-repo commits ` + + `${colors.green(ourCommitSha)} and ` + + `${colors.green(theirCommitSha)}`; + const metaCommitRet = yield exports.makeMetaCommit(repo, + index, + ourCommit, + theirCommit, + message, + refToUpdate); + infoMessage += "\n" + metaCommitRet.infoMessage; + return new MergeStepResult(infoMessage, + null, + metaCommitRet.metaCommit, + merges.commits); +}); - const metaCommit = yield repo.createCommit("HEAD", - sig, - sig, - message, - id, - [head, commit]); - result.metaCommit = metaCommit.tostrS(); +/** + * Merge `theirCommit` into `ourCommit` in the specified `repo` with specific + * commitMessage. using the specified `mode` to control whether or not a merge + * commit will be generated. `openOption` tells if creating a submodule under + * the working directory is forbidden (bare repo), is not encouraged or is + * always enforced. Commit message is either provided from `commitMessage` + * or from the `editMessage` callback. + * + * Return an object describing the resulting commit which can be: + * 1. our commit if our commit is up to date + * 2. their commit if this is a fast forward merge and FF is allowed + * 3. new commit whose parents are `ourCommit` and `theirCommit` + * + * Throw a `UserError` if: + * 1. there are no commits in common between `theirCommit` and `ourCommit`. + * 2. the repository has uncommitted changes + * 3. FF is enforced but not possible + * 4. FORCE_BARE is enabled, but there are merging conflicts + * + * @async + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit|null} ourCommit + * @param {NodeGit.Commit} theirCommit + * @param {MergeCommon.MODE} mode + * @param {Open.SUB_OPEN_OPTION} openOption + * @param {String|null} commitMessage + * @param {() -> Promise(String)} editMessage + * @return {Object} + * @return {String|null} return.metaCommit + * @return {Object} return.submoduleCommits map from submodule to commit + * @return {String|null} return.errorMessage + */ +exports.merge = co.wrap(function *(repo, + ourCommit, + theirCommit, + mode, + openOption, + commitMessage, + editMessage) { + // pack and validate merging objects + const context = new MergeContext(repo, + ourCommit, + theirCommit, + mode, + openOption, + commitMessage, + editMessage); + // + const result = { + metaCommit: null, + submoduleCommits: {}, + errorMessage: null, + }; + const mergeAsyncSteps = [ + mergeStepPrepare, + mergeStepFF, + mergeStepMergeSubmodules, + ]; + + for (const asyncStep of mergeAsyncSteps) { + const ret = yield asyncStep(context); + if (null !== ret.infoMessage) { + console.log(ret.infoMessage); + } + if (null !== ret.errorMessage) { + throw new UserError(ret.errorMessage); + } + if (null !== ret.finishSha) { + result.metaCommit = ret.finishSha; + result.submoduleCommits = ret.submoduleCommits; + return result; + } } return result; }); diff --git a/node/lib/util/open.js b/node/lib/util/open.js index 63add574c..021c114cc 100644 --- a/node/lib/util/open.js +++ b/node/lib/util/open.js @@ -43,6 +43,17 @@ const SubmoduleUtil = require("./submodule_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); const SubmoduleFetcher = require("./submodule_fetcher"); +/** + * @enum {SUB_OPEN_OPTION} + * Flags that describe whether to open a submodule if it is part of a merge. + */ +const SUB_OPEN_OPTION = { + FORCE_OPEN : 0, // non-bare repo and open sub if it is part of a merge + ALLOW_BARE : 1, // non-bare repo, do not open submodule unless have to + FORCE_BARE : 2, // bare repo, open submodule is not allowed +}; +exports.SUB_OPEN_OPTION = SUB_OPEN_OPTION; + /** * Open the submodule having the specified `submoduleName` in the meta-repo * associated with the specified `fetcher`; fetch the specified `submoduleSha` @@ -157,7 +168,14 @@ Opener.prototype._initialize = co.wrap(function *() { if (null === this.d_commit) { this.d_commit = yield this.d_repo.getHeadCommit(); } - this.d_subRepos = {}; + + // d_cachedSubs: normal subrepo opened and cached by this object + // d_cachedAbsorbedSubs: absorbed subrepo opened and cached by this object + // d_openSubs: subs that were open when this object was created + // d_absorbedSubs: subs that were half open when this object was created + this.d_cachedSubs = {}; + this.d_cachedAbsorbedSubs = {}; + this.d_openSubs = new Set(); if (!this.d_repo.isBare()) { const openSubsList = yield SubmoduleUtil.listOpenSubmodules(this.d_repo); @@ -202,13 +220,25 @@ Opener.prototype.getOpenedSubs = co.wrap(function*() { if (!this.d_initialized) { yield this._initialize(); } - const subs = Object.keys(this.d_subRepos); + const subs = Object.keys(this.d_cachedSubs); return subs.filter(name => !this.d_openSubs.has(name)); }); /** - * Return true if the submodule having the specified `subName` is open and - * false otherwise. + * Opener caches all repos that have previously been gotten, this method + * removes the sub repo from the absorbed cache given its name. Useful when + * the repo was previously opened as bare repo, and later need to be + * opened as a normal submodule. + * + * @param subName + */ +Opener.prototype.clearAbsorbedCache = function (subName) { + delete this.d_cachedAbsorbedSubs[subName]; +}; + +/** + * Return true if the submodule having the specified `subName` is fully + * openable, return false otherwise. * * @param {String} subName * @return {Boolean} @@ -217,12 +247,13 @@ Opener.prototype.isOpen = co.wrap(function *(subName) { if (!this.d_initialized) { yield this._initialize(); } - return this.d_openSubs.has(subName) || (subName in this.d_subRepos); + return this.d_openSubs.has(subName) || (subName in this.d_cachedSubs); }); /** * Return true if the submodule is opened nor half opened. * + * @async * @param {String} subName * @return {Boolean} */ @@ -232,12 +263,55 @@ Opener.prototype.isAtLeastHalfOpen = co.wrap(function *(subName) { } return this.d_absorbedSubs.has(subName) || this.d_openSubs.has(subName) || - (subName in this.d_subRepos); + (subName in this.d_cachedSubs) || + (subName in this.d_cachedAbsorbedSubs); +}); + +/** + * Return true if the submodule is opened as a bare or absorbed repo. + * + * @async + * @param {String} subName + * @return {Boolean} + */ +Opener.prototype.isHalfOpened = co.wrap(function *(subName) { + if (!this.d_initialized) { + yield this._initialize(); + } + return (subName in this.d_cachedAbsorbedSubs); +}); + +/** + * Get sha of a submodule and open the submodule on that sha + * + * @param {String} subName + * @returns {NodeGit.Repository} sub repo that is opened. + */ +Opener.prototype.fullOpen = co.wrap(function *(subName) { + const entry = yield this.d_tree.entryByPath(subName); + const sha = entry.sha(); + console.log(`\ +Opening ${colors.blue(subName)} on ${colors.green(sha)}.`); + return yield exports.openOnCommit(this.d_fetcher, + subName, + sha, + this.d_templatePath, + false); }); /** * Return the repository for the specified `submoduleName`, opening it if - * necessary. + * necessary based on the expected working directory type: + * 1. FORCE_BARE + * - directly return opened absorbed sub if there is one + * - open bare repo otherwise + * 2. ALLOW_BARE + * - directly return opened sub if there is one + * - directly return opened absorbed sub if there is one + * - open absorbed sub + * 3. FORCE_OPEN + * - directly return opened sub if there is one + * - open normal repo otherwise * * Note that after opening one or more submodules, * `SparseCheckoutUtil.writeMetaIndex` must be called so that `SKIP_WORKTREE` @@ -245,43 +319,59 @@ Opener.prototype.isAtLeastHalfOpen = co.wrap(function *(subName) { * each time a submodule is opened. * * @param {String} subName - * @param {boolean} bare + * @param {SUB_OPEN_OPTION} openOption * @return {NodeGit.Repository} */ -Opener.prototype.getSubrepo = co.wrap(function *(subName, bare) { +Opener.prototype.getSubrepo = co.wrap(function *(subName, openOption) { if (!this.d_initialized) { yield this._initialize(); } - let subRepo = this.d_subRepos[subName]; + let subRepo = this.d_cachedSubs[subName]; if (undefined !== subRepo) { return subRepo; // it was found } - if (bare) { - if (this.d_absorbedSubs.has(subName)) { - subRepo = yield SubmoduleUtil.getBareRepo(this.d_repo, subName); - } else { - subRepo = yield exports.openOnCommit(this.d_fetcher, - subName, - "", - this.d_templatePath, - true); + if (SUB_OPEN_OPTION.FORCE_OPEN !== openOption) { + subRepo = this.d_cachedAbsorbedSubs[subName]; + if (undefined !== subRepo) { + return subRepo; } } - else if (this.d_openSubs.has(subName)) { - subRepo = yield SubmoduleUtil.getRepo(this.d_repo, subName); - } - else { - const entry = yield this.d_tree.entryByPath(subName); - const sha = entry.sha(); - console.log(`\ -Opening ${colors.blue(subName)} on ${colors.green(sha)}.`); - subRepo = yield exports.openOnCommit(this.d_fetcher, - subName, - sha, - this.d_templatePath, - false); + const openable = yield this.isOpen(subName); + const halfOpenable = yield this.isAtLeastHalfOpen(subName); + + switch (openOption) { + case SUB_OPEN_OPTION.FORCE_BARE: + subRepo = halfOpenable ? + yield SubmoduleUtil.getBareRepo(this.d_repo, subName) : + yield exports.openOnCommit(this.d_fetcher, + subName, + "", + this.d_templatePath, + true); + this.d_cachedAbsorbedSubs[subName] = subRepo; + break; + case SUB_OPEN_OPTION.ALLOW_BARE: + if (openable) { + subRepo = yield SubmoduleUtil.getRepo(this.d_repo, subName); + this.d_cachedSubs[subName] = subRepo; + } else { + subRepo = halfOpenable ? + yield SubmoduleUtil.getBareRepo(this.d_repo, subName) : + yield exports.openOnCommit(this.d_fetcher, + subName, + "", + this.d_templatePath, + true); + this.d_cachedAbsorbedSubs[subName] = subRepo; + } + break; + default: + subRepo = openable ? + yield SubmoduleUtil.getRepo(this.d_repo, subName) : + yield this.fullOpen(subName); + this.d_cachedSubs[subName] = subRepo; + break; } - this.d_subRepos[subName] = subRepo; return subRepo; }); exports.Opener = Opener; diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index bdfaf9f6a..0322d4346 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -219,7 +219,9 @@ exports.reset = co.wrap(function *(repo, commit, type) { // Open the submodule and fetch the sha of the commit to which we're // resetting in case we don't have it. - const subRepo = yield opener.getSubrepo(name, false); + const subRepo = + yield opener.getSubrepo(name, + Open.SUB_OPEN_OPTION.FORCE_OPEN); let subCommitSha; diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index f34f11829..225700f85 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -317,7 +317,9 @@ exports.apply = co.wrap(function *(repo, id, reinstateIndex) { return; // RETURN } - const subRepo = yield opener.getSubrepo(name, false); + const subRepo = + yield opener.getSubrepo(name, + Open.SUB_OPEN_OPTION.FORCE_OPEN); // Try to get the comit for the stash; if it's missing, fail. diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 7cd352f54..0487183f8 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -583,33 +583,50 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { }); /** - * Throw a `UserError` unless the specified `status` reflects a repository in a + * Wrapper around `checkReadiness` and throw a `UserError` if the repo + * is not in anormal, ready state. + * @see {checkReadiness} + * @param {RepoStatus} status + * @throws {UserError} + */ +exports.ensureReady = function (status) { + const errorMessage = exports.checkReadiness(status); + if (null !== errorMessage) { + throw new UserError(errorMessage); + } +}; + +/** + * Return an error message if the specified `status` of a repository isn't in a * normal, ready state, that is, it does not have any conflicts or in-progress * operations from the sequencer. Adjust output paths to be relative to the * specified `cwd`. * * @param {RepoStatus} status + * @returns {String | null} if not null, the return implies that the repo is + * not ready. */ -exports.ensureReady = function (status) { +exports.checkReadiness = function (status) { assert.instanceOf(status, RepoStatus); if (null !== status.rebase) { - throw new UserError(`\ + return (`\ Before proceeding, you must complete the rebase in progress (by running 'git meta rebase --continue') or abort it (by running 'git meta rebase --abort').`); } if (status.isConflicted()) { - throw new UserError(`\ + return (`\ Please resolve outstanding conflicts before proceeding: ${PrintStatusUtil.printRepoStatus(status, "")}`); } if (null !== status.sequencerState) { const command = PrintStatusUtil.getSequencerCommand(status.sequencerState.type); - throw new UserError(`\ + return (`\ Before proceeding, you must complete the ${command} in progress (by running 'git meta ${command} --continue') or abort it (by running 'git meta ${command} --abort').`); } + return null; }; diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index 14286e275..ef3715004 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -522,7 +522,7 @@ exports.initSubmoduleAndRepo = co.wrap(function *(repoUrl, name, url, templatePath, - bare=false) { + bare) { if (null !== repoUrl) { assert.isString(repoUrl); } diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 33606b090..43c7e95ce 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -564,10 +564,10 @@ exports.getSubmodulesInPath = function (dir, indexSubNames) { } // test if the short path a parent dir of the long path - const isParentDir = (short, long) => { - return long.startsWith(short) && ( - short[short.length-1] === "/" || - long[short.length] === "/" + const isParentDir = (shortPath, longPath) => { + return longPath.startsWith(shortPath) && ( + shortPath[shortPath.length-1] === "/" || + longPath[shortPath.length] === "/" ); }; const result = []; diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index efbc3f445..ffa8a5e8b 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -814,7 +814,8 @@ git -C '${repo.path()}' -c gc.reflogExpire=0 -c gc.reflogExpireUnreachable=0 \ repo, subName, sub.url, - null); + null, + false); // Pull in commits from the commits repo, but remove the remote // when done. diff --git a/node/test/util/merge_bare_util.js b/node/test/util/merge_bare.js similarity index 93% rename from node/test/util/merge_bare_util.js rename to node/test/util/merge_bare.js index 0b1f82d17..8bf64e87d 100644 --- a/node/test/util/merge_bare_util.js +++ b/node/test/util/merge_bare.js @@ -34,13 +34,15 @@ const assert = require("chai").assert; const co = require("co"); const colors = require("colors"); -const MergeBareUtil = require("../../lib//util/merge_bare_util"); +const MergeUtil = require("../../lib/util/merge_util"); +const Open = require("../../lib/util/open"); +const MergeCommon = require("../../lib//util/merge_common"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); describe("MergeBareUtil", function () { describe("merge_with_all_cases", function () { // Similar to tests of merge, but with no need for a working directory. - const MODE = MergeBareUtil.MODE; + const MODE = MergeCommon.MODE; const cases = { "3 way merge in bare": { initial: ` @@ -207,6 +209,7 @@ a=B:Ca-1;Cb-1;Ba=a;Bb=b| x=S:C2-1 s=Sa:a;C3-1 s=Sa:b;Bmaster=2;Bfoo=3`, theirCommit: "3", ourCommit: "2", + fails: true, errorMessage: `\ CONFLICT (content): Conflicting entries for submodule: ${colors.red("s")} @@ -219,6 +222,7 @@ a=B:Ca-1 README.md=8;Cb-1 README.md=9;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, theirCommit: "4", ourCommit: "3", + fails: true, errorMessage: `\ CONFLICT (content): Conflicting entries for submodule: ${colors.red("s")} @@ -288,11 +292,15 @@ x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; message = "message\n"; } const mode = !("mode" in c) ? MODE.FORCE_COMMIT : c.mode; - const result = yield MergeBareUtil.merge(x, - ourCommit, - theirCommit, - mode, - message); + const openOption = Open.SUB_OPEN_OPTION.FORCE_BARE; + const defaultEditor = function () {}; + const result = yield MergeUtil.merge(x, + ourCommit, + theirCommit, + mode, + openOption, + message, + defaultEditor); const errorMessage = c.errorMessage || null; assert.equal(result.errorMessage, errorMessage); if (upToDate) { diff --git a/node/test/util/merge_full_open.js b/node/test/util/merge_full_open.js new file mode 100644 index 000000000..35ff79a83 --- /dev/null +++ b/node/test/util/merge_full_open.js @@ -0,0 +1,427 @@ +/* + * Copyright (c) 2019, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); +const colors = require("colors"); + +const MergeUtil = require("../../lib//util/merge_util"); +const MergeCommon = require("../../lib//util/merge_common"); +const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const Open = require("../../lib/util/open"); + +/** + * Return the commit map required by 'RepoASTTestUtil.testMultiRepoManipulator' + * from the specified 'result' returned by the 'merge' and 'continue' function, + * using the specified 'maps' provided to the manipulators. + */ +function mapReturnedCommits(result, maps) { + assert.isObject(result); + let newCommitMap = {}; + + // If a new commit was generated -- it wasn't a fast-forward commit -- + // record a mapping from the new commit to it's logical name: "x". + + const commitMap = maps.commitMap; + if (null !== result.metaCommit && !(result.metaCommit in commitMap)) { + newCommitMap[result.metaCommit] = "x"; + } + + // Map the new commits in submodules to the names of the submodules where + // they were made. + + Object.keys(result.submoduleCommits).forEach(name => { + commitMap[result.submoduleCommits[name]] = name; + }); + return { + commitMap: newCommitMap, + }; +} + +describe("MergeFullOpen", function () { + describe("merge", function () { + // Will do merge from repo `x`. A merge commit in the meta-repo will + // be named `x`; any merge commits in the sub-repos will be given the + // name of the sub-repo in which they are made. TODO: test for changes + // to submodule shas, and submodule deletions + + // Test plan: + // - basic merging with meta-repo: normal/ffw/force commit; note that + // fast-forward merges are tested in the driver for + // 'fastForwardMerge', so we just need to validate that it works once + // here + // - many scenarios with submodules + // - merges with open/closed unaffected submodules + // - where submodules are opened and closed + // - where they can and can't be fast-forwarded + + const MODE = MergeCommon.MODE; + const cases = { + "no merge base": { + initial: "x=S:Cx s=Sa:1;Bfoo=x", + fromCommit: "x", + fails: true, + }, + "not ready": { + initial: "x=S:QR 1: 1: 0 1", + fromCommit: "1", + fails: true, + }, + "url changes": { + initial: "a=B|b=B|x=U:C3-2 s=Sb:1;Bfoo=3", + fromCommit: "3", + fails: true, + }, + "ancestor url changes": { + initial: "a=B|b=B|x=U:C4-3 q=Sa:1;C3-2 s=Sb:1;Bfoo=4", + fromCommit: "4", + fails: true, + }, + "dirty": { + initial: "a=B|x=U:C3-1 t=Sa:1;Bfoo=3;Os W README.md=8", + fromCommit: "3", + fails: true, + }, + "dirty index": { + initial: "a=B|x=U:C3-1 t=Sa:1;Bfoo=3;Os I README.md=8", + fromCommit: "3", + fails: true, + }, + "trivial -- nothing to do xxxx": { + initial: "x=S", + fromCommit: "1", + }, + "up-to-date": { + initial: "a=B|x=U:C3-2 t=Sa:1;Bmaster=3;Bfoo=2", + fromCommit: "2", + }, + "trivial -- nothing to do, has untracked change": { + initial: "a=B|x=U:Os W foo=8", + fromCommit: "2", + }, + "staged change": { + initial: "a=B|x=U:Os I foo=bar", + fromCommit: "1", + fails: true, + }, + "submodule commit": { + initial: "a=B|x=U:Os Cs-1!H=s", + fromCommit: "1", + fails: true, + }, + "already a merge in progress": { + initial: "x=S:Qhia#M 1: 1: 0 1", + fromCommit: "1", + fails: true, + }, + "fast forward": { + initial: "a=B|x=S:C2-1 s=Sa:1;Bfoo=2", + fromCommit: "2", + expected: "a=B|x=E:Bmaster=2", + }, + "fast forward, but forced commit": { + initial: "a=B|x=S:C2-1 s=Sa:1;Bfoo=2", + fromCommit: "2", + mode: MergeCommon.MODE.FORCE_COMMIT, + expected: "a=B|x=E:Bmaster=x;Cx-1,2 s=Sa:1", + }, + "one merge": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 s=Sa:s;Bmaster=x;Os Cs-a,b b=b", + }, + "one merge, but ff only": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + mode: MergeCommon.MODE.FF_ONLY, + fails: true, + }, + "one merge with ancestor": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C5-4 t=Sa:b;C4-2 s=Sa:b;Bmaster=3;Bfoo=5`, + fromCommit: "5", + expected: ` +x=E:Cx-3,5 t=Sa:b,s=Sa:s;Bmaster=x;Os Cs-a,b b=b`, + }, + "one merge with editor": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + editMessage: () => Promise.resolve("foo\nbar\n# baz\n"), + expected: ` +x=E:Cfoo\nbar\n#x-3,4 s=Sa:s;Bmaster=x;Os Cfoo\nbar\n#s-a,b b=b`, + message: null, + }, + "one merge with empty message": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + editMessage: () => Promise.resolve(""), + message: null, + }, + "non-ffmerge with trivial ffwd submodule change": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 t=Sa:b;C4-2 s=Sa:b;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + expected: "x=E:Cx-3,4 s=Sa:b;Os H=b;Bmaster=x", + }, + "sub is same": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:b,t=Sa:b;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + expected: "x=E:Cx-3,4 t=Sa:b;Bmaster=x", + }, + "sub is same, closed": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:b,t=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 t=Sa:b;Bmaster=x", + }, + "sub is behind": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:a;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + expected: "x=E:Cx-3,4 ;Bmaster=x", + }, + "sub is behind, closed": { + initial: ` +a=Aa:Cb-a;Bb=b| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:a;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 ;Bmaster=x", + }, + "non-ffmerge with ffwd submodule change": { + initial: ` +a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + expected: "x=E:Cx-3,4 s=Sa:c;Os H=c;Bmaster=x", + }, + "non-ffmerge with ffwd submodule change, closed": { + initial: ` +a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 s=Sa:c;Bmaster=x", + }, + "non-ffmerge with deeper ffwd submodule change": { + initial: ` +a=Aa:Cb-a;Bb=b;Cc-b;Cd-c;Bd=d| +x=U:C3-2 s=Sa:b;C5-4 s=Sa:d;C4-2 s=Sa:c;Bmaster=3;Bfoo=5`, + fromCommit: "5", + expected: "x=E:Cx-3,5 s=Sa:d;Bmaster=x", + }, + "non-ffmerge with ffwd submodule change on lhs": { + initial: ` +a=Aa:Cb-a;Bb=b;Cc-b;Bc=c| +x=U:C3-2 s=Sa:b;C4-2 q=Sa:a;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 q=Sa:a;Bmaster=x", + }, + "non-ffmerge with non-ffwd submodule change": { + initial: ` +a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 s=Sa:s;Os Cs-b,c c=c!H=s;Bmaster=x", + }, + "non-ffmerge with non-ffwd submodule change, sub already open": { + initial: ` +a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + expected: "x=E:Cx-3,4 s=Sa:s;Os Cs-b,c c=c!H=s;Bmaster=x", + }, + "submodule commit is up-to-date": { + initial:` +a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:c;C4-2 s=Sa:b,t=Sa:a;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + expected: "x=E:Cx-3,4 t=Sa:a;Os H=c;Bmaster=x", + }, + "submodule commit is up-to-date, was not open": { + initial:` +a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:c;C4-2 s=Sa:b,t=Sa:a;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 t=Sa:a;Bmaster=x", + }, + "submodule commit is same": { + initial: ` +a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:c;C4-2 s=Sa:c,q=Sa:a;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: "x=E:Cx-3,4 q=Sa:a;Bmaster=x", + }, + "added in merge": { + initial: ` +a=B| +x=S:C2-1;C3-1 t=Sa:1;Bmaster=2;Bfoo=3`, + fromCommit: "3", + expected: "x=E:Cx-2,3 t=Sa:1;Bmaster=x", + }, + "added on both sides": { + initial: ` +a=B| +x=S:C2-1 s=Sa:1;C3-1 t=Sa:1;Bmaster=2;Bfoo=3`, + fromCommit: "3", + expected: "x=E:Cx-2,3 t=Sa:1;Bmaster=x", + }, + "conflicted add": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=S:C2-1 s=Sa:a;C3-1 s=Sa:b;Bmaster=2;Bfoo=3`, + fromCommit: "3", + fails: true, + expected: `x=E:Qmessage\n#M 2: 3: 0 3;I *s=~*S:a*S:b`, + errorMessage: `\ +Conflicting entries for submodule ${colors.red("s")} +`, + }, + "conflict in submodule": { + initial: ` +a=B:Ca-1 README.md=8;Cb-1 README.md=9;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + fromCommit: "4", + fails: true, + errorMessage: `\ +Submodule ${colors.red("s")} is conflicted. +`, + expected: ` +x=E:Qmessage\n#M 3: 4: 0 4; +Os Qmessage\n#M a: b: 0 b!I *README.md=hello world*8*9!W README.md=\ +<<<<<<< ours +8 +======= +9 +>>>>>>> theirs +; +`, + }, + "new commit in sub in target branch but not in HEAD branch": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 t=Sa:1;C4-3 s=Sa:a;C5-3 t=Sa:b;Bmaster=4;Bfoo=5;Os;Ot`, + fromCommit: "5", + expected: ` +x=E:Cx-4,5 t=Sa:b;Bmaster=x;Ot H=b;Os` + }, + "new commit in sub in target branch but not in HEAD branch, closed" + : { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 t=Sa:1;C4-3 s=Sa:a;C5-3 t=Sa:b;Bmaster=4;Bfoo=5`, + fromCommit: "5", + expected: ` +x=E:Cx-4,5 t=Sa:b;Bmaster=x` + }, + "merge in a branch with a removed sub": { + initial: ` +a=B:Ca-1;Ba=a| +x=U:C3-2 t=Sa:1;C4-2 s;Bmaster=3;Bfoo=4`, + fromCommit: "4", + expected: `x=E:Cx-3,4 s;Bmaster=x`, + }, + "merge to a branch with a removed sub": { + initial: ` +a=B:Ca-1;Ba=a| +x=U:C3-2 t=Sa:1;C4-2 s;Bmaster=4;Bfoo=3`, + fromCommit: "3", + expected: `x=E:Cx-4,3 t=Sa:1;Bmaster=x`, + }, + "change with multiple merge bases": { + initial: ` +a=B:Ca-1;Ba=a| +x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; + C3-2 s=Sa:a; + C4-2 t=Sa:a; + Cl-3,4 s,t; + Ct-3,4 a=Sa:1,t=Sa:a; + Bmaster=l;Bfoo=t`, + fromCommit: "t", + expected: "x=E:Cx-l,t a=Sa:1;Bmaster=x", + }, + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const expected = c.expected; + + const doMerge = co.wrap(function *(repos, maps) { + const upToDate = null === expected; + const mode = !("mode" in c) ? MODE.NORMAL : c.mode; + const x = repos.x; + const reverseCommitMap = maps.reverseCommitMap; + assert.property(reverseCommitMap, c.fromCommit); + const physicalCommit = reverseCommitMap[c.fromCommit]; + const commit = yield x.getCommit(physicalCommit); + let message = c.message; + if (undefined === message) { + message = "message\n"; + } + const defaultEditor = function () {}; + const editMessage = c.editMessage || defaultEditor; + const openOption = Open.SUB_OPEN_OPTION.FORCE_OPEN; + const result = yield MergeUtil.merge(x, + null, + commit, + mode, + openOption, + message, + editMessage); + const errorMessage = c.errorMessage || null; + assert.equal(result.errorMessage, errorMessage); + if (upToDate) { + assert.isNull(result.metaCommit); + return; // RETURN + } + return mapReturnedCommits(result, maps); + }); + yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, + expected || {}, + doMerge, + c.fails); + })); + }); + }); +}); diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index b7e717eb9..f12c06d92 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -35,7 +35,9 @@ const co = require("co"); const colors = require("colors"); const MergeUtil = require("../../lib//util/merge_util"); +const MergeCommon = require("../../lib//util/merge_common"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const Open = require("../../lib/util/open"); /** * Return the commit map required by 'RepoASTTestUtil.testMultiRepoManipulator' @@ -149,7 +151,7 @@ x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, // - where submodules are opened and closed // - where they can and can't be fast-forwarded - const MODE = MergeUtil.MODE; + const MODE = MergeCommon.MODE; const cases = { "no merge base": { initial: "x=S:Cx s=Sa:1;Bfoo=x", @@ -216,7 +218,7 @@ x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, "fast forward, but forced commit": { initial: "a=B|x=S:C2-1 s=Sa:1;Bfoo=2", fromCommit: "2", - mode: MergeUtil.MODE.FORCE_COMMIT, + mode: MergeCommon.MODE.FORCE_COMMIT, expected: "a=B|x=E:Bmaster=x;Cx-1,2 s=Sa:1", }, "one merge": { @@ -224,14 +226,14 @@ x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, a=B:Ca-1;Cb-1;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, fromCommit: "4", - expected: "x=E:Cx-3,4 s=Sa:s;Bmaster=x;Os Cs-a,b b=b", + expected: "x=E:Cx-3,4 s=Sa:s;Bmaster=x", }, "one merge, but ff only": { initial: ` a=B:Ca-1;Cb-1;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, fromCommit: "4", - mode: MergeUtil.MODE.FF_ONLY, + mode: MergeCommon.MODE.FF_ONLY, fails: true, }, "one merge with ancestor": { @@ -240,7 +242,7 @@ a=B:Ca-1;Cb-1;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;C5-4 t=Sa:b;C4-2 s=Sa:b;Bmaster=3;Bfoo=5`, fromCommit: "5", expected: ` -x=E:Cx-3,5 t=Sa:b,s=Sa:s;Bmaster=x;Os Cs-a,b b=b`, +x=E:Cx-3,5 t=Sa:b,s=Sa:s;Bmaster=x`, }, "one merge with editor": { initial: ` @@ -249,7 +251,7 @@ x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, fromCommit: "4", editMessage: () => Promise.resolve("foo\nbar\n# baz\n"), expected: ` -x=E:Cfoo\nbar\n#x-3,4 s=Sa:s;Bmaster=x;Os Cfoo\nbar\n#s-a,b b=b`, +x=E:Cfoo\nbar\n#x-3,4 s=Sa:s;Bmaster=x`, message: null, }, "one merge with empty message": { @@ -328,7 +330,7 @@ x=U:C3-2 s=Sa:b;C4-2 q=Sa:a;Bmaster=3;Bfoo=4`, a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c| x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4`, fromCommit: "4", - expected: "x=E:Cx-3,4 s=Sa:s;Os Cs-b,c c=c!H=s;Bmaster=x", + expected: "x=E:Cx-3,4 s=Sa:s;Bmaster=x", }, "non-ffmerge with non-ffwd submodule change, sub already open": { initial: ` @@ -378,6 +380,7 @@ a=B:Ca-1;Cb-1;Ba=a;Bb=b| x=S:C2-1 s=Sa:a;C3-1 s=Sa:b;Bmaster=2;Bfoo=3`, fromCommit: "3", expected: `x=E:Qmessage\n#M 2: 3: 0 3;I *s=~*S:a*S:b`, + fails: true, errorMessage: `\ Conflicting entries for submodule ${colors.red("s")} `, @@ -387,6 +390,7 @@ Conflicting entries for submodule ${colors.red("s")} a=B:Ca-1 README.md=8;Cb-1 README.md=9;Ba=a;Bb=b| x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, fromCommit: "4", + fails: true, errorMessage: `\ Submodule ${colors.red("s")} is conflicted. `, @@ -464,17 +468,26 @@ x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; } const defaultEditor = function () {}; const editMessage = c.editMessage || defaultEditor; + const openOption = Open.SUB_OPEN_OPTION.ALLOW_BARE; + const result = yield MergeUtil.merge(x, + null, commit, mode, + openOption, message, editMessage); const errorMessage = c.errorMessage || null; assert.equal(result.errorMessage, errorMessage); + if (upToDate) { assert.isNull(result.metaCommit); return; // RETURN } + + if (!result.metaCommit) { + return; + } return mapReturnedCommits(result, maps); }); yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, diff --git a/node/test/util/open.js b/node/test/util/open.js index c88a3d5f9..21109640f 100644 --- a/node/test/util/open.js +++ b/node/test/util/open.js @@ -40,6 +40,7 @@ const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); const SubmoduleFetcher = require("../../lib/util/submodule_fetcher"); const SubmoduleUtil = require("../../lib/util/submodule_util"); +const FORCE_OPEN = Open.SUB_OPEN_OPTION.FORCE_OPEN; describe("openOnCommit", function () { // Assumption is that 'x' is the target repo. // TODO: test for template path usage. We're just passing it through but @@ -104,8 +105,8 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos("a=B|x=U:Os"); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - const s1 = yield opener.getSubrepo("s", false); - const s2 = yield opener.getSubrepo("s", false); + const s1 = yield opener.getSubrepo("s", FORCE_OPEN); + const s2 = yield opener.getSubrepo("s", FORCE_OPEN); const base = yield SubmoduleUtil.getRepo(repo, "s"); assert.equal(s1, s2, "not re-opened"); assert.equal(s1.workdir(), base.workdir(), "right path"); @@ -114,8 +115,8 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos("a=B|x=U"); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - const s1 = yield opener.getSubrepo("s", false); - const s2 = yield opener.getSubrepo("s", false); + const s1 = yield opener.getSubrepo("s", FORCE_OPEN); + const s2 = yield opener.getSubrepo("s", FORCE_OPEN); const base = yield SubmoduleUtil.getRepo(repo, "s"); assert.equal(s1, s2, "not re-opened"); assert.equal(s1.workdir(), base.workdir(), "right path"); @@ -133,7 +134,7 @@ describe("openOnCommit", function () { const repo = w.repos.x; const commit = yield repo.getCommit(baseSha); const opener = new Open.Opener(repo, commit); - const s = yield opener.getSubrepo("s", false); + const s = yield opener.getSubrepo("s", FORCE_OPEN); const head = yield s.getHeadCommit(); assert.equal(head.id().tostrS(), subSha); })); @@ -158,7 +159,7 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos(state); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - yield opener.getSubrepo("s", false); + yield opener.getSubrepo("s", FORCE_OPEN); const open = yield opener.getOpenSubs(); assert.deepEqual(Array.from(open), []); })); @@ -183,7 +184,7 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos(state); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - yield opener.getSubrepo("s", false); + yield opener.getSubrepo("s", FORCE_OPEN); const opened = yield opener.getOpenedSubs(); assert.deepEqual(opened, []); })); @@ -192,7 +193,7 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos(state); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - yield opener.getSubrepo("s", false); + yield opener.getSubrepo("s", FORCE_OPEN); const opened = yield opener.getOpenedSubs(); assert.deepEqual(opened, ["s"]); })); @@ -209,7 +210,7 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos(state); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - const s = yield opener.getSubrepo("s", false); + const s = yield opener.getSubrepo("s", FORCE_OPEN); const result = yield opener.isOpen("s"); assert.equal(true, result); const config = yield s.config(); diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index 3cbf504c9..b65c5961d 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -1580,7 +1580,8 @@ describe("readRAST", function () { r, "x", "foo", - null); + null, + false); const ast = yield ReadRepoASTUtil.readRAST(r); const headId = yield r.getHeadCommit(); @@ -1626,7 +1627,8 @@ describe("readRAST", function () { r, "x", "foo", - null); + null, + false); const subCommit = yield TestUtil.generateCommit(subRepo); yield NodeGit.Checkout.tree(subRepo, subCommit, { checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, diff --git a/node/test/util/submodule_config_util.js b/node/test/util/submodule_config_util.js index 6782ec876..5468cfeb9 100644 --- a/node/test/util/submodule_config_util.js +++ b/node/test/util/submodule_config_util.js @@ -672,7 +672,8 @@ foo repo, subName, url, - null); + null, + false); assert.instanceOf(result, NodeGit.Repository); assert(TestUtil.isSameRealPath(result.workdir(), path.join(repoPath, subName))); @@ -714,7 +715,8 @@ foo repo, "foo", url, - null); + null, + false); const remote = yield newSub.getRemote("origin"); const newUrl = remote.url(); assert.equal(newUrl, url); @@ -789,7 +791,8 @@ foo repo, "foo", url, - templateDir); + templateDir, + false); const copiedPath = path.join(repo.path(), "modules", From 3db6a40df499209d6631faa312404cc0f664eaac Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Thu, 14 Feb 2019 17:28:24 -0500 Subject: [PATCH 242/402] Fix merge command --- node/lib/cmd/merge.js | 1 + node/lib/cmd/merge_bare.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index 34b1e19f7..43443556d 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -170,6 +170,7 @@ Merge of '${commitName}' }; const commit = yield repo.getCommit(commitish.id()); const result = yield MergeUtil.merge(repo, + null, commit, mode, Open.SUB_OPEN_OPTION.ALLOW_BARE, diff --git a/node/lib/cmd/merge_bare.js b/node/lib/cmd/merge_bare.js index ac4ac20d8..d20560ade 100644 --- a/node/lib/cmd/merge_bare.js +++ b/node/lib/cmd/merge_bare.js @@ -119,12 +119,14 @@ Could not resolve ${colors.red(theirCommitName)} to a commit.`); const ourCommit = yield repo.getCommit(ourCommitish.id()); const theirCommit = yield repo.getCommit(theirCommitish.id()); + const noopEditor = function() {}; const result = yield MergeUtil.merge(repo, ourCommit, theirCommit, mode, Open.SUB_OPEN_OPTION.FORCE_BARE, - args.message); + args.message, + noopEditor); if (null !== result.errorMessage) { throw new UserError(result.errorMessage); } From f56a670be2844282ea09ad4f8496dbd1e96f5e22 Mon Sep 17 00:00:00 2001 From: Brock Peabody Date: Wed, 6 Mar 2019 10:38:12 -0500 Subject: [PATCH 243/402] Fix rebase deduction logic in pull. I think there must have been a change in the way argparse handles defaults, and where previously we were getting an `undefined` we are now getting a `null`. --- node/lib/util/pull.js | 2 +- node/test/util/pull.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/node/lib/util/pull.js b/node/lib/util/pull.js index 5b3d4993f..90f165f92 100644 --- a/node/lib/util/pull.js +++ b/node/lib/util/pull.js @@ -100,7 +100,7 @@ ${colors.red(source)} in the remote ${colors.yellow(remoteName)}.`); * @return bool */ exports.userWantsRebase = co.wrap(function*(args, repo, branch) { - if (args.rebase !== undefined) { + if (args.rebase !== undefined && args.rebase !== null) { return args.rebase; } diff --git a/node/test/util/pull.js b/node/test/util/pull.js index 0e818232d..93a7380b5 100644 --- a/node/test/util/pull.js +++ b/node/test/util/pull.js @@ -132,6 +132,10 @@ describe("userWantsRebase", function () { null, null)); + assert.equal(false, yield Pull.userWantsRebase({"rebase": null}, + repo, + master)); + assert.equal(false, yield Pull.userWantsRebase({}, repo, master)); From cb2a576f2e1c2c04bb00f9c90fb273af0bd41616 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Wed, 22 May 2019 08:20:27 -0400 Subject: [PATCH 244/402] turn off merge with bare in normal merge mode --- node/lib/cmd/merge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index 43443556d..4be42ebb1 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -173,7 +173,7 @@ Merge of '${commitName}' null, commit, mode, - Open.SUB_OPEN_OPTION.ALLOW_BARE, + Open.SUB_OPEN_OPTION.FORCE_OPEN, args.message, editMessage); if (null !== result.errorMessage) { From 60ac10bde9908ee1c061acbb7852759855e53927 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Tue, 4 Jun 2019 17:40:50 -0400 Subject: [PATCH 245/402] upgrade NodeGit to v0.23.0 --- node/lib/util/rm.js | 9 +- node/lib/util/tree_util.js | 2 +- node/package-lock.json | 2527 ++++++++++++++++++++++++++ node/package.json | 6 +- node/test/util/read_repo_ast_util.js | 2 +- 5 files changed, 2539 insertions(+), 7 deletions(-) create mode 100644 node/package-lock.json diff --git a/node/lib/util/rm.js b/node/lib/util/rm.js index 5a3abea6e..7c78a4e9e 100644 --- a/node/lib/util/rm.js +++ b/node/lib/util/rm.js @@ -194,8 +194,13 @@ const checkCleanliness = co.wrap(function *(repo, headTree, index, pathname, return yield checkIndexIsHeadOrWorkdir(repo, headTree, entry, pathname, displayPath); } - - const status = yield NodeGit.Status.file(repo, pathname); + let status; + try { + // Status.file throws errors after 0.22.0 + status = yield NodeGit.Status.file(repo, pathname); + } catch (err) { + return null; + } if (status === 0 || status & STATUS.WT_DELETED !== 0) { //git considers these OK regardless diff --git a/node/lib/util/tree_util.js b/node/lib/util/tree_util.js index 4ef86b390..f721160d5 100644 --- a/node/lib/util/tree_util.js +++ b/node/lib/util/tree_util.js @@ -226,7 +226,7 @@ exports.writeTree = co.wrap(function *(repo, baseTree, changes) { } } } - const id = builder.write(); + const id = yield builder.write(); return yield repo.getTree(id); }); return yield writeSubtree(baseTree, directory, ""); diff --git a/node/package-lock.json b/node/package-lock.json new file mode 100644 index 000000000..f431f5774 --- /dev/null +++ b/node/package-lock.json @@ -0,0 +1,2527 @@ +{ + "name": "git-meta", + "version": "0.4.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "optional": true + }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "binary-search": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.5.tgz", + "integrity": "sha512-RHFP0AdU6KAB0CCZsRMU2CJTk2EpL8GLURT+4gilpjr1f/7M91FgUMnXuQLmf3OKLet34gjuNFwO7e4agdX5pw==" + }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "~2.0.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" + }, + "child-process-promise": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz", + "integrity": "sha1-RzChHvYQ+tRQuPIjx50x172tgHQ=", + "requires": { + "cross-spawn": "^4.0.2", + "node-version": "^1.0.0", + "promise-polyfill": "^6.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + }, + "circular-json": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.9.tgz", + "integrity": "sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ==", + "dev": true + }, + "cli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", + "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", + "requires": { + "exit": "0.1.2", + "glob": "^7.1.1" + } + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "colors": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", + "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "optional": true + }, + "component-props": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/component-props/-/component-props-1.1.1.tgz", + "integrity": "sha1-+bffm5kntubZfJvScqqGdnDzSUQ=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "requires": { + "date-now": "^0.1.4" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "deepcopy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deepcopy/-/deepcopy-2.0.0.tgz", + "integrity": "sha512-d5ZK7pJw7F3k6M5vqDjGiiUS9xliIyWkdzBjnPhnSeRGjkYOGZMCFkdKVwV/WiHOe0NwzB8q+iDo7afvSf0arA==", + "requires": { + "type-detect": "^4.0.8" + } + }, + "deeper": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/deeper/-/deeper-2.1.0.tgz", + "integrity": "sha1-vFZOX3MXT98gHgiwADDooU2nQ2g=" + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + } + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", + "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, + "entities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" + }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, + "foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs-extra": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.1.2.tgz", + "integrity": "sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU=", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0" + } + }, + "fs-minipass": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz", + "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", + "requires": { + "minipass": "^2.2.1" + } + }, + "fs-promise": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fs-promise/-/fs-promise-2.0.3.tgz", + "integrity": "sha1-9k5PhUvPaJqovdy6JokW2z20aFQ=", + "requires": { + "any-promise": "^1.3.0", + "fs-extra": "^2.0.0", + "mz": "^2.6.0", + "thenify-all": "^1.6.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + }, + "group-by": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/group-by/-/group-by-0.0.1.tgz", + "integrity": "sha1-hXYgV19nFHhvjYa7Gf0T4YjdaKQ=", + "requires": { + "to-function": "*" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "handlebars": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "htmlparser2": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", + "requires": { + "domelementtype": "1", + "domhandler": "2.3", + "domutils": "1.5", + "entities": "1.0", + "readable-stream": "1.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=" + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "jshint": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.10.2.tgz", + "integrity": "sha512-e7KZgCSXMJxznE/4WULzybCMNXNAd/bf5TSrvVEq78Q/K8ZwFpmBqQeDtNiHc3l49nV4E/+YeHU/JZjSUIrLAA==", + "requires": { + "cli": "~1.0.0", + "console-browserify": "1.1.x", + "exit": "0.1.x", + "htmlparser2": "3.8.x", + "lodash": "~4.17.11", + "minimatch": "~3.0.2", + "shelljs": "0.3.x", + "strip-json-comments": "1.0.x" + }, + "dependencies": { + "shelljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", + "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=" + }, + "strip-json-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=" + } + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json5": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "minipass": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + } + } + }, + "minizlib": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "mocha": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", + "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", + "dev": true, + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.2.2", + "yargs-parser": "13.0.0", + "yargs-unparser": "1.5.0" + }, + "dependencies": { + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "mocha-jshint": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/mocha-jshint/-/mocha-jshint-2.3.1.tgz", + "integrity": "sha1-MD8n5TOThVnSDyakEj1by1ZFpUY=", + "requires": { + "jshint": "^2.8.0", + "minimatch": "^3.0.0", + "shelljs": "^0.4.0", + "uniq": "^1.0.1" + } + }, + "mocha-parallel-tests": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mocha-parallel-tests/-/mocha-parallel-tests-2.1.2.tgz", + "integrity": "sha512-FatFg3MHLio9ir1oP6J0HNEo6R5344JC1y+We90iALdiT9F9xNPN0KbGXxRNlGlSl0GodfSESKbRzBvT9ctgIw==", + "dev": true, + "requires": { + "circular-json": "^0.5.9", + "debug": "^4.1.1", + "uuid": "^3.3.2", + "yargs": "^13.2.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + }, + "needle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } + } + }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + } + }, + "node-pre-gyp": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, + "dependencies": { + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "tar": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.9.tgz", + "integrity": "sha512-xisFa7Q2i3HOgfn+nmnWLGHD6Tm23hxjkx6wwGmgxkJFr6wxwXnJOdJYcZjL453PSdF0+bemO03+flAzkIdLBQ==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.5", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + } + } + }, + "node-version": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/node-version/-/node-version-1.2.0.tgz", + "integrity": "sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ==" + }, + "nodegit": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/nodegit/-/nodegit-0.23.1.tgz", + "integrity": "sha512-k1bCg260yZlmfLGYPrinqAQNbQ6PnBJhKq6wWfe94yIO3WnOOyoSuweAkAGPR6FhrGq2LkCKHdJaitwhKCEGyA==", + "requires": { + "fs-extra": "^7.0.0", + "json5": "^2.1.0", + "lodash": "^4.17.11", + "nan": "^2.11.1", + "node-gyp": "^3.8.0", + "node-pre-gyp": "^0.11.0", + "promisify-node": "~0.3.0", + "ramda": "^0.25.0", + "request-promise-native": "^1.0.5", + "tar-fs": "^1.16.3" + }, + "dependencies": { + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + } + } + }, + "nodegit-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nodegit-promise/-/nodegit-promise-4.0.0.tgz", + "integrity": "sha1-VyKxhPLfcycWEGSnkdLoQskWezQ=", + "requires": { + "asap": "~2.0.3" + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1" + } + }, + "npm-bundled": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" + }, + "npm-packlist": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", + "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "promise-polyfill": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", + "integrity": "sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=" + }, + "promisify-node": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/promisify-node/-/promisify-node-0.3.0.tgz", + "integrity": "sha1-tLVaz5D6p9K4uQyjlomQhsAwYM8=", + "requires": { + "nodegit-promise": "~4.0.0" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "psl": { + "version": "1.1.32", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", + "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" + }, + "pump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", + "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "ramda": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.25.0.tgz", + "integrity": "sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ==" + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "request-promise-core": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", + "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "requires": { + "lodash": "^4.17.11" + } + }, + "request-promise-native": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", + "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "requires": { + "request-promise-core": "1.1.2", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shelljs": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.4.0.tgz", + "integrity": "sha1-GZ/p4t43nv0D00X/FAYlJeSzHsI=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "requires": { + "through": "2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + }, + "tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "requires": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, + "tar-fs": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", + "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", + "requires": { + "chownr": "^1.0.1", + "mkdirp": "^0.5.1", + "pump": "^1.0.0", + "tar-stream": "^1.1.2" + } + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, + "temp": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.0.tgz", + "integrity": "sha512-YfUhPQCJoNQE5N+FJQcdPz63O3x3sdT4Xju69Gj4iZe0lBKOtnAMi0SLj9xKhGkcGhsxThvTJ/usxtFPo438zQ==", + "requires": { + "rimraf": "~2.6.2" + } + }, + "thenify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", + "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, + "to-function": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/to-function/-/to-function-2.0.6.tgz", + "integrity": "sha1-fVbNnDuS+o29eyLoPVGSTedA68U=", + "requires": { + "component-props": "*" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, + "uglify-js": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", + "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "optional": true, + "requires": { + "commander": "~2.20.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + } + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "walk": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.14.tgz", + "integrity": "sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg==", + "requires": { + "foreachasync": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", + "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", + "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", + "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.11", + "yargs": "^12.0.5" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + } + } +} diff --git a/node/package.json b/node/package.json index 07621329b..282901e46 100644 --- a/node/package.json +++ b/node/package.json @@ -36,7 +36,7 @@ "deeper": "", "fs-promise": "", "group-by": "", - "nodegit": "^0.21.1", + "nodegit": "^0.23.0", "rimraf": "", "split": "", "walk": "" @@ -45,9 +45,9 @@ "deepcopy": "", "istanbul": "", "mkdirp": "", - "mocha": "^3.1.2", + "mocha": "^6.1.4", "mocha-jshint": "", - "mocha-parallel-tests": "^1.2.3", + "mocha-parallel-tests": "^2.1.2", "temp": "" } } diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index b65c5961d..254d25e92 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -410,7 +410,7 @@ describe("readRAST", function () { const r = yield NodeGit.Repository.init(path, 1); const sig = r.defaultSignature(); const builder = yield NodeGit.Treebuilder.create(r, null); - const treeObj = builder.write(); + const treeObj = yield builder.write(); const tree = yield r.getTree(treeObj.tostrS()); const commitId = yield NodeGit.Commit.create(r, 0, From f21ed7f08b56006f0e5f1215c3738a32c93f5086 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Wed, 5 Jun 2019 18:44:11 -0400 Subject: [PATCH 246/402] fix #707: failed to set workdir correctly if submodule first opened in bare mode then open with normal mode --- node/lib/util/submodule_config_util.js | 24 ++++++--- node/test/util/submodule_config_util.js | 68 ++++++++++++++++++------- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index ef3715004..68758c143 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -548,23 +548,31 @@ exports.initSubmoduleAndRepo = co.wrap(function *(repoUrl, const FLAGS = NodeGit.Repository.INIT_FLAG; + const initRepo = co.wrap(function *() { + return bare ? + yield NodeGit.Repository.init(subRepoDir, 1) : + yield NodeGit.Repository.initExt(subRepoDir, { + workdirPath: exports.computeRelativeWorkDir(name), + flags: FLAGS.NO_DOTGIT_DIR | FLAGS.MKPATH | + FLAGS.RELATIVE_GITLINK | + (null === templatePath ? 0 : FLAGS.EXTERNAL_TEMPLATE), + templatePath: templatePath + }); + }); // See if modules repo exists. let subRepo = null; try { subRepo = bare ? yield NodeGit.Repository.openBare(subRepoDir) : yield NodeGit.Repository.open(subRepoDir); + // re-init if previously opened as bare + if (!bare && subRepo.isBare()) { + subRepo = yield initRepo(); + } } catch (e) { // Or, make it if not. - - subRepo = yield NodeGit.Repository.initExt(subRepoDir, { - workdirPath: bare ? null : exports.computeRelativeWorkDir(name), - flags: FLAGS.NO_DOTGIT_DIR | FLAGS.MKPATH | - FLAGS.RELATIVE_GITLINK | - (null === templatePath ? 0 : FLAGS.EXTERNAL_TEMPLATE), - templatePath: templatePath - }); + subRepo = yield initRepo(); } if (bare) { diff --git a/node/test/util/submodule_config_util.js b/node/test/util/submodule_config_util.js index 5468cfeb9..c6a6883aa 100644 --- a/node/test/util/submodule_config_util.js +++ b/node/test/util/submodule_config_util.js @@ -639,19 +639,11 @@ foo describe("initSubmoduleAndRepo", function () { - const runTest = co.wrap(function *(repo, - subRootRepo, - url, - subName, - originUrl) { - if (undefined === originUrl) { - originUrl = ""; - } + /** Setup a simple meta repo with one submodule (not opened). */ + const setupMeta = co.wrap(function *(subRootRepo, repo, url, subName) { const subHead = yield subRootRepo.getHeadCommit(); - const submodule = yield NodeGit.Submodule.addSetup(repo, - url, - subName, - 1); + const submodule = + yield NodeGit.Submodule.addSetup(repo,url, subName, 1); const subRepo = yield submodule.open(); yield subRepo.fetchAll(); subRepo.setHeadDetached(subHead.id()); @@ -666,14 +658,26 @@ foo sig, "my message"); yield SubmoduleConfigUtil.deinit(repo, [subName]); + return subHead; + }); + + const runTest = co.wrap(function *(repo, + subRootRepo, + url, + subName, + originUrl) { + if (undefined === originUrl) { + originUrl = ""; + } + const subHead = yield setupMeta(subRootRepo, repo, url, subName); const repoPath = repo.workdir(); - const result = yield SubmoduleConfigUtil.initSubmoduleAndRepo( - originUrl, - repo, - subName, - url, - null, - false); + const result = + yield SubmoduleConfigUtil.initSubmoduleAndRepo(originUrl, + repo, + subName, + url, + null, + false); assert.instanceOf(result, NodeGit.Repository); assert(TestUtil.isSameRealPath(result.workdir(), path.join(repoPath, subName))); @@ -722,6 +726,32 @@ foo assert.equal(newUrl, url); })); + it("reset workdir if open in bare first", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const subRootRepo = yield TestUtil.createSimpleRepository(); + const url = subRootRepo.workdir(); + const originUrl = ""; + const subName = "foo"; + yield setupMeta(subRootRepo, repo, url, subName); + const newSub1 = + yield SubmoduleConfigUtil.initSubmoduleAndRepo(originUrl, + repo, + subName, + url, + null, + true); + assert.notExists(newSub1.workdir()); + const newSub2 = + yield SubmoduleConfigUtil.initSubmoduleAndRepo(originUrl, + repo, + subName, + url, + null, + false); + assert.equal(path.relative(repo.workdir(), newSub2.workdir()), + subName); + })); + it("deep name", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); const subRootRepo = yield TestUtil.createSimpleRepository(); From 4e79f8fdd3dbb6f4bac25b124f03828a9620828a Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 28 Jun 2019 13:34:39 -0400 Subject: [PATCH 247/402] message may have had a newline automatically added --- node/lib/util/synthetic_branch_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 2b8b64921..286472317 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -260,7 +260,7 @@ function* parentLoop(repo, commit, oldSha, handled) { } const ok = yield GitUtil.readNote(repo, NOTES_REF, commit.id()); - if (ok !== null && ok.message() === "ok") { + if (ok !== null && (ok.message() === "ok" || ok.message() === "ok\n")) { handled[commit.id()] = true; return true; } From 2e0df320c1ed9d7781f6c388c0b9f11ead0b8363 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 28 Jun 2019 13:41:35 -0400 Subject: [PATCH 248/402] it is ok to delete refs --- node/lib/util/synthetic_branch_util.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 286472317..f4198a4e6 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -250,6 +250,11 @@ function* parentLoop(repo, commit, oldSha, handled) { assert.isString(oldSha); assert.isObject(handled); + // deleting is OK + if (commit === "0000000000000000000000000000000000000000") { + return true; + } + if (commit.id() in handled) { return handled[commit.id()]; } From f9d0d4d5c1fdaec1d05e6bc235877dae5636408b Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 1 Jul 2019 11:40:10 -0400 Subject: [PATCH 249/402] store notes in a common location --- node/lib/util/synthetic_branch_util.js | 44 +++++++++++++++++++------- node/test/util/synthetic-branch.js | 38 ++++++++++++++++++---- 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index f4198a4e6..3126c9450 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -239,12 +239,14 @@ function* checkSubmodules(repo, commit) { * * @async * @param {NodeGit.Repostory} repo The meta repository + * @param {NodeGit.Repostory} notesRepo The repo to store notes for already + checked shas * @param {NodeGit.Commit} commit The meta branch's commit to check * @param {String} oldSha the previous (known-good) value of this ref * @param {Object} handled the commit ids that have been already * processed (and the result of processing them). */ -function* parentLoop(repo, commit, oldSha, handled) { +function* parentLoop(repo, notesRepo, commit, oldSha, handled) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); assert.isString(oldSha); @@ -264,7 +266,7 @@ function* parentLoop(repo, commit, oldSha, handled) { return true; } - const ok = yield GitUtil.readNote(repo, NOTES_REF, commit.id()); + const ok = yield GitUtil.readNote(notesRepo, NOTES_REF, commit.id()); if (ok !== null && (ok.message() === "ok" || ok.message() === "ok\n")) { handled[commit.id()] = true; return true; @@ -283,7 +285,7 @@ function* parentLoop(repo, commit, oldSha, handled) { const parents = yield commit.getParents(commit.parentcount()); const parentChecks = yield parents.map(function *(parent) { - return yield *parentLoop(repo, parent, oldSha, handled); + return yield *parentLoop(repo, notesRepo, parent, oldSha, handled); }); const result = parentChecks.every(identity); handled[commit.id()] = result; @@ -303,8 +305,9 @@ function* parentLoop(repo, commit, oldSha, handled) { * @param {String} newSha the new value of this ref */ -function* checkUpdate(repo, oldSha, newSha, handled) { +function* checkUpdate(repo, notesRepo, oldSha, newSha, handled) { assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(notesRepo, NodeGit.Repository); assert.isString(oldSha); assert.isString(newSha); assert.isObject(handled); @@ -317,10 +320,10 @@ function* checkUpdate(repo, oldSha, newSha, handled) { } const newCommit = yield repo.getCommit(newAnnotated.id()); - const success = yield parentLoop(repo, newCommit, oldSha, + const success = yield parentLoop(repo, notesRepo, newCommit, oldSha, handled); if (success) { - yield NodeGit.Note.create(repo, NOTES_REF, newCommit.committer(), + yield NodeGit.Note.create(notesRepo, NOTES_REF, newCommit.committer(), newCommit.committer(), newAnnotated.id(), "ok", 1); } @@ -333,18 +336,20 @@ function* checkUpdate(repo, oldSha, newSha, handled) { * * @async * @param {NodeGit.Repostory} repo The meta repository + * @param {NodeGit.Repostory} notesRepo The repo to store notes for already + checked shas * @param [{Object}] updates. Each object has fields oldSha, newSha, and ref, * @return true if the update should be rejected. * all strings. */ -function* metaUpdateIsBad(repo, updates) { +function* metaUpdateIsBad(repo, notesRepo, updates) { const handled = {}; const checkFailures = updates.map(function*(update) { if (!update.ref.startsWith("refs/heads/")) { return false; } - const ok = yield checkUpdate(repo, update.oldSha, update.newSha, - handled); + const ok = yield checkUpdate(repo, notesRepo, update.oldSha, + update.newSha, handled); if (!ok) { console.error( "Ref update failed synthetic branch check for " + @@ -363,11 +368,12 @@ function* metaUpdateIsBad(repo, updates) { * * @async * @param {NodeGit.Repostory} repo The meta repository + * @param {NodeGit.Repostory} repo ignored * @param [{Object}] updates. Each object has fields oldSha, newSha, and ref, * all strings. * @return true if the submodule update should be rejected */ -function* submoduleIsBad(repo, updates) { +function* submoduleIsBad(repo, notesRepo, updates) { const checkFailures = updates.map(function*(update) { /*jshint noyield:true*/ if (!update.ref.startsWith(SYNTHETIC_BRANCH_BASE)) { @@ -397,6 +403,11 @@ function* initAltOdb(repo) { } } +function* getNotesRepoPath(config) { + const configVar = "gitmeta.syntheticrefnotesrepopath"; + return (yield ConfigUtil.getConfigString(config, configVar)) || "."; +} + /** * A git pre-receive hook, which reads from stdin and checks * each updated ref. @@ -421,8 +432,19 @@ function doPreReceive(check) { }).on("end", function() { co(function *() { const repo = yield NodeGit.Repository.open("."); + + // To avoid processing the same metadata commits over and over + // again when the hook is used in multiple forks of the same + // repo, we want to store notes in the "base fork", wich + // is determined by a config setting. If no such setting exists, + // we fall back to using the current repo. + const config = yield repo.config(); + const notesRepoPath = yield getNotesRepoPath(config); + + const notesRepo = yield NodeGit.Repository.open(notesRepoPath); + yield initAltOdb(repo); - return yield check(repo, updates); + return yield check(repo, notesRepo, updates); }).then(function(res) { process.exit(+res); }, function(e) { diff --git a/node/test/util/synthetic-branch.js b/node/test/util/synthetic-branch.js index cf6c87aa0..057b029d0 100644 --- a/node/test/util/synthetic-branch.js +++ b/node/test/util/synthetic-branch.js @@ -43,6 +43,7 @@ const TestUtil = require("../../lib/util/test_util"); describe("synthetic-branch", function () { const syntheticBranch = co.wrap(function *(repos) { const x = repos.x; + const notesRepo = repos.n || repos.x; const config = yield x.config(); yield config.setString("gitmeta.subreporootpath", "../../"); yield config.setString("gitmeta.skipsyntheticrefpattern", @@ -61,7 +62,8 @@ describe("synthetic-branch", function () { value. */ } const headId = head.id().toString(); - const pass = yield SyntheticBranch.checkUpdate(x, old, headId, {}); + const pass = yield SyntheticBranch.checkUpdate(x, notesRepo, old, + headId, {}); if (!pass) { throw new UserError("fail"); } @@ -187,6 +189,28 @@ describe("synthetic-branch", function () { c.expected, syntheticBranch, c.fails); + + // Test with notes in a separate repo -- we need to create + // that repo, and make sure it's the one that gets written to and + // read from. + + let input; + if (c.input.includes("N")) { + input = c.input.replace(/N[^|]* /, "|n=B:C5-1;Bmaster=5;$&"); + } else { + input = c.input + "|n=B:C5-1;Bmaster=5"; + } + + let expected = c.expected; + if (c.expected.includes("N")) { + expected = expected.replace(/N[^|]*/, + "|n=B:C5-1;Bmaster=5;$&"); + } // else it's a failure and won't change the notes repo + + yield RepoASTTestUtil.testMultiRepoManipulator(input, + expected, + syntheticBranch, + c.fails); })); }); }); @@ -214,11 +238,11 @@ describe("synthetic-branch-submodule-pre-receive", function () { const oid = yield createCommit(repo, addFile); // an empty push succeeds - let fail = yield SyntheticBranch.submoduleIsBad(repo, []); + let fail = yield SyntheticBranch.submoduleIsBad(repo, repo, []); assert(!fail); // a push with f to a correct branch succeeds - fail = yield SyntheticBranch.submoduleIsBad(repo, [{ + fail = yield SyntheticBranch.submoduleIsBad(repo, repo, [{ oldSha: "0000000000000000000000000000000000000000", newSha: oid.toString(), ref: "refs/commits/" + oid.toString(), @@ -226,7 +250,7 @@ describe("synthetic-branch-submodule-pre-receive", function () { assert(!fail); // a push with f to a bogus branch fails - fail = yield SyntheticBranch.submoduleIsBad(repo, [{ + fail = yield SyntheticBranch.submoduleIsBad(repo, repo, [{ oldSha: "0000000000000000000000000000000000000000", newSha: oid.toString(), ref: "refs/commits/0000000000000000000000000000000000000000", @@ -308,7 +332,7 @@ describe("synthetic-branch-meta-pre-receive", function () { process.env.GIT_ALTERNATE_OBJECT_DIRECTORIES = metaObjects; //fail: no synthetic ref - let fail = yield SyntheticBranch.metaUpdateIsBad(repo, [{ + let fail = yield SyntheticBranch.metaUpdateIsBad(repo, repo, [{ ref: "refs/heads/example", oldSha: "0000000000000000000000000000000000000000", newSha: metaOid.toString() @@ -320,7 +344,7 @@ describe("synthetic-branch-meta-pre-receive", function () { subOid, 0, "create synthetic ref"); //fail: no alt odb - fail = yield SyntheticBranch.metaUpdateIsBad(repo, [{ + fail = yield SyntheticBranch.metaUpdateIsBad(repo, repo, [{ ref: "refs/heads/example", oldSha: "0000000000000000000000000000000000000000", newSha: metaOid.toString() @@ -329,7 +353,7 @@ describe("synthetic-branch-meta-pre-receive", function () { //pass yield SyntheticBranch.initAltOdb(repo); - fail = yield SyntheticBranch.metaUpdateIsBad(repo, [{ + fail = yield SyntheticBranch.metaUpdateIsBad(repo, repo, [{ ref: "refs/heads/example", oldSha: "0000000000000000000000000000000000000000", newSha: metaOid.toString() From 863bbe90f3521f9096b04a6ce439875b1b883127 Mon Sep 17 00:00:00 2001 From: Ang Lee Date: Tue, 30 Jul 2019 17:52:34 -0400 Subject: [PATCH 250/402] Exec pre-commit of each submodule before start commiting. Add an option --no-verify for bypassing pre-commit hooks Caveats: * pre-commit hooks is not supported with option [--interactive] --- node/lib/cmd/commit.js | 46 +++++++++++++-- node/lib/util/commit.js | 117 ++++++++++++++++++++++++++++++++++++++- node/lib/util/hook.js | 29 +++++++--- node/package-lock.json | 116 ++++++++++++++++++++++++++++++++++++++ node/package.json | 1 + node/test/util/commit.js | 116 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 409 insertions(+), 16 deletions(-) diff --git a/node/lib/cmd/commit.js b/node/lib/cmd/commit.js index 03fd6ab8b..7f33fea3b 100644 --- a/node/lib/cmd/commit.js +++ b/node/lib/cmd/commit.js @@ -84,6 +84,13 @@ have matching commits and have no new commits.`, constant: true, help: `When amending, reuse previous messages without editing.`, }); + parser.addArgument(["--no-verify"], { + required: false, + action: "storeConst", + defaultValue: false, + constant: true, + help: `This option bypasses the pre-commit hooks.`, + }); parser.addArgument(["-i", "--interactive"], { required: false, action: "storeConst", @@ -108,11 +115,24 @@ the URL of submodules).`, const doCommit = co.wrap(function *(args) { const Commit = require("../util/commit"); const GitUtil = require("../util/git_util"); - const Hook = require("../util/hook"); + const Hook = require("../util/hook"); const repo = yield GitUtil.getCurrentRepo(); const cwd = process.cwd(); + if (!args.no_verify) { + try { + yield Commit.execSubmodulePrecommitHooks(repo, + cwd, + args.all, + args.file, + args.interactive); + } catch (e) { + console.error("Failed to commit. \n" + e.message); + process.exit(1); + } + } + yield Commit.doCommitCommand(repo, cwd, args.message, @@ -124,10 +144,10 @@ const doCommit = co.wrap(function *(args) { }); const doAmend = co.wrap(function *(args) { - const Commit = require("../util/commit"); - const GitUtil = require("../util/git_util"); - const Hook = require("../util/hook"); - const UserError = require("../util/user_error"); + const Commit = require("../util/commit"); + const GitUtil = require("../util/git_util"); + const Hook = require("../util/hook"); + const UserError = require("../util/user_error"); const usingPaths = 0 !== args.file.length; @@ -136,9 +156,23 @@ const doAmend = co.wrap(function *(args) { } const repo = yield GitUtil.getCurrentRepo(); + const cwd = process.cwd(); + + if (!args.no_verify) { + try { + yield Commit.execSubmodulePrecommitHooks(repo, + cwd, + args.all, + args.file, + args.interactive); + } catch (e) { + console.error("Failed to commit. \n" + e.message); + process.exit(1); + } + } yield Commit.doAmendCommand(repo, - process.cwd(), + cwd, args.message, args.all, args.interactive, diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index a37e7e1b2..5b1c37def 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -358,7 +358,7 @@ exports.formatEditorPrompt = function (status, cwd) { * `index`. We need to do this whenever generating a meta-repo commit because * otherwise, we could commit a staged commit in a submodule that would have * been reverted in its open repo. - * + * * @param {NodeGit.Repository} repo * @param {NodeGit.Index} index * @param {Object} submodules name -> RepoStatus.Submodule @@ -1467,7 +1467,7 @@ exports.calculatePathCommitStatus = function (current, requested) { * impossible to ignore or target those commits. We can't use them with * configuration changes due to the complexity of manipulating the * `.gitmodules` file. - * + * * TODO: * (a) Consider allowing previously-staged commits to be included with a * flag. @@ -1631,7 +1631,7 @@ exports.calculateAllRepoStatus = function (normalStatus, toWorkdirStatus) { * provided (default false). Restrict status to the specified `paths` if * nonempty (default []), using the specified `cwd` to resolve their meaning. * The behavior undefined unless `0 === paths.length || !all`. - * + * * @param {NodeGit.Repository} repo * @param {String} cwd * @param {Object} [options] @@ -2222,3 +2222,114 @@ it empty. You can remove the commit entirely with "git meta reset HEAD^".`); message, subMessages); }); + +const submoduleHasChangesToCommit = function(status, submoduleName) { + const submodule = status.submodules[submoduleName]; + if (!submodule) { + return false; + } + const { workdir } = submodule; + return null !== workdir && !workdir.status.isIndexClean(); +}; + +const submoduleHasPrecommitHook = co.wrap(function *(repo, submoduleName) { + const Hook = require("../util/hook"); + const SubmoduleUtil = require("../util/submodule_util"); + + const subRepo = yield SubmoduleUtil.getRepo(repo, submoduleName); + return Hook.hasHook(subRepo, "pre-commit"); +}); + +const filterInParallel = co.wrap(function *(collection, asyncPredicate) { + const DoWorkQueue = require("../util/do_work_queue"); + + const booleans = yield DoWorkQueue.doInParallel(collection, + asyncPredicate); + return collection.filter((item, index) => booleans[index]); +}); + +/** + * Get the names of submodules that should run 'pre-commit' hook against. + * This means the submodule must have a 'pre-commit' hook as well as have + * changes to commit + * + * @async + * @param {NodeGit.Repository} repo + * @param {RepoStatus} repoStatus + * @return {String[]} + */ +exports.getSubmoduleNamesForPrecommitCheck = co.wrap(function *(repo, + repoStatus) { + const SubmoduleUtil = require("../util/submodule_util"); + + const allSubmoduleNames = yield SubmoduleUtil.getSubmoduleNames(repo); + return yield filterInParallel( + allSubmoduleNames, + co.wrap(function *(submoduleName) { + return submoduleHasChangesToCommit(repoStatus, submoduleName) && + (yield submoduleHasPrecommitHook(repo, submoduleName)); + }) + ); +}); + +/** + * Execute 'pre-commit' hooks of submodules. Only execute when a submodule + * has the hook and has changes to commit. + * If there is an error occurred, throw an error + * If there are hooks to be run and interactive mode is on, a warning will be + * raised to communicate that 'pre-commit' hooks were skipped. + * + * @async + * @param {NodeGit.Repository} repo + * @param {String} cwd + * @param {Boolean} all + * @param {String[]} paths + * @param {Boolean} interactive + * @return {void} + */ +exports.execSubmodulePrecommitHooks = co.wrap(function *(repo, + cwd, + all, + paths, + interactive) { + const Hook = require("../util/hook"); + const SubmoduleUtil = require("../util/submodule_util"); + + const repoStatus = yield exports.getCommitStatus(repo, cwd, { + all, + paths, + }); + + const submoduleNames = yield exports.getSubmoduleNamesForPrecommitCheck( + repo, + repoStatus + ); + + if (submoduleNames.length > 0) { + if (interactive) { + // "interactive mode is not supported because pre-commit hooks + // happen before we offer the commit message for editing, + // and in interactive mode, we don't know which hooks actually + // need to be run until the editing is done." + console.warn( + "Warning. pre-commit hooks skipped when using option [-i]" + ); + } else { + // execute pre-commit hook for each submodule + for (const submoduleName of submoduleNames) { + const subRepo = yield SubmoduleUtil.getRepo( + repo, + submoduleName + ); + const isOk = yield Hook.execHook(subRepo, "pre-commit"); + if (!isOk) { + throw new Error( + "Error occurred when running pre-commit hook of " + + "submodule: " + + submoduleName + ); + } + } + } + } +}); diff --git a/node/lib/util/hook.js b/node/lib/util/hook.js index 68aea17c3..10e0295d3 100644 --- a/node/lib/util/hook.js +++ b/node/lib/util/hook.js @@ -37,13 +37,29 @@ const path = require("path"); const process = require("process"); const fs = require("fs"); +/** + * Check if git-meta hook with given hook name exists + * Return true if hook exists. + * @param {String} name + * @return {Boolean} + */ +exports.hasHook = function (repo, name) { + assert.isString(name); + + const rootDirectory = repo.path(); + const hookPath = path.join(rootDirectory, "hooks"); + const absPath = path.resolve(hookPath, name); + + return fs.existsSync(absPath); +}; + /** * Run git-meta hook with given hook name. - * Return 0 on success, non-zero status otherwise. + * Return true on success or hook does not exist, false otherwise. * @async * @param {String} name * @param {String[]} args - * @return {int} + * @return {Boolean} */ exports.execHook = co.wrap(function*(repo, name, args=[]) { assert.isString(name); @@ -53,20 +69,19 @@ exports.execHook = co.wrap(function*(repo, name, args=[]) { const absPath = path.resolve(hookPath, name); if (!fs.existsSync(absPath)) { - return -1; + return true; } try { process.chdir(repo.workdir()); - const result = yield spawn(absPath, args, { stdio: "inherit" }); - return result.status; + yield spawn(absPath, args, { stdio: "inherit" }); + return true; } catch (e) { if (e.code === "EACCES") { console.log("EACCES: Cannot execute: " + absPath); - return e.stderr; } else if (e.stdout) { console.log(e.stdout); - return e.stdout; } + return false; } }); diff --git a/node/package-lock.json b/node/package-lock.json index f431f5774..11ff10af4 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -4,6 +4,42 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@sinonjs/commons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", + "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.2.tgz", + "integrity": "sha512-ILO/rR8LfAb60Y1Yfp9vxfYAASK43NFC2mLzpvLUbCQY/Qu8YwReboseu8aheCEkyElZF2L2T9mHcR2bgdvZyA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -73,6 +109,12 @@ "sprintf-js": "~1.0.2" } }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -1204,6 +1246,12 @@ "verror": "1.10.0" } }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, "lcid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", @@ -1246,6 +1294,12 @@ "chalk": "^2.0.1" } }, + "lolex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.1.0.tgz", + "integrity": "sha512-BYxIEXiVq5lGIXeVHnsFzqa1TxN5acnKnPCdlZSpzm8viNEOhiigupA4vTQ9HEFQ6nLTQ9wQOgBknJgzUYQ9Aw==", + "dev": true + }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -1483,6 +1537,19 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.0.tgz", + "integrity": "sha512-Z3sfYEkLFzFmL8KY6xnSJLRxwQwYBjOXi/24lb62ZnZiGA0JUzGGTI6TBIgfCSMIDl9Jlu8SRmHNACLTemDHww==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^4.1.0", + "path-to-regexp": "^1.7.0" + } + }, "node-environment-flags": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", @@ -1833,6 +1900,23 @@ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, "pathval": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", @@ -2045,6 +2129,38 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "sinon": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz", + "integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.1", + "diff": "^3.5.0", + "lolex": "^4.0.1", + "nise": "^1.4.10", + "supports-color": "^5.5.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "source-map": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", diff --git a/node/package.json b/node/package.json index 282901e46..674b15767 100644 --- a/node/package.json +++ b/node/package.json @@ -48,6 +48,7 @@ "mocha": "^6.1.4", "mocha-jshint": "", "mocha-parallel-tests": "^2.1.2", + "sinon": "^7.3.2", "temp": "" } } diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 599675a9c..81c0041c2 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -32,6 +32,7 @@ const assert = require("chai").assert; const co = require("co"); +const sinon = require("sinon"); const NodeGit = require("nodegit"); const path = require("path"); @@ -43,6 +44,7 @@ const StatusUtil = require("../../lib/util/status_util"); const SubmoduleUtil = require("../../lib/util/submodule_util"); const TestUtil = require("../../lib/util/test_util"); const UserError = require("../../lib/util/user_error"); +const Hook = require("../../lib/util/hook"); function mapCommitResult(commitResult) { @@ -3284,4 +3286,118 @@ x=U:Cbar\n#x-2 s=Sa:s;Bmaster=x;Os Cbar\n#s-1 README.md=foo, a=b`, })); }); }); + + describe("execSubmodulePrecommitHooks", () => { + let sandbox; + const noop = () => Promise.resolve(); + const repoMap = { + a: "submodule-a", + b: "submodule-b" + }; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it( + "should run 'pre-commit' hooks of submodules", + co.wrap(function*() { + sandbox + .stub(Commit, "getSubmoduleNamesForPrecommitCheck") + .callsFake(() => ["a", "b"]); + sandbox.stub(Commit, "getCommitStatus").callsFake(noop); + sandbox + .stub(SubmoduleUtil, "getRepo") + .callsFake((repo, submoduleName) => + Promise.resolve(repoMap[submoduleName]) + ); + + const execHookSpy = sandbox + .stub(Hook, "execHook") + .callsFake(() => Promise.resolve(true)); + yield Commit.execSubmodulePrecommitHooks(); + assert.isTrue(execHookSpy.calledWith(repoMap.a)); + assert.isTrue(execHookSpy.calledWith(repoMap.b)); + }) + ); + it("should throw if execHook return false", function() { + sandbox + .stub(Commit, "getSubmoduleNamesForPrecommitCheck") + .callsFake(() => ["a", "b"]); + sandbox.stub(Commit, "getCommitStatus").callsFake(noop); + sandbox + .stub(SubmoduleUtil, "getRepo") + .callsFake((repo, submoduleName) => + Promise.resolve(repoMap[submoduleName]) + ); + + sandbox.stub(Hook, "execHook").callsFake((repo, name) => { + assert.isTrue("pre-commit" === name); + return Promise.resolve(repo !== repoMap.b); // fail on 'b' + }); + + return Commit.execSubmodulePrecommitHooks().catch(err => { + assert.instanceOf(err, Error); + assert( + err.message, + "Error occurred when running pre-commit hook of " + + "submodule: b" + ); + }); + }); + + describe("interactive mode", () => { + const execSubmodulePrecommitHooksInteractive = () => + Commit.execSubmodulePrecommitHooks( + null, + null, + null, + null, + true + ); + it( + "should warn if there are hooks to run but skipped", + co.wrap(function*() { + sandbox + .stub(Commit, "getSubmoduleNamesForPrecommitCheck") + .callsFake(() => ["a", "b"]); + sandbox.stub(Commit, "getCommitStatus").callsFake(noop); + sandbox + .stub(SubmoduleUtil, "getRepo") + .callsFake((repo, submoduleName) => + Promise.resolve(repoMap[submoduleName]) + ); + + const consoleWarnSpy = sandbox + .stub(console, "warn") + .callsFake(noop); + yield execSubmodulePrecommitHooksInteractive(); + assert.isTrue( + consoleWarnSpy.calledWith( + "Warning. pre-commit hooks skipped when using " + + "option [-i]" + ) + ); + }) + ); + it( + "should not warn if there is no hooks", + co.wrap(function*() { + sandbox + .stub(Commit, "getSubmoduleNamesForPrecommitCheck") + .callsFake(() => []); + sandbox.stub(Commit, "getCommitStatus").callsFake(noop); + const consoleWarnSpy = sandbox + .stub(console, "warn") + .callsFake(noop); + yield execSubmodulePrecommitHooksInteractive(); + assert.isFalse(consoleWarnSpy.called); + }) + ); + }); + }); }); From 731f3a226ba14998a2426637765379a71bfd5702 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 31 Jul 2019 13:41:25 -0400 Subject: [PATCH 251/402] retry certain push failures --- node/lib/util/git_util.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 896c63753..ac839a226 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -360,7 +360,13 @@ exports.push = co.wrap(function *(repo, remote, source, target, force, quiet) { const execString = `\ git -C '${repo.path()}' push ${forceStr} ${remote} ${source}:${target}`; try { - const result = yield ChildProcess.exec(execString, {env : environ}); + let result = yield ChildProcess.exec(execString, {env : environ}); + if (result.error && + result.stderr.indexOf("reference already exists") !== -1) { + // GitLab has a race condition that somehow causes this to + // happen spuriously -- let's retry. + result = yield ChildProcess.exec(execString, {env : environ}); + } if (result.error || !quiet) { if (result.stdout) { console.log(result.stdout); From d534534c58c577456d87c2d1350c3693e3795a9c Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 23 Aug 2019 16:04:52 -0400 Subject: [PATCH 252/402] support nested urls in push Nested urls, like cache::https://example.com/my/repo, break node's url parsing. So we have to manually resolve relative urls. --- node/lib/util/submodule_config_util.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index 68758c143..cd1638c6e 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -255,7 +255,16 @@ exports.resolveUrl = function (baseUrl, relativeUrl) { if (!baseUrl.endsWith("/")) { baseUrl += "/"; } - const res = url.resolve(baseUrl, relativeUrl); + + // Handle prefixed urls like cache::https://... + const prefixStart = baseUrl.lastIndexOf("::"); + let prefix = ""; + if (prefixStart !== -1) { + prefix = baseUrl.substring(0, prefixStart + 2); + baseUrl = baseUrl.substring(prefixStart + 2); + } + + const res = prefix + url.resolve(baseUrl, relativeUrl); // Trim trailing "/" which will stick around in some situations (e.g., "." // for relativeUrl) but not others, to give uniform results. From 02f99480d68e20e9b6553f76707e3812b40be381 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 4 Sep 2019 14:33:55 -0400 Subject: [PATCH 253/402] allow the synthetic ref check to exclude by path --- node/lib/util/synthetic_branch_util.js | 56 +++++++++++++++++++++----- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 3126c9450..3d5a9ec61 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -54,14 +54,23 @@ function identity(v) { return v; } -function SyntheticBranchConfig(whitelistPattern) { - if (whitelistPattern.length > 0) { - const whitelistRE = new RegExp(whitelistPattern); - this.whitelistTest = function(url) { - return whitelistRE.test(url); +function SyntheticBranchConfig(urlWhitelistPattern, pathWhitelistPattern) { + if (urlWhitelistPattern.length > 0) { + const urlWhitelistRE = new RegExp(urlWhitelistPattern); + this.urlWhitelistTest = function(url) { + return urlWhitelistRE.test(url); }; } else { - this.whitelistTest = function() { return false; }; + this.urlWhitelistTest = function() { return false; }; + } + + if (pathWhitelistPattern.length > 0) { + const pathWhitelistRE = new RegExp(pathWhitelistPattern); + this.pathWhitelistTest = function(path) { + return pathWhitelistRE.test(path); + }; + } else { + this.pathWhitelistTest = function() { return false; }; } } @@ -107,6 +116,21 @@ exports.urlToLocalPath = function *(repo, url) { } }; +/** + * Check that a given path is on the path synthetic-ref-check whitelist, if + * such a whitelist exists. + * @async + * @param {SyntheticBranchConfig} cfg The configuration for + * synthetic_branch_util + * @param {String} url The path of the submodule + * in the meta tree. + */ +function skipCheckForPath(cfg, path) { + assert.instanceOf(cfg, SyntheticBranchConfig); + assert.isString(path); + return cfg.pathWhitelistTest(path); +} + /** * Check that a given URL is on the URLs synthetic-ref-check whitelist, if * such a whitelist exists. @@ -119,7 +143,7 @@ exports.urlToLocalPath = function *(repo, url) { function skipCheckForURL(cfg, url) { assert.instanceOf(cfg, SyntheticBranchConfig); assert.isString(url); - return cfg.whitelistTest(url); + return cfg.urlWhitelistTest(url); } /** @@ -133,7 +157,7 @@ function skipCheckForURL(cfg, url) { * @param {String} url the configured URL of the submodule * in the meta tree. */ -function* checkSubmodule(repo, cfg, metaCommit, submoduleEntry, url) { +function* checkSubmodule(repo, cfg, metaCommit, submoduleEntry, url, path) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(cfg, SyntheticBranchConfig); assert.instanceOf(submoduleEntry, NodeGit.TreeEntry); @@ -142,6 +166,10 @@ function* checkSubmodule(repo, cfg, metaCommit, submoduleEntry, url) { return true; } + if (skipCheckForPath(cfg, path)) { + return true; + } + const localPath = yield *exports.urlToLocalPath(repo, url); const submoduleRepo = yield NodeGit.Repository.open(localPath); const submoduleCommitId = submoduleEntry.id(); @@ -184,10 +212,15 @@ function* checkSubmodules(repo, commit) { assert.instanceOf(commit, NodeGit.Commit); const config = yield repo.config(); - const whitelistPattern = ( + const urlWhitelistPattern = ( yield ConfigUtil.getConfigString( config, "gitmeta.skipsyntheticrefpattern")) || ""; - const cfg = new SyntheticBranchConfig(whitelistPattern); + const pathWhitelistPattern = ( + yield ConfigUtil.getConfigString( + config, "gitmeta.skipsyntheticrefpathpattern")) || ""; + + const cfg = new SyntheticBranchConfig(urlWhitelistPattern, + pathWhitelistPattern); const parent = yield GitUtil.getParentCommit(repo, commit); const names = yield computeChangedSubmodules(repo, @@ -222,7 +255,8 @@ function* checkSubmodules(repo, commit) { return false; } const url = submodule.url; - return yield *checkSubmodule(repo, cfg, commit, entry, url); + return yield *checkSubmodule(repo, cfg, commit, entry, url, + submodulePath); }); return (yield result).every(identity); }); From 6c84d121e6091b47d705f36fdad711d94d0aaf1c Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 6 Sep 2019 12:39:42 -0400 Subject: [PATCH 254/402] refuse to open unmerged submodules, like git does --- node/lib/cmd/open.js | 5 +++++ node/lib/util/submodule_util.js | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 7f5ef9427..5742b4385 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -118,6 +118,11 @@ exports.executeableSubcommand = co.wrap(function *(args) { return; // RETURN } + if (shas[index] === null) { + console.warn(`Skipping unmerged submodule ${colors.cyan(name)}`); + return; // RETURN + } + console.log(`\ Opening ${colors.blue(name)} on ${colors.green(shas[index])}.`); diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 43c7e95ce..d0b0ce677 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -196,7 +196,8 @@ exports.getCurrentSubmoduleShas = function (index, submoduleNames) { if (entry) { result.push(entry.id.tostrS()); } else { - result.push(`${colors.red("missing entry")}`); + // Probably a merge conflict + result.push(null); } } return result; From 38c5e9e17adadacca44951c08c4da654dcd770c1 Mon Sep 17 00:00:00 2001 From: jhdub23 Date: Fri, 6 Sep 2019 16:27:09 -0700 Subject: [PATCH 255/402] doc: Replaced refs/meta with refs/commits to reflect code behavior. --- doc/architecture.md | 94 ++++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/doc/architecture.md b/doc/architecture.md index e984b2b9f..356500080 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -328,7 +328,7 @@ of this strategy on client-side checkouts. A synthetic-meta-ref is a ref in a sub-repo whose name includes the commit ID of the commit to which it points, such as: -`refs/meta/929e8afc03fef8d64249ad189341a4e8889561d7`. The term is derived from +`refs/commits/929e8afc03fef8d64249ad189341a4e8889561d7`. The term is derived from the fact that such a ref is: 1. _synthetic_ -- generated by a tool @@ -347,23 +347,23 @@ A mono-repo has two invariants with respect to synthetic-meta-refs: Some mono-repos in valid states: ``` -'-------------------` '-------------------` -| meta-repo | | a | -| - - - - - - - - - | | - - - - - - - - - | -| master | a [a1] | | refs/meta/a1 [a1] | -`-------------------, `-------------------, +'-------------------` '-----------------------` +| meta-repo | | a | +| - - - - - - - - - | | - - - - - - - - - - - | +| master | a [a1] | | refs/commits/a1 [a1] | +`-------------------, `-----------------------, The 'master' branch in the meta-repo indicates commit 'a1' for repo 'a' and a valid synthetic-meta-ref exists. ``` ``` -'-------------------` '-------------------` -| meta-repo | | a | -| - - - - - - - - - | | - - - - - - - - - | -| master | a [a1] | | refs/meta/a1 [a1] | -| - - - - -+- - - - | | refs/meta/ab [ab] | -| release | a [ab] | `-------------------, +'-------------------` '-----------------------` +| meta-repo | | a | +| - - - - - - - - - | | - - - - - - - - - - - | +| master | a [a1] | | refs/commits/a1 [a1] | +| - - - - -+- - - - | | refs/commits/ab [ab] | +| release | a [ab] | `-----------------------, `-------------------, The meta-repo has another branch, 'release', indicating commit 'ab' in 'a', @@ -371,11 +371,11 @@ which also has a valid synthetic-meta-ref. ``` ``` -'-----------------------` '-------------------` -| meta-repo | | a | -| - - - - - - - - - - - | | - - - - - - - - - | -| master | a [a1] | | refs/meta/a2 [a2] | -| - - - - -+- - - - - - | `-------------------, +'-----------------------` '-----------------------` +| meta-repo | | a | +| - - - - - - - - - - - | | - - - - - - - - - - - | +| master | a [a1] | | refs/commits/a2 [a2] | +| - - - - -+- - - - - - | `-----------------------, | release | a [a2->a1] | `-----------------------, @@ -387,31 +387,31 @@ for 'a1'. A few mono-repos in invalid states: ``` -'-------------------` '-------------------` -| meta-repo | | a | -| - - - - - - - - - | | - - - - - - - - - | -| master | a [a1] | | refs/meta/a1 [a2] | -`-------------------, `-------------------, +'-------------------` '-----------------------` +| meta-repo | | a | +| - - - - - - - - - | | - - - - - - - - - - - | +| master | a [a1] | | refs/commits/a1 [a2] | +`-------------------, `-----------------------, The synthetic-meta-ref for 'a1' does not point to 'a1'. ``` ``` -'-------------------` '-------------------` -| meta-repo | | a | -| - - - - - - - - - | | - - - - - - - - - | -| master | a [a1] | | refs/meta/ab [ab] | -`-------------------, `-------------------, +'-------------------` '-----------------------` +| meta-repo | | a | +| - - - - - - - - - | | - - - - - - - - - - - | +| master | a [a1] | | refs/commits/ab [ab] | +`-------------------, `-----------------------, No synthetic-meta-ref for commit 'a1'. ``` ``` -'-----------------------` '-------------------` -| meta-repo | | a | -| - - - - - - - - - - - | | - - - - - - - - - | -| master | a [a1] | | refs/meta/a1 [a1] | -| - - - - -+- - - - - - | `-------------------, +'-----------------------` '-----------------------` +| meta-repo | | a | +| - - - - - - - - - - - | | - - - - - - - - - - - | +| master | a [a1] | | refs/commits/a1 [a1] | +| - - - - -+- - - - - - | `-----------------------, | release | a [a2->a1] | `-----------------------, @@ -458,32 +458,32 @@ local `---------------------------------, remote -'---------------------` '--------------` '--------------` -| meta-repo | | | a | | b | -| master | a [a1] | | refs/meta/a1 | | refs/meta/b1 | -| [m1] | b [b1] | | [a1] | | [b1] | -`---------------------, `--------------, `--------------, +'---------------------` '-----------------` '-----------------` +| meta-repo | | | a | | b | +| master | a [a1] | | refs/commits/a1 | | refs/commits/b1 | +| [m1] | b [b1] | | [a1] | | [b1] | +`---------------------, `-----------------, `-----------------, ``` Where we have new commits, `a2` and `b2` in repos `a` and `b`, respectively, and a new meta-repo commit, `m2` that references them. Note that `a1` and `b1` -have appropriate synthetic-meta-refs +have appropriate synthetic-meta-refs After invoking `git meta push`, the remote repos would look like: ``` -'---------------------` '--------------` '--------------` -| meta-repo | | | a | | b | -| master | a [a2] | | refs/meta/a1 | | refs/meta/b1 | -| [m2] | b [b2] | | [a1] | | [b1] | -`---------------------, | refs/meta/a2 | | refs/meta/b2 | - | [a2] | | [b2] | - `--------------, `--------------, +'---------------------` '-----------------` '-----------------` +| meta-repo | | | a | | b | +| master | a [a2] | | refs/commits/a1 | | refs/commits/b1 | +| [m2] | b [b2] | | [a1] | | [b1] | +`---------------------, | refs/commits/a2 | | refs/commits/b2 | + | [a2] | | [b2] | + `-----------------, `-----------------, ``` Note that `git meta push` created meta-refs in the sub-repos for the new commits before it updated the meta-repo. If the process had been interrupted, -for example, after pushing `refs/meta/a2` but before pushing `refs/meta/b2`, +for example, after pushing `refs/commits/a2` but before pushing `refs/commits/b2`, the mono-repo would still be in a valid state. If no meta-repo commit ever -referenced `a2`, the synthetic-meta-ref `refs/meta/a2` would eventually be +referenced `a2`, the synthetic-meta-ref `refs/commits/a2` would eventually be cleand up. ### Client-side access to sub-repo commits From 4f2a656b073dc6f6c2d6c11399da5ceffb4a2117 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 6 Sep 2019 11:13:25 -0400 Subject: [PATCH 256/402] remove docstring for unused parameter --- node/lib/util/write_repo_ast_util.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index ffa8a5e8b..d74b71e0c 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -77,12 +77,10 @@ const configRepo = co.wrap(function *(repo) { * Return the tree and a map of associated subtrees corresponding to the * specified `changes` in the specified `repo`, and based on the optionally * specified `parent`. Use the specified `shaMap` to resolve logical shas to - * actual written shas (such as for submodule heads). Use the specified `db` - * to write objects. + * actual written shas (such as for submodule heads). * * @async * @param {NodeGit.Repository} repo - * @param {NodeGit.Odb} db * @param {Object} shaMap maps logical to physical ID * @param {Object} changes map of changes * @param {Object} [parent] From d40a88598072c5156d124e6e58432844ec05b8d2 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 6 Sep 2019 11:57:17 -0400 Subject: [PATCH 257/402] do not crash in status on empty repo --- node/lib/util/print_status_util.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/node/lib/util/print_status_util.js b/node/lib/util/print_status_util.js index eb9ce66ec..157583ebb 100644 --- a/node/lib/util/print_status_util.js +++ b/node/lib/util/print_status_util.js @@ -387,6 +387,9 @@ exports.printCurrentBranch = function (status) { if (null !== status.currentBranchName) { return `On branch ${colors.green(status.currentBranchName)}.\n`; } + if (status.headCommit === null) { + return `No commits yet\n`; + } return `\ On detached head ${colors.red(GitUtil.shortSha(status.headCommit))}.\n`; }; From 3e16822ec07479ae4b153705c7008dd1aeee3b7e Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 9 Sep 2019 16:33:09 -0400 Subject: [PATCH 258/402] rename variable to avoid shadowing --- node/lib/cmd/open.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 5742b4385..dc6967b5a 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -112,19 +112,19 @@ exports.executeableSubcommand = co.wrap(function *(args) { const templatePath = yield SubmoduleConfigUtil.getTemplatePath(repo); - const opener = co.wrap(function *(name, index) { + const opener = co.wrap(function *(name, idx) { if (openSubs.has(name)) { console.warn(`Submodule ${colors.cyan(name)} is already open.`); return; // RETURN } - if (shas[index] === null) { + if (shas[idx] === null) { console.warn(`Skipping unmerged submodule ${colors.cyan(name)}`); return; // RETURN } console.log(`\ -Opening ${colors.blue(name)} on ${colors.green(shas[index])}.`); +Opening ${colors.blue(name)} on ${colors.green(shas[idx])}.`); // If we fail to open due to an expected condition, indicated by // the throwing of a `UserError` object, catch and log the error, @@ -134,7 +134,7 @@ Opening ${colors.blue(name)} on ${colors.green(shas[index])}.`); try { yield Open.openOnCommit(fetcher, name, - shas[index], + shas[idx], templatePath, false); subsOpenSuccessfully.push(name); From a16faefcff9963e431c84547e15173a89bcd4179 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 13 Sep 2019 13:23:36 -0400 Subject: [PATCH 259/402] Write notes for all parents and simplify the synthetic branch check to no longer recurse in parallel. Recusing in parallel can create exponentially-many threads; it seems to be extremely ineffecient. Writing for all parents makes future lookups faster. --- node/lib/util/synthetic_branch_util.js | 36 ++++++++++++++------------ node/test/util/synthetic-branch.js | 8 +++++- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 3d5a9ec61..b174cb6c6 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -271,6 +271,9 @@ function* checkSubmodules(repo, commit) { * Returns true if the check passes on all branches; false if any * fail. * + * On success, create a note reflecting the work done to save time + * on future updates. + * @async * @param {NodeGit.Repostory} repo The meta repository * @param {NodeGit.Repostory} notesRepo The repo to store notes for already @@ -318,21 +321,26 @@ function* parentLoop(repo, notesRepo, commit, oldSha, handled) { } const parents = yield commit.getParents(commit.parentcount()); - const parentChecks = yield parents.map(function *(parent) { - return yield *parentLoop(repo, notesRepo, parent, oldSha, handled); - }); - const result = parentChecks.every(identity); - handled[commit.id()] = result; - return result; + let success = true; + for (const parent of parents) { + if (!(yield *parentLoop(repo, notesRepo, parent, oldSha, handled))) { + success = false; + break; + } + } + if (success) { + yield NodeGit.Note.create(notesRepo, NOTES_REF, commit.committer(), + commit.committer(), commit.id(), + "ok", 1); + } + handled[commit.id()] = success; + return success; } /** * Main entry point. Check that a proposed ref update from oldSha * to newSha has synthetic branches for all submodule updates. * - * On success, create a note reflecting the work done to save time - * on future updates. - * * @async * @param {NodeGit.Repostory} repo The meta repository * @param {String} oldSha the previous (known-good) value of this ref @@ -354,14 +362,8 @@ function* checkUpdate(repo, notesRepo, oldSha, newSha, handled) { } const newCommit = yield repo.getCommit(newAnnotated.id()); - const success = yield parentLoop(repo, notesRepo, newCommit, oldSha, - handled); - if (success) { - yield NodeGit.Note.create(notesRepo, NOTES_REF, newCommit.committer(), - newCommit.committer(), newAnnotated.id(), - "ok", 1); - } - return success; + return yield parentLoop(repo, notesRepo, newCommit, oldSha, + handled); } /** diff --git a/node/test/util/synthetic-branch.js b/node/test/util/synthetic-branch.js index 057b029d0..8fab5c59b 100644 --- a/node/test/util/synthetic-branch.js +++ b/node/test/util/synthetic-branch.js @@ -73,7 +73,8 @@ describe("synthetic-branch", function () { "simplest": { input: "x=S:C8-2;C2-1;Bmaster=8", expected: "x=S:C8-2;C2-1;Bmaster=8;" + - "N refs/notes/git-meta/subrepo-check 8=ok", + "N refs/notes/git-meta/subrepo-check 8=ok;" + + "N refs/notes/git-meta/subrepo-check 2=ok", }, "read a note, do nothing": { input: "x=S:C8-2;C2-1;Bmaster=8;" + @@ -136,6 +137,7 @@ describe("synthetic-branch", function () { input: "x=S:C2-1;C3-2 y=S/y:4;Bmaster=3|" + "y=S:C4-1;Bmaster=4", expected: "x=S:C2-1;C3-2 y=S/y:4;Bmaster=3;" + + "N refs/notes/git-meta/subrepo-check 2=ok;" + "N refs/notes/git-meta/subrepo-check 3=ok|" + "y=S:C4-1;Bmaster=4", }, @@ -156,6 +158,7 @@ describe("synthetic-branch", function () { "|y=S:C4-1;Bmaster=4" + "|u=S:C7-1;Bmaster=7", expected: "x=S:C2-1;C3-2 y=S/whitelisted:7;Bmaster=3;" + + "N refs/notes/git-meta/subrepo-check 2=ok;" + "N refs/notes/git-meta/subrepo-check 3=ok" + "|y=S:C4-1;Bmaster=4|u=S:C7-1;Bmaster=7" }, @@ -163,6 +166,7 @@ describe("synthetic-branch", function () { input: "x=S:C2-1;C3-2 y/z=S/z:4;Bmaster=3|" + "z=S:C4-1;Bmaster=4", expected: "x=S:C2-1;C3-2 y/z=S/z:4;Bmaster=3;" + + "N refs/notes/git-meta/subrepo-check 2=ok;" + "N refs/notes/git-meta/subrepo-check 3=ok|" + "z=S:C4-1;Bmaster=4", }, @@ -170,6 +174,7 @@ describe("synthetic-branch", function () { input: "x=S:C2-1 y/z=S/z:4;C3-2;Bmaster=3|" + "z=S:C4-1;Bmaster=4", expected: "x=S:C2-1 y/z=S/z:4;C3-2;Bmaster=3;" + + "N refs/notes/git-meta/subrepo-check 2=ok;" + "N refs/notes/git-meta/subrepo-check 3=ok|" + "z=S:C4-1;Bmaster=4", }, @@ -177,6 +182,7 @@ describe("synthetic-branch", function () { input: "x=S:C2-1 y/z=S/z:4;C3-2 y/foo=bar;Bmaster=3|" + "z=S:C4-1;Bmaster=4", expected: "x=S:C2-1 y/z=S/z:4;C3-2 y/foo=bar;Bmaster=3;" + + "N refs/notes/git-meta/subrepo-check 2=ok;" + "N refs/notes/git-meta/subrepo-check 3=ok|" + "z=S:C4-1;Bmaster=4", }, From 9610e6f6302f70445d8c7106bb0941a55ceb7977 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 20 Sep 2019 11:51:43 -0400 Subject: [PATCH 260/402] move delete check to place where it will actually work --- node/lib/util/synthetic_branch_util.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index b174cb6c6..a21bc8de5 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -289,11 +289,6 @@ function* parentLoop(repo, notesRepo, commit, oldSha, handled) { assert.isString(oldSha); assert.isObject(handled); - // deleting is OK - if (commit === "0000000000000000000000000000000000000000") { - return true; - } - if (commit.id() in handled) { return handled[commit.id()]; } @@ -354,6 +349,11 @@ function* checkUpdate(repo, notesRepo, oldSha, newSha, handled) { assert.isString(newSha); assert.isObject(handled); + // deleting is OK + if (newSha === "0000000000000000000000000000000000000000") { + return true; + } + const newAnnotated = yield GitUtil.resolveCommitish(repo, newSha); if (newAnnotated === null) { From 1964ea4ae70b73110a837aa4979f828ddaf752b3 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 23 Sep 2019 19:00:55 -0400 Subject: [PATCH 261/402] set exec's maxBuffer It's kind of annoying that we have to do this, but I've hit this a few times for reasons I don't really understand. So let's just make the buffer huge and hope we don't fill it. --- node/lib/util/git_util.js | 31 ++++++++++++++++++++++++------- node/lib/util/merge_util.js | 4 +++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index ac839a226..b8e6e6e60 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -48,6 +48,8 @@ const Hook = require("./hook"); const GitUtilFast = require("./git_util_fast"); const UserError = require("./user_error"); +const EXEC_BUFFER = 1024*1024*100; + /** * Create a branch having the specified `branchName` in the specified `repo` * pointing to the current head. @@ -360,12 +362,18 @@ exports.push = co.wrap(function *(repo, remote, source, target, force, quiet) { const execString = `\ git -C '${repo.path()}' push ${forceStr} ${remote} ${source}:${target}`; try { - let result = yield ChildProcess.exec(execString, {env : environ}); + let result = yield ChildProcess.exec(execString, { + env : environ, + maxBuffer: EXEC_BUFFER + }); if (result.error && result.stderr.indexOf("reference already exists") !== -1) { // GitLab has a race condition that somehow causes this to // happen spuriously -- let's retry. - result = yield ChildProcess.exec(execString, {env : environ}); + result = yield ChildProcess.exec(execString, { + env : environ, + maxBuffer: EXEC_BUFFER + }); } if (result.error || !quiet) { if (result.stdout) { @@ -481,7 +489,9 @@ exports.fetch = co.wrap(function *(repo, remoteName) { const execString = `git -C '${repo.path()}' fetch -q '${remoteName}'`; try { - return yield ChildProcess.exec(execString); + return yield ChildProcess.exec(execString, { + maxBuffer: EXEC_BUFFER + }); } catch (e) { throw new UserError(e.message); @@ -510,7 +520,9 @@ exports.fetchBranch = co.wrap(function *(repo, remoteName, branch) { const execString = `\ git -C '${repo.path()}' fetch -q '${remoteName}' '${branch}'`; try { - return yield ChildProcess.exec(execString); + return yield ChildProcess.exec(execString, { + maxBuffer: EXEC_BUFFER + }); } catch (e) { throw new UserError(e.message); @@ -544,7 +556,9 @@ exports.fetchSha = co.wrap(function *(repo, url, sha) { const execString = `git -C '${repo.path()}' fetch -q '${url}' ${sha}`; try { - yield ChildProcess.exec(execString); + yield ChildProcess.exec(execString, { + maxBuffer: EXEC_BUFFER + }); yield repo.getCommit(sha); } catch (e) { @@ -712,7 +726,8 @@ exports.getEditorCommand = co.wrap(function *(repo) { // `git`. const result = - yield ChildProcess.exec(`git -C '${repo.path()}' var GIT_EDITOR`); + yield ChildProcess.exec(`git -C '${repo.path()}' var GIT_EDITOR`, + {maxBuffer: EXEC_BUFFER}); return result.stdout.split("\n")[0]; }); @@ -938,7 +953,9 @@ exports.mergeBases = co.wrap(function *(repo, commit1, commit2) { const id2 = commit2.id().tostrS(); const execString = `\ git -C '${repo.path()}' merge-base ${id1} ${id2}`; - const result = yield ChildProcess.exec(execString); + const result = yield ChildProcess.exec(execString, { + maxBuffer: EXEC_BUFFER + }); if (result.error) { throw new UserError("Couldn't run git merge-base: " + result.stderr); diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 46ea55097..f2c3c3e40 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -751,7 +751,9 @@ exports.continue = co.wrap(function *(repo) { const resetMerge = co.wrap(function *(repo) { // TODO: add this to libgit2 const execString = `git -C '${repo.workdir()}' reset --merge`; - yield ChildProcess.exec(execString); + yield ChildProcess.exec(execString, { + maxBuffer: 1024*1024*100 + }); }); /** From a59c3ef6720c1cf40ca06c465d75eb21a1e8eccf Mon Sep 17 00:00:00 2001 From: Ang Lee Date: Mon, 14 Oct 2019 12:46:40 -0400 Subject: [PATCH 262/402] Make push start with submodule's workdir if one exists --- node/lib/util/git_util.js | 29 ++++++++++++++++++++++------- node/lib/util/push.js | 2 +- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index b8e6e6e60..c29d6ce26 100644 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -353,14 +353,29 @@ exports.push = co.wrap(function *(repo, remote, source, target, force, quiet) { forceStr = "-f"; } - const environ = Object.assign({}, process.env); - // Hack: set a fake work-tree because the repo's core.worktree might - // be set to a directory that, due to sparseness, doesn't exist, and - // git push has a bug which requires it to have a worktree. - environ.GIT_WORK_TREE = repo.path(); + const { execString, environ } = (() => { + if (repo.workdir()) { + return { + execString: `\ +git -C '${repo.workdir()}' push ${forceStr} ${remote} ${source}:${target}`, + environ: Object.assign({}, process.env) + }; + } + + // Hack: set a fake work-tree because the repo's core.worktree might + // be set to a directory that, due to sparseness, doesn't exist, and + // git push has a bug which requires it to have a worktree. + return { + execString: `\ +git -C '${repo.path()}' push ${forceStr} ${remote} ${source}:${target}`, + environ: Object.assign( + {}, + process.env, + { GIT_WORK_TREE: repo.path() } + ) + }; + })(); - const execString = `\ -git -C '${repo.path()}' push ${forceStr} ${remote} ${source}:${target}`; try { let result = yield ChildProcess.exec(execString, { env : environ, diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 199bacf89..5dd6c96d9 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -236,7 +236,7 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { const sha = pushMap[subName]; const syntheticName = SyntheticBranchUtil.getSyntheticBranchForCommit(sha); - const subRepo = yield SubmoduleUtil.getBareRepo(repo, subName); + const subRepo = yield SubmoduleUtil.getRepo(repo, subName); // Resolve the submodule's URL against the URL of the meta-repo, // ignoring the remote that is configured in the open submodule. From 909ea825d1f742acbf2303875d9e907fd70e48f0 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 23 Oct 2019 16:51:40 -0400 Subject: [PATCH 263/402] fix #735 --- node/lib/util/submodule_config_util.js | 5 ++- node/test/util/close_util.js | 58 +++++++++++++++++++------- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index cd1638c6e..36651ca53 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -191,7 +191,10 @@ exports.deinit = co.wrap(function *(repo, submoduleNames) { next = path.dirname(next); } } catch (e) { - if ("ENOTEMPTY" !== e.code) { + // It's possible that we're closing d/x and d/y, and + // that in doing so, we end up trying to delete d + // twice, which would give ENOENT. + if ("ENOTEMPTY" !== e.code && "ENOENT" !== e.code) { throw e; } } diff --git a/node/test/util/close_util.js b/node/test/util/close_util.js index f95fd7f44..a8341a5f4 100644 --- a/node/test/util/close_util.js +++ b/node/test/util/close_util.js @@ -78,6 +78,24 @@ describe("close_util", function () { paths: ["s","t"], expected: "x=S:C2-1 s=Sa:1,t=Sa:1;Bmaster=2", }, + // This tests something only triggered by a race condition, so + // it might be hard to trigger a failure + "multiple, in a subdir": { + state: `a=B|x=S:C2-1 a/b/c/d/e/s1=Sa:1, +a/b/c/d/e/s2=Sa:1, +a/b/c/d/e/s3=Sa:1, +a/b/c/d/e/s4=Sa:1; +Bmaster=2; +Oa/b/c/d/e/s1; +Oa/b/c/d/e/s2; +Oa/b/c/d/e/s3; +Oa/b/c/d/e/s4`, + paths: ["a"], + expected: `x=S:C2-1 a/b/c/d/e/s1=Sa:1, +a/b/c/d/e/s2=Sa:1, +a/b/c/d/e/s3=Sa:1, +a/b/c/d/e/s4=Sa:1;Bmaster=2`, + }, "dirty fail staged": { state: "a=B|x=U:Os I a=b", paths: ["s"], @@ -102,22 +120,32 @@ describe("close_util", function () { }, }; Object.keys(cases).forEach(caseName => { - const c = cases[caseName]; - const closer = co.wrap(function *(repos) { - const x = repos.x; - let cwd = x.workdir(); - if (undefined !== c.cwd) { - cwd = path.join(cwd, c.cwd); - } - yield CloseUtil.close(x, cwd, c.paths, c.force || false); - }); - it(caseName, co.wrap(function *() { - yield RepoASTTestUtil.testMultiRepoManipulator(c.state, - c.expected, - closer, - c.fails); + for (const sparse of [true, false]) { + const c = cases[caseName]; + const closer = co.wrap(function *(repos) { + const x = repos.x; + let cwd = x.workdir(); + if (undefined !== c.cwd) { + cwd = path.join(cwd, c.cwd); + } + yield CloseUtil.close(x, cwd, c.paths, c.force || false); + }); + it(caseName, co.wrap(function *() { + let state = c.state; + let expected = c.expected; + if (sparse) { + state = state.replace("x=S", "x=%S"); + if (expected !== undefined) { + expected = expected.replace("x=S", "x=%S"); + } + } + yield RepoASTTestUtil.testMultiRepoManipulator(state, + expected, + closer, + c.fails); - })); + })); + } }); }); }); From af3500c2a57e4cf2f1c7363af58f89278e36bf7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2019 15:24:07 +0000 Subject: [PATCH 264/402] Bump lodash from 4.17.11 to 4.17.15 in /node Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.15. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.15) Signed-off-by: dependabot[bot] --- node/package-lock.json | 109 ++++++++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 33 deletions(-) diff --git a/node/package-lock.json b/node/package-lock.json index 11ff10af4..7879e543e 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -172,9 +172,9 @@ } }, "binary-search": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.5.tgz", - "integrity": "sha512-RHFP0AdU6KAB0CCZsRMU2CJTk2EpL8GLURT+4gilpjr1f/7M91FgUMnXuQLmf3OKLet34gjuNFwO7e4agdX5pw==" + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz", + "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==" }, "bl": { "version": "1.2.2", @@ -384,9 +384,9 @@ "dev": true }, "colors": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", - "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" }, "combined-stream": { "version": "1.0.8", @@ -397,9 +397,9 @@ } }, "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "optional": true }, "component-props": { @@ -528,18 +528,23 @@ "dev": true }, "dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz", + "integrity": "sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==", "requires": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" + "domelementtype": "^2.0.1", + "entities": "^2.0.0" }, "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" + }, "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==" } } }, @@ -647,9 +652,9 @@ "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=" }, "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "execa": { "version": "1.0.0", @@ -802,6 +807,16 @@ "inherits": "~2.0.0", "mkdirp": ">=0.5 0", "rimraf": "2" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + } } }, "function-bind": { @@ -898,9 +913,9 @@ "dev": true }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz", + "integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==", "requires": { "neo-async": "^2.6.0", "optimist": "^0.6.1", @@ -1281,9 +1296,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "log-symbols": { "version": "2.2.0", @@ -1585,6 +1600,16 @@ "semver": "~5.3.0", "tar": "^2.0.0", "which": "1" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + } } }, "node-pre-gyp": { @@ -1613,6 +1638,14 @@ "osenv": "^0.1.4" } }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, "tar": { "version": "4.4.9", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.9.tgz", @@ -2072,9 +2105,9 @@ "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", "requires": { "glob": "^7.1.3" } @@ -2290,6 +2323,16 @@ "integrity": "sha512-YfUhPQCJoNQE5N+FJQcdPz63O3x3sdT4Xju69Gj4iZe0lBKOtnAMi0SLj9xKhGkcGhsxThvTJ/usxtFPo438zQ==", "requires": { "rimraf": "~2.6.2" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "requires": { + "glob": "^7.1.3" + } + } } }, "thenify": { @@ -2369,12 +2412,12 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" }, "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.5.tgz", + "integrity": "sha512-7L3W+Npia1OCr5Blp4/Vw83tK1mu5gnoIURtT1fUVfQ3Kf8WStWV6NJz0fdoBJZls0KlweruRTLVe6XLafmy5g==", "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" }, "dependencies": { From 0582e9b0534f42443ab9eaabefd2455cf66871bf Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 15 Nov 2019 16:07:48 -0500 Subject: [PATCH 265/402] Do not fail to push when a changed submodule is closed; all we need is the bare repo --- node/lib/util/push.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 5dd6c96d9..199bacf89 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -236,7 +236,7 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { const sha = pushMap[subName]; const syntheticName = SyntheticBranchUtil.getSyntheticBranchForCommit(sha); - const subRepo = yield SubmoduleUtil.getRepo(repo, subName); + const subRepo = yield SubmoduleUtil.getBareRepo(repo, subName); // Resolve the submodule's URL against the URL of the meta-repo, // ignoring the remote that is configured in the open submodule. From c53892587ecf572a17281535e91d756790da9af7 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 6 Jan 2020 16:54:51 -0500 Subject: [PATCH 266/402] Open fewer submodules in parallel Unfortunately, we have http.postbuffer set to an astronomical value due to https://gitlab.com/gitlab-org/gitlab/issues/17649. This means that high parallelism on open can cause OOMs. --- node/lib/cmd/open.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index dc6967b5a..93ba390d1 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -151,7 +151,7 @@ Opening ${colors.blue(name)} on ${colors.green(shas[idx])}.`); } console.log(`Finished opening ${colors.blue(name)}.`); }); - yield DoWorkQueue.doInParallel(subsToOpen, opener); + yield DoWorkQueue.doInParallel(subsToOpen, opener, 10); // Make sure the index entries are updated in case we're in sparse mode. From 1da9e2f28bf6d93f97de2eae27144d1d269c63c4 Mon Sep 17 00:00:00 2001 From: Chaim Mintz Date: Mon, 6 Jan 2020 16:15:37 -0500 Subject: [PATCH 267/402] fix merge-bare no-ff param behavior not be inverted --- node/lib/cmd/merge_bare.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) mode change 100644 => 100755 node/lib/cmd/merge_bare.js diff --git a/node/lib/cmd/merge_bare.js b/node/lib/cmd/merge_bare.js old mode 100644 new mode 100755 index d20560ade..96d249646 --- a/node/lib/cmd/merge_bare.js +++ b/node/lib/cmd/merge_bare.js @@ -48,7 +48,7 @@ print resulting merged commit sha`; * @property {String} */ exports.description =` Merge changes from two commits. This command -can work with or without a working tree, resulting a dangling merged commit +can work with or without a working tree, resulting a dangling merged commit that is not pointed by any refs. It will also abort if there are merge conflicts between two commits.`; @@ -97,8 +97,8 @@ exports.executeableSubcommand = co.wrap(function *(args) { const repo = yield GitUtil.getCurrentRepo(); const mode = args.no_ff ? - MergeCommon.MODE.NORMAL : - MergeCommon.MODE.FORCE_COMMIT; + MergeCommon.MODE.FORCE_COMMIT : + MergeCommon.MODE.NORMAL; let ourCommitName = args.ourCommit[0]; let theirCommitName = args.theirCommit[0]; if (null === ourCommitName || null === theirCommitName) { @@ -110,7 +110,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { Could not resolve ${colors.red(ourCommitName)} to a commit.`); } - const theirCommitish + const theirCommitish = yield GitUtil.resolveCommitish(repo, theirCommitName); if (null === theirCommitish) { throw new UserError(`\ From 8b2fcffb020c794b79463ed8f534c220c6e86952 Mon Sep 17 00:00:00 2001 From: Chaim Mintz Date: Mon, 6 Jan 2020 16:18:54 -0500 Subject: [PATCH 268/402] fix merge to use a commit for submodules instead of HEAD so merge-bare can find the submodules in the commits provided --- node/lib/util/merge_common.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) mode change 100644 => 100755 node/lib/util/merge_common.js diff --git a/node/lib/util/merge_common.js b/node/lib/util/merge_common.js old mode 100644 new mode 100755 index 520e68d2d..26dd1b70e --- a/node/lib/util/merge_common.js +++ b/node/lib/util/merge_common.js @@ -90,7 +90,7 @@ class MergeContext { this.d_openOption = openOption; this.d_commitMessage = commitMessage; this.d_editMessage = editMessage; - this.d_opener = new Open.Opener(metaRepo, null); + this.d_opener = new Open.Opener(metaRepo, ourCommit); this.d_changeIndex = null; this.d_changes = null; this.d_conflictsMessage = ""; @@ -166,7 +166,7 @@ MergeContext.prototype.getChanges = co.wrap(function *() { return this.d_changes; }); -/** +/** * @async * @return {NodeGit.Commit} return left side merge commit */ @@ -185,7 +185,7 @@ MergeContext.prototype.getOurCommit = co.wrap(function *() { * return an index object that contains the merge changes and whose tree * representation will be flushed to disk. * @async - * @return {NodeGit.Index} + * @return {NodeGit.Index} */ MergeContext.prototype.getIndexToWrite = co.wrap(function *() { return this.forceBare ? @@ -194,7 +194,7 @@ MergeContext.prototype.getIndexToWrite = co.wrap(function *() { }); /** - * in memeory index object by merging `ourCommit` and `theirCommit` + * in memeory index object by merging `ourCommit` and `theirCommit` * @return {NodeGit.Index} */ MergeContext.prototype.getChangeIndex = co.wrap(function *() { @@ -202,8 +202,8 @@ MergeContext.prototype.getChangeIndex = co.wrap(function *() { return this.d_changeIndex; } this.d_changeIndex = yield NodeGit.Merge.commits(this.d_metaRepo, - yield this.getOurCommit(), - this.d_theirCommit, + yield this.getOurCommit(), + this.d_theirCommit, []); return this.d_changeIndex; }); @@ -211,12 +211,12 @@ MergeContext.prototype.getChangeIndex = co.wrap(function *() { /** * Return the previously set/built commit message, or use the callback to * build commit messsage. Once built, the commit message will be cached. - * + * * @async * @return {String} commit message */ MergeContext.prototype.getCommitMessage = co.wrap(function *() { - const message = (null === this.d_commitMessage) ? + const message = (null === this.d_commitMessage) ? GitUtil.stripMessage(yield this.d_editMessage()) : this.d_commitMessage; if ("" === message) { @@ -302,7 +302,7 @@ class MergeStepResult { /** * A merge result that signifies we need to abort current merging process. - * + * * @static * @param {MergeStepResult} msg error message */ @@ -312,9 +312,9 @@ class MergeStepResult { /** * A merge result that does not have any submodule commit. Only a finishing * sha at the meta repo level will be returned. - * + * * @static - * @param {String} infoMessage + * @param {String} infoMessage * @param {String} finishSha meta repo commit sha */ static justMeta(infoMessage, finishSha) { From 2e3b83685a5f3f664789b30302b04816940e2da0 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Tue, 7 Jan 2020 10:09:26 -0500 Subject: [PATCH 269/402] fix git meta fails when HEAD does not exist #742 --- node/lib/cmd/checkout.js | 3 ++- node/test/util/print_status_util.js | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index 8c3a69450..783cc30ba 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -139,7 +139,8 @@ exports.executeableSubcommand = co.wrap(function *(args) { } const repo = yield GitUtil.getCurrentRepo(); const headId = yield repo.getHeadCommit(); - const oldHead = headId.id().tostrS(); + const zeroSha = "0000000000000000000000000000000000000000"; + const oldHead = headId === null ? zeroSha : headId.id().tostrS(); // Validate and determine what operation we're actually doing. diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js index 03326d3bb..3661fc8c4 100644 --- a/node/test/util/print_status_util.js +++ b/node/test/util/print_status_util.js @@ -694,6 +694,11 @@ A merge is in progress. }), check: /aaaa/, }, + "no-commits": { + input: new RepoStatus({ + }), + check: /No commits yet/, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; From 4121a2acfcf9a9b5e580a7f3070519dc9d010fd0 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Thu, 9 Jan 2020 16:45:15 -0500 Subject: [PATCH 270/402] Allow git merge reading GIT_AUTHOR_xxx and GIT_COMMITTER_xxx environemnt variables --- node/lib/util/merge_common.js | 36 ++++++++++++++++++++++- node/lib/util/merge_util.js | 41 +++++++++++++++++--------- node/test/util/merge_bare.js | 49 ++++++++++++++++++++++++++++++- node/test/util/merge_full_open.js | 6 +++- node/test/util/merge_util.js | 7 +++-- 5 files changed, 120 insertions(+), 19 deletions(-) diff --git a/node/lib/util/merge_common.js b/node/lib/util/merge_common.js index 26dd1b70e..0010bf83d 100755 --- a/node/lib/util/merge_common.js +++ b/node/lib/util/merge_common.js @@ -71,7 +71,11 @@ class MergeContext { mode, openOption, commitMessage, - editMessage) { + editMessage, + authorName, + authorEmail, + committerName, + committerEmail) { assert.instanceOf(metaRepo, NodeGit.Repository); if (null !== ourCommit) { assert.instanceOf(ourCommit, NodeGit.Commit); @@ -94,6 +98,10 @@ class MergeContext { this.d_changeIndex = null; this.d_changes = null; this.d_conflictsMessage = ""; + this.d_authorName = authorName; + this.d_authorEmail = authorEmail; + this.d_committerName = committerName; + this.d_committerEmail = committerEmail; } /** @@ -233,6 +241,32 @@ MergeContext.prototype.getSig = co.wrap(function *() { return yield ConfigUtil.defaultSignature(this.d_metaRepo); }); +/** + * @async + * @returns {NodeGit.Signature} author to be set with merge commit + */ +MergeContext.prototype.getAuthor = co.wrap(function *() { + if (this.d_authorName && this.d_authorEmail) { + return NodeGit.Signature.now( + this.d_authorName, + this.d_authorEmail); + } + return yield ConfigUtil.defaultSignature(this.d_metaRepo); +}); + +/** + * @async + * @returns {NodeGit.Signature} committer to be set with merge commit + */ +MergeContext.prototype.getCommitter = co.wrap(function *() { + if (this.d_committerName && this.d_committerEmail) { + return NodeGit.Signature.now( + this.d_committerName, + this.d_committerEmail); + } + return yield ConfigUtil.defaultSignature(this.d_metaRepo); +}); + /** * @async * @returns {SubmoduleFetcher} diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index f2c3c3e40..b8e57dc2c 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -151,6 +151,8 @@ exports.fastForwardMerge = co.wrap(function *(repo, commit) { * @param {NodeGit.Commit} theirCommit * @param {String} commitMessage * @param {String | null} refToUpdate + * @param {NodeGit.Signature} author + * @param {NodeGit.Signature} committer * @return {Object} * @return {String|null} return.infoMessage informative message * @return {String|null} return.metaCommit in case no further merge operation @@ -161,13 +163,13 @@ exports.makeMetaCommit = co.wrap(function *(repo, ourCommit, theirCommit, commitMessage, - refToUpdate) { + refToUpdate, + author, + committer) { const id = yield indexToWrite.writeTreeTo(repo); - const sig = yield ConfigUtil.defaultSignature(repo); - const metaCommit = yield repo.createCommit(refToUpdate, - sig, - sig, + author, + committer, commitMessage, id, [ourCommit, theirCommit]); @@ -188,7 +190,8 @@ exports.makeMetaCommit = co.wrap(function *(repo, * @param {SubmoduleChange} change specifies the commits to merge * @param {String} message commit message * @param {SubmoduleFetcher} fetcher helper to fetch commits in the sub - * @param {NodeGit.Signature} sig default signature + * @param {NodeGit.Signature} author author signature + * @param {NodeGit.Signature} author committer signature * @param {Open.Opener} opener helper to open a sub * @param {SUB_OPEN_OPTION} openOption option to open a sub * @return {Object} @@ -201,7 +204,8 @@ exports.mergeSubmodule = co.wrap(function *(metaIndex, message, opener, fetcher, - sig, + author, + committer, openOption) { assert.instanceOf(metaIndex, NodeGit.Index); assert.isString(subName); @@ -209,7 +213,8 @@ exports.mergeSubmodule = co.wrap(function *(metaIndex, assert.isString(message); assert.instanceOf(opener, Open.Opener); assert.instanceOf(fetcher, SubmoduleFetcher); - assert.instanceOf(sig, NodeGit.Signature); + assert.instanceOf(author, NodeGit.Signature); + assert.instanceOf(committer, NodeGit.Signature); assert.isNumber(openOption); let subRepo = yield opener.getSubrepo(subName, openOption); @@ -303,8 +308,8 @@ ${colors.green(theirSha)}.`); const refToUpdate = isHalfOpened ? null : "HEAD"; const treeId = yield subIndex.writeTreeTo(subRepo); const mergeCommit = yield subRepo.createCommit(refToUpdate, - sig, - sig, + author, + committer, message, treeId, [ourCommit, theirCommit]); @@ -447,7 +452,8 @@ const mergeStepMergeSubmodules = co.wrap(function *(context) { const ourCommitSha = ourCommit.id().tostrS(); const refToUpdate = context.refToUpdate; const repo = context.metaRepo; - const sig = yield context.getSig(); + const author = yield context.getAuthor(); + const committer = yield context.getCommitter(); const theirCommit = context.theirCommit; const theirCommitSha = theirCommit.id().tostrS(); @@ -487,7 +493,8 @@ const mergeStepMergeSubmodules = co.wrap(function *(context) { message, opener, fetcher, - sig, + author, + committer, openOption); if (null !== subResult.mergeSha) { merges.commits[subName] = subResult.mergeSha; @@ -547,7 +554,9 @@ const mergeStepMergeSubmodules = co.wrap(function *(context) { ourCommit, theirCommit, message, - refToUpdate); + refToUpdate, + author, + committer); infoMessage += "\n" + metaCommitRet.infoMessage; return new MergeStepResult(infoMessage, null, @@ -601,7 +610,11 @@ exports.merge = co.wrap(function *(repo, mode, openOption, commitMessage, - editMessage); + editMessage, + process.env.GIT_AUTHOR_NAME, + process.env.GIT_AUTHOR_EMAIL, + process.env.GIT_COMMITTER_NAME, + process.env.GIT_COMMITTER_EMAIL); // const result = { metaCommit: null, diff --git a/node/test/util/merge_bare.js b/node/test/util/merge_bare.js index 8bf64e87d..a1299bbc4 100644 --- a/node/test/util/merge_bare.js +++ b/node/test/util/merge_bare.js @@ -78,6 +78,26 @@ x=U:C3-2 s=Sa:a;C5-4 t=Sa:b;C4-2 s=Sa:b;Bmaster=3;Bfoo=5`, theirCommit: "5", ourCommit: "3", }, + "one merge with author and committer": { + initial: ` +a=B:Ca-1;Cb-1;Ba=a;Bb=b| +x=U:C3-2 s=Sa:a;C4-2 s=Sa:b;Bmaster=3;Bfoo=4`, + theirCommit: "4", + ourCommit: "3", + authorName: "alice", + authorEmail: "alice@example.com", + committerName: "bob", + committerEmail: "bob@example.com", + verify: co.wrap(function *(repo, result) { + const commit = yield repo.getCommit(result.metaCommit); + const author = commit.author(); + const committer = commit.committer(); + assert.equal(author.name(), "alice"); + assert.equal(author.email(), "alice@example.com"); + assert.equal(committer.name(), "bob"); + assert.equal(committer.email(), "bob@example.com"); + }), + }, "non-ffmerge with trivial ffwd submodule change": { initial: ` a=Aa:Cb-a;Bb=b| @@ -273,6 +293,31 @@ x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; + let authorName, authorEmail, committerName, committerEmail; + this.beforeEach(function () { + if (c.authorName && c.authorEmail) { + authorName = process.env.GIT_AUTHOR_NAME; + authorEmail = process.env.GIT_AUTHOR_EMAIL; + process.env.GIT_AUTHOR_NAME = c.authorName; + process.env.GIT_AUTHOR_EMAIL = c.authorEmail; + } + if (c.committerName && c.committerEmail) { + committerName = process.env.GIT_COMMITTER_NAME; + committerEmail = process.env.GIT_COMMITTER_EMAIL; + process.env.GIT_COMMITTER_NAME = c.committerName; + process.env.GIT_COMMITTER_EMAIL = c.committerEmail; + } + }); + this.afterEach(function () { + if (authorName && authorEmail) { + process.env.GIT_AUTHOR_NAME = authorName; + process.env.GIT_AUTHOR_EMAIL = authorEmail; + } + if (committerName && committerEmail) { + process.env.GIT_AUTHOR_NAME = committerName; + process.env.GIT_AUTHOR_EMAIL = committerEmail; + } + }); it(caseName, co.wrap(function *() { // expect no changes to the repo const expected = "x=E"; @@ -307,7 +352,9 @@ x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; assert.isNull(result.metaCommit); return; // RETURN } - + if (c.verify) { + yield c.verify(x, result); + } if (result.metaCommit) { const parents = c.parents ? c.parents.map(v => reverseCommitMap[v]) : diff --git a/node/test/util/merge_full_open.js b/node/test/util/merge_full_open.js index 35ff79a83..88ff85d91 100644 --- a/node/test/util/merge_full_open.js +++ b/node/test/util/merge_full_open.js @@ -408,7 +408,11 @@ x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; mode, openOption, message, - editMessage); + editMessage, + null, + null, + null, + null); const errorMessage = c.errorMessage || null; assert.equal(result.errorMessage, errorMessage); if (upToDate) { diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index f12c06d92..af6f356cf 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -476,7 +476,11 @@ x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; mode, openOption, message, - editMessage); + editMessage, + null, + null, + null, + null); const errorMessage = c.errorMessage || null; assert.equal(result.errorMessage, errorMessage); @@ -484,7 +488,6 @@ x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; assert.isNull(result.metaCommit); return; // RETURN } - if (!result.metaCommit) { return; } From bfb721234f4bb956ade03aaf32c59ef483e18c01 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Thu, 9 Jan 2020 17:21:26 -0500 Subject: [PATCH 271/402] Imporve git meta merge-bare #744 --- node/lib/cmd/merge_bare.js | 10 +++++----- node/lib/util/merge_util.js | 28 ++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/node/lib/cmd/merge_bare.js b/node/lib/cmd/merge_bare.js index 96d249646..f11930ee0 100755 --- a/node/lib/cmd/merge_bare.js +++ b/node/lib/cmd/merge_bare.js @@ -62,12 +62,10 @@ exports.configureParser = function (parser) { parser.addArgument(["ourCommit"], { type: "string", help: "our side commitish to merge", - nargs: 1, }); parser.addArgument(["theirCommit"], { type: "string", help: "their side commitish to merge", - nargs: 1, }); parser.addArgument(["--no-ff"], { @@ -99,8 +97,8 @@ exports.executeableSubcommand = co.wrap(function *(args) { const mode = args.no_ff ? MergeCommon.MODE.FORCE_COMMIT : MergeCommon.MODE.NORMAL; - let ourCommitName = args.ourCommit[0]; - let theirCommitName = args.theirCommit[0]; + let ourCommitName = args.ourCommit; + let theirCommitName = args.theirCommit; if (null === ourCommitName || null === theirCommitName) { throw new UserError("Two commits must be given."); } @@ -130,7 +128,9 @@ Could not resolve ${colors.red(theirCommitName)} to a commit.`); if (null !== result.errorMessage) { throw new UserError(result.errorMessage); } - + if (null !== result.metaCommit) { + console.log(result.metaCommit); + } // Run post-merge hook if merge successfully. // Fixme: --squash is not supported yet, once supported, need to parse 0/1 // as arg into the post-merge hook, 1 means it is a squash merge, 0 means diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index b8e57dc2c..fb9e3b183 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -98,17 +98,23 @@ const checkForMerge = co.wrap(function *(path) { * @param {Object} conflicts map from name to commit causing conflict * @return {String} conflict message */ -exports.formatConflictsMessage = function(conflicts) { +const getBareMergeConflictsMessage = function(conflicts) { if (0 === Object.keys(conflicts).length) { return ""; } let errorMessage = "CONFLICT (content): \n"; const names = Object.keys(conflicts).sort(); for (let name of names) { - errorMessage += `Conflicting entries for submodule: ` + - `${colors.red(name)}\n`; + const conflict = conflicts[name]; + if (Array.isArray(conflict)) { + for (const path of conflict) { + errorMessage += `\tconflicted: ${name}/${path}\n`; + } + } else { + errorMessage += `Merge conflict in submodule '${name}':\n`; + } } - errorMessage += "Automatic merge failed\n"; + errorMessage += "\nAutomatic merge failed\n"; return errorMessage; }; @@ -197,6 +203,7 @@ exports.makeMetaCommit = co.wrap(function *(repo, * @return {Object} * @return {String|null} return.mergeSha * @return {String|null} return.conflictSha + * @return {String []} return.conflictPaths */ exports.mergeSubmodule = co.wrap(function *(metaIndex, subName, @@ -234,6 +241,7 @@ exports.mergeSubmodule = co.wrap(function *(metaIndex, const result = { mergeSha: null, conflictSha: null, + conflictPaths: [], }; // See if up-to-date @@ -254,7 +262,7 @@ exports.mergeSubmodule = co.wrap(function *(metaIndex, return result; // RETURN } - console.log(`Submodule ${colors.blue(subName)}: merging commit \ + console.error(`Submodule ${colors.blue(subName)}: merging commit \ ${colors.green(theirSha)}.`); // Start the merge. @@ -274,6 +282,8 @@ ${colors.green(theirSha)}.`); // record conflicts and then bubble up the conflicts. // 3. if bare is not allowed, record conflicts and bubble up conflicts if (subIndex.hasConflicts()) { + result.conflictPaths = + Object.keys(StatusUtil.readConflicts(subIndex, [])); if (forceBare) { result.conflictSha = theirSha; return result; @@ -460,7 +470,7 @@ const mergeStepMergeSubmodules = co.wrap(function *(context) { let conflictMessage = ""; // abort merge if conflicted under FROCE_BARE mode if (forceBare && Object.keys(changes.conflicts).length > 0) { - conflictMessage = exports.formatConflictsMessage(changes.conflicts); + conflictMessage = getBareMergeConflictsMessage(changes.conflicts); return MergeStepResult.error(conflictMessage); // RETURN } @@ -483,6 +493,7 @@ const mergeStepMergeSubmodules = co.wrap(function *(context) { const merges = { conflicts: {}, + conflictPaths: {}, commits: {}, }; const mergeSubmoduleRunner = co.wrap(function *(subName) { @@ -501,13 +512,14 @@ const mergeStepMergeSubmodules = co.wrap(function *(context) { } if (null !== subResult.conflictSha) { merges.conflicts[subName] = subResult.conflictSha; + merges.conflictPaths[subName] = subResult.conflictPaths; } }); yield DoWorkQueue.doInParallel(Object.keys(changes.changes), mergeSubmoduleRunner); // Render any conflicts if (forceBare) { - conflictMessage = exports.formatConflictsMessage(merges.conflicts); + conflictMessage = getBareMergeConflictsMessage(merges.conflictPaths); } else { conflictMessage = yield CherryPickUtil.writeConflicts(repo, @@ -630,7 +642,7 @@ exports.merge = co.wrap(function *(repo, for (const asyncStep of mergeAsyncSteps) { const ret = yield asyncStep(context); if (null !== ret.infoMessage) { - console.log(ret.infoMessage); + console.error(ret.infoMessage); } if (null !== ret.errorMessage) { throw new UserError(ret.errorMessage); From 06e2f77a3ac8f44f8e3f89693c19826b1b25872a Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Wed, 15 Jan 2020 10:30:12 -0500 Subject: [PATCH 272/402] Add error message to remind user to use git meta push other than git push --- node/lib/util/synthetic_branch_util.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index a21bc8de5..35076272d 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -395,7 +395,12 @@ function* metaUpdateIsBad(repo, notesRepo, updates) { }); const resolved = yield checkFailures; - return resolved.some(identity); + const isBad = resolved.some(identity); + if (isBad) { + console.error("\nPush to meta repository is incomplete, " + + "did you forget to use git meta push?\n"); + } + return isBad; } /** From dbb62a6805e67caa82791c3c6eb1162c8b0b55f1 Mon Sep 17 00:00:00 2001 From: "Craig P. Motlin" Date: Thu, 16 Jan 2020 10:49:32 -0500 Subject: [PATCH 273/402] Combine two error messages for failed synthetic branch check. --- node/lib/util/synthetic_branch_util.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 35076272d..c21b2d9a8 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -388,19 +388,16 @@ function* metaUpdateIsBad(repo, notesRepo, updates) { update.newSha, handled); if (!ok) { console.error( - "Ref update failed synthetic branch check for " + - update.ref); + "Update to ref '" + + update.ref + + "' failed synthetic branch check. " + + "Did you forget to use `git meta push`?"); } return !ok; }); const resolved = yield checkFailures; - const isBad = resolved.some(identity); - if (isBad) { - console.error("\nPush to meta repository is incomplete, " + - "did you forget to use git meta push?\n"); - } - return isBad; + return resolved.some(identity); } /** From eca1c48ce528a94137e61d861b43901802b24c7c Mon Sep 17 00:00:00 2001 From: David Turner and Chaim Mintz Date: Thu, 16 Jan 2020 13:57:08 -0500 Subject: [PATCH 274/402] Create synthetic ref for fetches in submodules Subsequent fetches can use these for negotiation and only download new objects instead of downloading the entire history. The tests are changed to mostly ignore all synthetic refs, since changing evrey test would be a nightmare. But a few tests have to care about synthetic refs because they are directly testing synthetic refs. Fortunately, there are no cases of tests that care about some but not all such refs. --- node/lib/util/git_util.js | 6 +++++- node/lib/util/read_repo_ast_util.js | 24 ++++++++++++++++++++++-- node/lib/util/repo_ast_test_util.js | 4 +++- node/test/util/destitch_util.js | 1 + node/test/util/push.js | 7 +++++-- node/test/util/stitch_util.js | 1 + 6 files changed, 37 insertions(+), 6 deletions(-) mode change 100644 => 100755 node/lib/util/git_util.js diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js old mode 100644 new mode 100755 index c29d6ce26..c354f24a7 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -46,6 +46,7 @@ const path = require("path"); const ConfigUtil = require("./config_util"); const Hook = require("./hook"); const GitUtilFast = require("./git_util_fast"); +const SyntheticBranchUtil = require("./synthetic_branch_util"); const UserError = require("./user_error"); const EXEC_BUFFER = 1024*1024*100; @@ -569,7 +570,10 @@ exports.fetchSha = co.wrap(function *(repo, url, sha) { catch (e) { } - const execString = `git -C '${repo.path()}' fetch -q '${url}' ${sha}`; + const syntheticName = + SyntheticBranchUtil.getSyntheticBranchForCommit(sha); + const execString = `git -C '${repo.path()}' fetch -q '${url}' \ +${sha}:${syntheticName}`; try { yield ChildProcess.exec(execString, { maxBuffer: EXEC_BUFFER diff --git a/node/lib/util/read_repo_ast_util.js b/node/lib/util/read_repo_ast_util.js index f45715267..61095a5aa 100644 --- a/node/lib/util/read_repo_ast_util.js +++ b/node/lib/util/read_repo_ast_util.js @@ -231,17 +231,32 @@ const loadIndexAndWorkdir = co.wrap(function *(repo, headCommit) { }; }); +const syntheticRefRegexp = new RegExp("^refs/commits/[0-9a-f]{40}$"); +const isSyntheticRef = function(refName) { + return syntheticRefRegexp.test(refName); +}; + /** * Return a representation of the specified `repo` encoded in an `AST` object. * * @async * @param {NodeGit.Repository} repo + * @param boolean includeRefsCommits if true, refs from the + * refs/commits namespace are read. Ordinarily, these are ignored + * because they are created any time a submodule is fetched as part of + * a git meta open, which is often to be done as part of repo writing. + * But some tests rely on refs in this namespace, and these tests need + * to include them. * @return {RepoAST} */ -exports.readRAST = co.wrap(function *(repo) { +exports.readRAST = co.wrap(function *(repo, includeRefsCommits) { // We're going to list all the branches in `repo`, and walk each of their // histories to generate a complete set of commits. + assert.instanceOf(repo, NodeGit.Repository); + if (includeRefsCommits === undefined) { + includeRefsCommits = false; + } assert.instanceOf(repo, NodeGit.Repository); const branches = yield repo.getReferences(NodeGit.Reference.TYPE.LISTALL); let commits = {}; @@ -399,7 +414,12 @@ exports.readRAST = co.wrap(function *(repo) { new RepoAST.Branch(id.tostrS(), tracking); } else if (!branch.isNote()) { - refTargets[branch.shorthand()] = id.tostrS(); + if (includeRefsCommits || + !isSyntheticRef(branch.name())) { + refTargets[branch.shorthand()] = id.tostrS(); + } else { + return; + } } else { return; // RETURN diff --git a/node/lib/util/repo_ast_test_util.js b/node/lib/util/repo_ast_test_util.js index 3e4612c3b..9d38757b0 100644 --- a/node/lib/util/repo_ast_test_util.js +++ b/node/lib/util/repo_ast_test_util.js @@ -215,6 +215,7 @@ exports.createMultiRepos = co.wrap(function *(input) { * @param {Object} options.expectedTransformer.mapping.reverseCommitMap * @param {Object} options.expectedTransformer.mapping.reverseUrlMap * @param {Object} options.expectedTransformer.return + * @param {Boolean} options.ignoreRefsCommits */ exports.testMultiRepoManipulator = co.wrap(function *(input, expected, manipulator, shouldFail, options) { @@ -245,6 +246,7 @@ exports.testMultiRepoManipulator = else { assert.isFunction(options.actualTransformer); } + const includeRefsCommits = options.includeRefsCommits || false; const inputASTs = createMultiRepoASTMap(input); // Write the repos in their initial states. @@ -344,7 +346,7 @@ exports.testMultiRepoManipulator = const path = manipulated.urlMap[repoName]; repo = yield NodeGit.Repository.open(path); } - const newAST = yield ReadRepoASTUtil.readRAST(repo); + const newAST = yield ReadRepoASTUtil.readRAST(repo, includeRefsCommits); const commits = RepoASTUtil.listCommits(newAST); Object.keys(commits).forEach(rememberCommit); actualASTs[repoName] = RepoASTUtil.mapCommitsAndUrls(newAST, diff --git a/node/test/util/destitch_util.js b/node/test/util/destitch_util.js index b8f2dca8c..2e777e1aa 100644 --- a/node/test/util/destitch_util.js +++ b/node/test/util/destitch_util.js @@ -753,6 +753,7 @@ b=E:Fcommits/s.x.s=s.x.s`, c.expected, destitcher, c.fails, { + includeRefsCommits : true, actualTransformer: refMapper, }); })); diff --git a/node/test/util/push.js b/node/test/util/push.js index 4ee86645e..da14db4b7 100644 --- a/node/test/util/push.js +++ b/node/test/util/push.js @@ -323,7 +323,9 @@ x=S:B3=3;C2-1 s=Sa:1;Rorigin=a master=3;Rtarget=b;Bmaster=2 origin/master;Os`, yield RepoASTTestUtil.testMultiRepoManipulator( c.initial, expected, - manipulator); + manipulator, + false, + {includeRefsCommits : true}); })); }); @@ -500,6 +502,7 @@ x=E:Rorigin=a foo=2`, c.expected, c.manipulator, c.fails, { + includeRefsCommits : true, expectedTransformer: refMapper, }); })); @@ -630,4 +633,4 @@ describe("getClosePushedCommit", function () { } })); }); -}); \ No newline at end of file +}); diff --git a/node/test/util/stitch_util.js b/node/test/util/stitch_util.js index 07eede973..501314057 100644 --- a/node/test/util/stitch_util.js +++ b/node/test/util/stitch_util.js @@ -1256,6 +1256,7 @@ describe("fetchSubCommits", function () { c.expected, fetcher, c.fails, { + includeRefsCommits: true, actualTransformer: refMapper, }); From 265cf646cd566f50189d3809f44b0418d535e546 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Fri, 28 Feb 2020 12:10:34 -0500 Subject: [PATCH 275/402] Allow merge-bare and merge to best effort merge submodule urls --- .travis.yml | 2 +- node/lib/util/cherry_pick_util.js | 114 +- node/lib/util/merge_util.js | 15 +- node/lib/util/submodule_config_util.js | 30 +- node/package-lock.json | 1656 +++++++++++------------- node/test/util/cherry_pick_util.js | 51 + 6 files changed, 975 insertions(+), 893 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4a2ae80b0..de044a25b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "6.9.1" + - "8.11.1" env: - CC=gcc-5 diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index c7a64c216..274de091e 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -88,11 +88,13 @@ function ensureCherryInProgress(seq) { * @param {Open.Opener} opener * @param {NodeGit.Index} index * @param {Object} submodules name to Submodule + * @param {(null|Object)} urlsInIndex name to sub.url */ exports.changeSubmodules = co.wrap(function *(repo, opener, index, - submodules) { + submodules, + urlsInIndex) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(opener, Open.Opener); assert.instanceOf(index, NodeGit.Index); @@ -100,7 +102,9 @@ exports.changeSubmodules = co.wrap(function *(repo, if (0 === Object.keys(submodules).count) { return; // RETURN } - const urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + const urls = (urlsInIndex === null || urlsInIndex === undefined) ? + (yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index)) : + urlsInIndex; const changes = {}; function rmrf(dir) { return new Promise(callback => { @@ -296,6 +300,81 @@ const workAroundLibgit2MergeBug = co.wrap(function *(data, repo, name, } }); +/** + * + * @param {Object} ancestorUrls urls from the merge base + * @param {Object} ourUrls urls from the left side of a merge + * @param {Object} theirUrls urls from the right side + * @returns {Object} + * @returns {Object} return.url submodule name to URLs + * @returns {Object} return.conflicts: name to a conflict object that contains + * urls of ancestors, ours and theirs. + */ +exports.resolveUrlsConflicts = function(ancestorUrls, ourUrls, theirUrls) { + const allSubNames = new Set(Object.keys(ancestorUrls)); + Object.keys(ourUrls).forEach(x => allSubNames.add(x)); + Object.keys(theirUrls).forEach(x => allSubNames.add(x)); + + const result = { + urls: {}, + conflicts: {}, + }; + const addUrl = function(name, url) { + if (url) { + result.urls[name] = url; + } + }; + for (const sub of allSubNames) { + const ancestorUrl = ancestorUrls[sub]; + const ourUrl = ourUrls[sub]; + const theirUrl = theirUrls[sub]; + if (ancestorUrl === ourUrl) { + addUrl(sub, theirUrl); + } else if (ancestorUrl === theirUrl) { + addUrl(sub, ourUrl); + } else if (ourUrl === theirUrl) { + addUrl(sub, ourUrl); + } else { + result.conflicts[sub] = { + ancestor: ancestorUrl, + our: ourUrl, + their: theirUrl + }; + } + } + return result; +}; + + +/** + * Resolve conflicts to `.gitmodules` file, return the merged list of urls or + * a Conflict object indicating the merge cannot be done automatically. + * + * @param repo repository where blob of `.gitmodules` can be read + * @param {(null|NodeGit.IndexEntry)} ancestorEntry entry of `.gitmodules` + * from merge base + * @param {(null|NodeGit.IndexEntry)} ourEntry entry of `.gitmodules` + * on the left side + * @param {(null|NodeGit.IndexEntry)} theirEntry entry of `.gitmodules` + * on the right side + * @returns {Object} + * @returns {Object} return.urls, list of sub names to urls + * @returns {Object} return.conflicts, object describing conflicts + */ +exports.resolveModuleFileConflicts = co.wrap(function*( + repo, + ancestorEntry, + ourEntry, + theirEntry +) { + assert.instanceOf(repo, NodeGit.Repository); + const getUrls = SubmoduleConfigUtil.getSubmodulesFromIndexEntry; + const ancestorUrls = yield getUrls(repo, ancestorEntry); + const ourUrls = yield getUrls(repo, ourEntry); + const theirUrls = yield getUrls(repo, theirEntry); + return exports.resolveUrlsConflicts(ancestorUrls, ourUrls, theirUrls); +}); + /** * Determine how to apply the submodule changes introduced in the * specified `srcCommit` to the commit `targetCommit` of the specified repo @@ -323,9 +402,6 @@ exports.computeChangesBetweenTwoCommits = co.wrap(function *(repo, assert.instanceOf(srcCommit, NodeGit.Commit); assert.instanceOf(targetCommit, NodeGit.Commit); const conflicts = {}; - const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit( - repo, - targetCommit); // Group together all parts of conflicted entries. const conflictEntries = new Map(); // name -> normal, ours, theirs @@ -384,6 +460,27 @@ exports.computeChangesBetweenTwoCommits = co.wrap(function *(repo, } } + // Get submodule urls. If there are no merge conflicts to `.gitmodules`, + // parse the file and return its list. If there are, best effort merging + // the urls. Throw user error if merge conflict cannot be resolved. + const modulesFileEntry = + conflictEntries.get(SubmoduleConfigUtil.modulesFileName); + let urls; + if (modulesFileEntry) { + const urlsRes = yield exports.resolveModuleFileConflicts(repo, + modulesFileEntry[STAGE.ANCESTOR], + modulesFileEntry[STAGE.OURS], + modulesFileEntry[STAGE.THEIRS] + ); + if (Object.keys(urlsRes.conflicts).length > 0) { + let errMsg = "Conflicts to submodule URLs: \n" + + JSON.stringify(urlsRes.conflicts); + throw new UserError(errMsg); + } + urls = urlsRes.urls; + } else { + urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + } // Now we handle the changes that Git was able to take care of by itself. // First, we're going to need to write the index to a tree; this write @@ -418,6 +515,7 @@ exports.computeChangesBetweenTwoCommits = co.wrap(function *(repo, simpleChanges: simpleChanges, changes: changes, conflicts: conflicts, + urls: urls, }; }); @@ -621,7 +719,11 @@ exports.rewriteCommit = co.wrap(function *(repo, commit) { // and fast-forwards. const opener = new Open.Opener(repo, null); - yield exports.changeSubmodules(repo, opener, index, changes.simpleChanges); + yield exports.changeSubmodules(repo, + opener, + index, + changes.simpleChanges, + null); // Render any conflicts diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index fb9e3b183..5b73940c4 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -52,6 +52,7 @@ const SubmoduleChange = require("./submodule_change"); const SubmoduleFetcher = require("./submodule_fetcher"); const SubmoduleRebaseUtil = require("./submodule_rebase_util"); const SubmoduleUtil = require("./submodule_util"); +const SubmoduleConfigUtil = require("./submodule_config_util"); const UserError = require("./user_error"); const CommitAndRef = SequencerState.CommitAndRef; @@ -370,9 +371,10 @@ const mergeStepPrepare = co.wrap(function *(context) { return MergeStepResult.error(errorMessage); // RETURN } - yield CherryPickUtil.ensureNoURLChanges(metaRepo, theirCommit, baseCommit); - if (!forceBare) { + yield CherryPickUtil.ensureNoURLChanges(metaRepo, + theirCommit, + baseCommit); const status = yield StatusUtil.getRepoStatus(metaRepo); const statusError = StatusUtil.checkReadiness(status); if (null !== statusError) { @@ -476,14 +478,15 @@ const mergeStepMergeSubmodules = co.wrap(function *(context) { // deal with simple changes if (forceBare) { - yield CherryPickUtil.changeSubmodulesBare(repo, - index, - changes.simpleChanges); + // for merge-bare, no need to open or delete submodules, directly + // writes the post merge urls to .gitmodules file. + yield SubmoduleConfigUtil.writeUrls(repo, index, changes.urls, true); } else { yield CherryPickUtil.changeSubmodules(repo, opener, index, - changes.simpleChanges); + changes.simpleChanges, + changes.urls); } const message = yield context.getCommitMessage(); diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index 36651ca53..d83c6f4f7 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -385,6 +385,26 @@ exports.getSubmodulesFromCommit = co.wrap(function *(repo, commit) { return exports.parseSubmoduleConfig(data); }); +/** + * Return a map from submodule name to url in the specified `repo` by parsing + * the blob content of `.gitmodules` + * + * @private + * @async + * @param {NodeGit.Repository} repo repo where blob can be read + * @param {NodeGit.IndexEntry} entry index entry for `.gitmodules` + * @return {Object} map from name to url + */ +exports.getSubmodulesFromIndexEntry = co.wrap(function *(repo, entry) { + assert.instanceOf(repo, NodeGit.Repository); + if (!entry) { + return {}; // RETURN + } + assert.instanceOf(entry, NodeGit.IndexEntry); + const blob = yield repo.getBlob(entry.id); + return exports.parseSubmoduleConfig(blob.toString()); +}); + /** * Return a map from submodule name to url in the specified `repo`. * @@ -399,13 +419,7 @@ exports.getSubmodulesFromIndex = co.wrap(function *(repo, index) { assert.instanceOf(index, NodeGit.Index); const entry = index.getByPath(exports.modulesFileName); - if (undefined === entry) { - return {}; // RETURN - } - const oid = entry.id; - const blob = yield repo.getBlob(oid); - const data = blob.toString(); - return exports.parseSubmoduleConfig(data); + return yield exports.getSubmodulesFromIndexEntry(repo, entry); }); /** @@ -639,7 +653,7 @@ exports.writeConfigText = function (urls) { for (let i = 0; i < keys.length; ++i) { name = keys[i]; url = urls[name]; - result += `\ + result += `\n\ [submodule "${name}"] \tpath = ${name} \turl = ${url} diff --git a/node/package-lock.json b/node/package-lock.json index 7879e543e..2d4d9bca0 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -5,33 +5,33 @@ "requires": true, "dependencies": { "@sinonjs/commons": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", - "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz", + "integrity": "sha512-Debi3Baff1Qu1Unc3mjJ96MgpbwTn43S1+9yJ0llWygPwDNu2aaWBD6yc9y/Z8XDRNhx7U+u2UDg2OGQXkclUQ==", "dev": true, "requires": { "type-detect": "4.0.8" } }, "@sinonjs/formatio": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", - "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", "dev": true, "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" + "@sinonjs/commons": "1.7.1", + "@sinonjs/samsam": "3.3.3" } }, "@sinonjs/samsam": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.2.tgz", - "integrity": "sha512-ILO/rR8LfAb60Y1Yfp9vxfYAASK43NFC2mLzpvLUbCQY/Qu8YwReboseu8aheCEkyElZF2L2T9mHcR2bgdvZyA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", "dev": true, "requires": { - "@sinonjs/commons": "^1.0.2", - "array-from": "^2.1.1", - "lodash": "^4.17.11" + "@sinonjs/commons": "1.7.1", + "array-from": "2.1.1", + "lodash": "4.17.15" } }, "@sinonjs/text-encoding": { @@ -46,14 +46,14 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "3.1.1", + "fast-json-stable-stringify": "2.1.0", + "json-schema-traverse": "0.4.1", + "uri-js": "4.2.2" } }, "amdefine": { @@ -79,7 +79,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "1.9.3" } }, "any-promise": { @@ -97,8 +97,8 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "delegates": "1.0.0", + "readable-stream": "2.3.7" } }, "argparse": { @@ -106,7 +106,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "requires": { - "sprintf-js": "~1.0.2" + "sprintf-js": "1.0.3" } }, "array-from": { @@ -125,7 +125,7 @@ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", "requires": { - "safer-buffer": "~2.1.0" + "safer-buffer": "2.1.2" } }, "assert-plus": { @@ -154,9 +154,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" }, "balanced-match": { "version": "1.0.0", @@ -168,7 +168,7 @@ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "requires": { - "tweetnacl": "^0.14.3" + "tweetnacl": "0.14.5" } }, "binary-search": { @@ -181,8 +181,8 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2" } }, "block-stream": { @@ -190,7 +190,7 @@ "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", "requires": { - "inherits": "~2.0.0" + "inherits": "2.0.4" } }, "brace-expansion": { @@ -198,7 +198,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { - "balanced-match": "^1.0.0", + "balanced-match": "1.0.0", "concat-map": "0.0.1" } }, @@ -213,8 +213,8 @@ "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" + "buffer-alloc-unsafe": "1.1.0", + "buffer-fill": "1.0.0" } }, "buffer-alloc-unsafe": { @@ -243,12 +243,12 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" + "assertion-error": "1.1.0", + "check-error": "1.0.2", + "deep-eql": "3.0.1", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.8" } }, "chalk": { @@ -257,9 +257,9 @@ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.5.0" }, "dependencies": { "has-flag": { @@ -274,7 +274,7 @@ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" } } } @@ -289,15 +289,15 @@ "resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz", "integrity": "sha1-RzChHvYQ+tRQuPIjx50x172tgHQ=", "requires": { - "cross-spawn": "^4.0.2", - "node-version": "^1.0.0", - "promise-polyfill": "^6.0.1" + "cross-spawn": "4.0.2", + "node-version": "1.2.0", + "promise-polyfill": "6.1.0" } }, "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "circular-json": { "version": "0.5.9", @@ -311,24 +311,24 @@ "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", "requires": { "exit": "0.1.2", - "glob": "^7.1.1" + "glob": "7.1.6" } }, "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "dev": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" + "string-width": "3.1.0", + "strip-ansi": "5.2.0", + "wrap-ansi": "5.1.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "is-fullwidth-code-point": { @@ -338,22 +338,23 @@ "dev": true }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "emoji-regex": "7.0.3", + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "5.2.0" } }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "4.1.0" } } } @@ -393,7 +394,7 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "requires": { - "delayed-stream": "~1.0.0" + "delayed-stream": "1.0.0" } }, "commander": { @@ -417,7 +418,7 @@ "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", "requires": { - "date-now": "^0.1.4" + "date-now": "0.1.4" } }, "console-control-strings": { @@ -435,8 +436,8 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" + "lru-cache": "4.1.5", + "which": "1.3.1" } }, "dashdash": { @@ -444,7 +445,7 @@ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { - "assert-plus": "^1.0.0" + "assert-plus": "1.0.0" } }, "date-now": { @@ -457,7 +458,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "decamelize": { @@ -471,7 +472,7 @@ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", "requires": { - "type-detect": "^4.0.0" + "type-detect": "4.0.8" } }, "deep-extend": { @@ -489,7 +490,7 @@ "resolved": "https://registry.npmjs.org/deepcopy/-/deepcopy-2.0.0.tgz", "integrity": "sha512-d5ZK7pJw7F3k6M5vqDjGiiUS9xliIyWkdzBjnPhnSeRGjkYOGZMCFkdKVwV/WiHOe0NwzB8q+iDo7afvSf0arA==", "requires": { - "type-detect": "^4.0.8" + "type-detect": "4.0.8" } }, "deeper": { @@ -503,7 +504,7 @@ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "requires": { - "object-keys": "^1.0.12" + "object-keys": "1.1.1" } }, "delayed-stream": { @@ -528,12 +529,12 @@ "dev": true }, "dom-serializer": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz", - "integrity": "sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" + "domelementtype": "2.0.1", + "entities": "2.0.0" }, "dependencies": { "domelementtype": { @@ -558,7 +559,7 @@ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", "requires": { - "domelementtype": "1" + "domelementtype": "1.3.1" } }, "domutils": { @@ -566,8 +567,8 @@ "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", "requires": { - "dom-serializer": "0", - "domelementtype": "1" + "dom-serializer": "0.2.2", + "domelementtype": "1.3.1" } }, "ecc-jsbn": { @@ -575,8 +576,8 @@ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" + "jsbn": "0.1.1", + "safer-buffer": "2.1.2" } }, "emoji-regex": { @@ -586,11 +587,11 @@ "dev": true }, "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "requires": { - "once": "^1.4.0" + "once": "1.4.0" } }, "entities": { @@ -599,28 +600,33 @@ "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" }, "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "dev": true, "requires": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" + "es-to-primitive": "1.2.1", + "function-bind": "1.1.1", + "has": "1.0.3", + "has-symbols": "1.0.1", + "is-callable": "1.1.5", + "is-regex": "1.0.5", + "object-inspect": "1.7.0", + "object-keys": "1.1.1", + "object.assign": "4.1.0", + "string.prototype.trimleft": "2.1.1", + "string.prototype.trimright": "2.1.1" } }, "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "1.1.5", + "is-date-object": "1.0.2", + "is-symbol": "1.0.3" } }, "escape-string-regexp": { @@ -634,11 +640,11 @@ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", "requires": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.2.0" + "esprima": "2.7.3", + "estraverse": "1.9.3", + "esutils": "2.0.3", + "optionator": "0.8.3", + "source-map": "0.2.0" } }, "esprima": { @@ -656,42 +662,6 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - } - } - }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -708,14 +678,14 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" }, "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fast-levenshtein": { "version": "2.0.6", @@ -728,7 +698,7 @@ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "locate-path": "3.0.0" } }, "flat": { @@ -737,7 +707,7 @@ "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", "dev": true, "requires": { - "is-buffer": "~2.0.3" + "is-buffer": "2.0.4" } }, "foreachasync": { @@ -755,9 +725,9 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "asynckit": "0.4.0", + "combined-stream": "1.0.8", + "mime-types": "2.1.26" } }, "fs-constants": { @@ -770,16 +740,16 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.1.2.tgz", "integrity": "sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU=", "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0" + "graceful-fs": "4.2.3", + "jsonfile": "2.4.0" } }, "fs-minipass": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz", - "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", "requires": { - "minipass": "^2.2.1" + "minipass": "2.9.0" } }, "fs-promise": { @@ -787,10 +757,10 @@ "resolved": "https://registry.npmjs.org/fs-promise/-/fs-promise-2.0.3.tgz", "integrity": "sha1-9k5PhUvPaJqovdy6JokW2z20aFQ=", "requires": { - "any-promise": "^1.3.0", - "fs-extra": "^2.0.0", - "mz": "^2.6.0", - "thenify-all": "^1.6.0" + "any-promise": "1.3.0", + "fs-extra": "2.1.2", + "mz": "2.7.0", + "thenify-all": "1.6.0" } }, "fs.realpath": { @@ -803,18 +773,31 @@ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" + "graceful-fs": "4.2.3", + "inherits": "2.0.4", + "mkdirp": "0.5.1", + "rimraf": "2.7.1" }, "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { - "glob": "^7.1.3" + "glob": "7.1.6" } } } @@ -830,14 +813,14 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.3" } }, "get-caller-file": { @@ -851,59 +834,38 @@ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - }, - "dependencies": { - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { - "assert-plus": "^1.0.0" + "assert-plus": "1.0.0" } }, "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.4", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" } }, "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, "group-by": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/group-by/-/group-by-0.0.1.tgz", "integrity": "sha1-hXYgV19nFHhvjYa7Gf0T4YjdaKQ=", "requires": { - "to-function": "*" + "to-function": "2.0.6" } }, "growl": { @@ -913,14 +875,14 @@ "dev": true }, "handlebars": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz", - "integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==", - "requires": { - "neo-async": "^2.6.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.3.tgz", + "integrity": "sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg==", + "requires": { + "neo-async": "2.6.1", + "optimist": "0.6.1", + "source-map": "0.6.1", + "uglify-js": "3.8.0" }, "dependencies": { "source-map": { @@ -940,8 +902,8 @@ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" + "ajv": "6.12.0", + "har-schema": "2.0.0" } }, "has": { @@ -950,7 +912,7 @@ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { - "function-bind": "^1.1.1" + "function-bind": "1.1.1" } }, "has-flag": { @@ -959,9 +921,9 @@ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" }, "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true }, "has-unicode": { @@ -980,11 +942,11 @@ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", "requires": { - "domelementtype": "1", - "domhandler": "2.3", - "domutils": "1.5", - "entities": "1.0", - "readable-stream": "1.1" + "domelementtype": "1.3.1", + "domhandler": "2.3.0", + "domutils": "1.5.1", + "entities": "1.0.0", + "readable-stream": "1.1.14" }, "dependencies": { "isarray": { @@ -997,10 +959,10 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", + "core-util-is": "1.0.2", + "inherits": "2.0.4", "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "string_decoder": "0.10.31" } }, "string_decoder": { @@ -1015,9 +977,9 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.16.1" } }, "iconv-lite": { @@ -1025,15 +987,15 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": "2.1.2" } }, "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", "requires": { - "minimatch": "^3.0.4" + "minimatch": "3.0.4" } }, "inflight": { @@ -1041,42 +1003,36 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { - "once": "^1.3.0", - "wrappy": "1" + "once": "1.4.0", + "wrappy": "1.0.2" } }, "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", "dev": true }, "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", "dev": true }, "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", "dev": true }, "is-fullwidth-code-point": { @@ -1084,31 +1040,25 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { - "number-is-nan": "^1.0.0" + "number-is-nan": "1.0.1" } }, "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", "dev": true, "requires": { - "has": "^1.0.1" + "has": "1.0.3" } }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "dev": true, "requires": { - "has-symbols": "^1.0.0" + "has-symbols": "1.0.1" } }, "is-typedarray": { @@ -1136,20 +1086,20 @@ "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", "requires": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" + "abbrev": "1.0.9", + "async": "1.5.2", + "escodegen": "1.8.1", + "esprima": "2.7.3", + "glob": "5.0.15", + "handlebars": "4.7.3", + "js-yaml": "3.13.1", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "once": "1.4.0", + "resolve": "1.1.7", + "supports-color": "3.2.3", + "which": "1.3.1", + "wordwrap": "1.0.0" }, "dependencies": { "abbrev": { @@ -1162,11 +1112,24 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "inflight": "1.0.6", + "inherits": "2.0.4", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" } } } @@ -1176,8 +1139,8 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "1.0.10", + "esprima": "4.0.1" }, "dependencies": { "esprima": { @@ -1193,18 +1156,18 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jshint": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.10.2.tgz", - "integrity": "sha512-e7KZgCSXMJxznE/4WULzybCMNXNAd/bf5TSrvVEq78Q/K8ZwFpmBqQeDtNiHc3l49nV4E/+YeHU/JZjSUIrLAA==", - "requires": { - "cli": "~1.0.0", - "console-browserify": "1.1.x", - "exit": "0.1.x", - "htmlparser2": "3.8.x", - "lodash": "~4.17.11", - "minimatch": "~3.0.2", - "shelljs": "0.3.x", - "strip-json-comments": "1.0.x" + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.11.0.tgz", + "integrity": "sha512-ooaD/hrBPhu35xXW4gn+o3SOuzht73gdBuffgJzrZBJZPGgGiiTvJEgTyxFvBO2nz0+X1G6etF8SzUODTlLY6Q==", + "requires": { + "cli": "1.0.1", + "console-browserify": "1.1.0", + "exit": "0.1.2", + "htmlparser2": "3.8.3", + "lodash": "4.17.15", + "minimatch": "3.0.4", + "shelljs": "0.3.0", + "strip-json-comments": "1.0.4" }, "dependencies": { "shelljs": { @@ -1235,11 +1198,11 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", + "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", "requires": { - "minimist": "^1.2.0" + "minimist": "1.2.0" } }, "jsonfile": { @@ -1247,7 +1210,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", "requires": { - "graceful-fs": "^4.1.6" + "graceful-fs": "4.2.3" } }, "jsprim": { @@ -1262,27 +1225,18 @@ } }, "just-extend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", - "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz", + "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==", "dev": true }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" + "prelude-ls": "1.1.2", + "type-check": "0.3.2" } }, "locate-path": { @@ -1291,8 +1245,8 @@ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "p-locate": "3.0.0", + "path-exists": "3.0.0" } }, "lodash": { @@ -1306,13 +1260,13 @@ "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", "dev": true, "requires": { - "chalk": "^2.0.1" + "chalk": "2.4.2" } }, "lolex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.1.0.tgz", - "integrity": "sha512-BYxIEXiVq5lGIXeVHnsFzqa1TxN5acnKnPCdlZSpzm8viNEOhiigupA4vTQ9HEFQ6nLTQ9wQOgBknJgzUYQ9Aw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", "dev": true }, "lru-cache": { @@ -1320,55 +1274,29 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" + "pseudomap": "1.0.2", + "yallist": "2.1.2" } }, "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" }, "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", "requires": { - "mime-db": "1.40.0" + "mime-db": "1.43.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "1.1.11" } }, "minimist": { @@ -1377,48 +1305,38 @@ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "minipass": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" + "safe-buffer": "5.1.2", + "yallist": "3.1.1" }, "dependencies": { "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } } }, "minizlib": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", "requires": { - "minipass": "^2.2.1" + "minipass": "2.9.0" } }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } - } + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", + "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==" }, "mocha": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", - "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.2.tgz", + "integrity": "sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==", "dev": true, "requires": { "ansi-colors": "3.2.3", @@ -1441,9 +1359,9 @@ "supports-color": "6.0.0", "which": "1.3.1", "wide-align": "1.1.3", - "yargs": "13.2.2", - "yargs-parser": "13.0.0", - "yargs-unparser": "1.5.0" + "yargs": "13.3.0", + "yargs-parser": "13.1.1", + "yargs-unparser": "1.6.0" }, "dependencies": { "glob": { @@ -1452,12 +1370,12 @@ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.4", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" } }, "has-flag": { @@ -1466,13 +1384,34 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, "supports-color": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" } } } @@ -1482,22 +1421,22 @@ "resolved": "https://registry.npmjs.org/mocha-jshint/-/mocha-jshint-2.3.1.tgz", "integrity": "sha1-MD8n5TOThVnSDyakEj1by1ZFpUY=", "requires": { - "jshint": "^2.8.0", - "minimatch": "^3.0.0", - "shelljs": "^0.4.0", - "uniq": "^1.0.1" + "jshint": "2.11.0", + "minimatch": "3.0.4", + "shelljs": "0.4.0", + "uniq": "1.0.1" } }, "mocha-parallel-tests": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mocha-parallel-tests/-/mocha-parallel-tests-2.1.2.tgz", - "integrity": "sha512-FatFg3MHLio9ir1oP6J0HNEo6R5344JC1y+We90iALdiT9F9xNPN0KbGXxRNlGlSl0GodfSESKbRzBvT9ctgIw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/mocha-parallel-tests/-/mocha-parallel-tests-2.3.0.tgz", + "integrity": "sha512-GdU55VkOCXK2aBmjFzBE11RsSolA71WJppucU3BrTbMUpGIWnTidrje5Hvdr+hOolynEiv6fHWyoNT41mq4HkA==", "dev": true, "requires": { - "circular-json": "^0.5.9", - "debug": "^4.1.1", - "uuid": "^3.3.2", - "yargs": "^13.2.2" + "circular-json": "0.5.9", + "debug": "4.1.1", + "uuid": "3.4.0", + "yargs": "13.3.0" }, "dependencies": { "debug": { @@ -1506,24 +1445,24 @@ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } } } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "requires": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "any-promise": "1.3.0", + "object-assign": "4.1.1", + "thenify-all": "1.6.0" } }, "nan": { @@ -1532,13 +1471,13 @@ "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, "needle": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", - "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.2.tgz", + "integrity": "sha512-DUzITvPVDUy6vczKKYTnWc/pBZ0EnjMJnQ3y+Jo5zfKFimJs7S3HFCxCRZYB9FUZcrzUQr3WsmvZgddMEIZv6w==", "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" + "debug": "3.2.6", + "iconv-lite": "0.4.24", + "sax": "1.2.4" } }, "neo-async": { @@ -1546,23 +1485,28 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "nise": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.0.tgz", - "integrity": "sha512-Z3sfYEkLFzFmL8KY6xnSJLRxwQwYBjOXi/24lb62ZnZiGA0JUzGGTI6TBIgfCSMIDl9Jlu8SRmHNACLTemDHww==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", + "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", "dev": true, "requires": { - "@sinonjs/formatio": "^3.1.0", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "lolex": "^4.1.0", - "path-to-regexp": "^1.7.0" + "@sinonjs/formatio": "3.2.2", + "@sinonjs/text-encoding": "0.7.1", + "just-extend": "4.1.0", + "lolex": "5.1.2", + "path-to-regexp": "1.8.0" + }, + "dependencies": { + "lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "requires": { + "@sinonjs/commons": "1.7.1" + } + } } }, "node-environment-flags": { @@ -1571,14 +1515,14 @@ "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", "dev": true, "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" + "object.getownpropertydescriptors": "2.1.0", + "semver": "5.7.1" }, "dependencies": { "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true } } @@ -1588,26 +1532,39 @@ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" + "fstream": "1.0.12", + "glob": "7.1.6", + "graceful-fs": "4.2.3", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "npmlog": "4.1.2", + "osenv": "0.1.5", + "request": "2.88.2", + "rimraf": "2.7.1", + "semver": "5.3.0", + "tar": "2.2.2", + "which": "1.3.1" }, "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { - "glob": "^7.1.3" + "glob": "7.1.6" } } } @@ -1617,25 +1574,38 @@ "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.3.2", + "nopt": "4.0.1", + "npm-packlist": "1.4.8", + "npmlog": "4.1.2", + "rc": "1.2.8", + "rimraf": "2.7.1", + "semver": "5.3.0", + "tar": "4.4.13" }, "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, "nopt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "requires": { - "abbrev": "1", - "osenv": "^0.1.4" + "abbrev": "1.1.1", + "osenv": "0.1.5" } }, "rimraf": { @@ -1643,27 +1613,27 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { - "glob": "^7.1.3" + "glob": "7.1.6" } }, "tar": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.9.tgz", - "integrity": "sha512-xisFa7Q2i3HOgfn+nmnWLGHD6Tm23hxjkx6wwGmgxkJFr6wxwXnJOdJYcZjL453PSdF0+bemO03+flAzkIdLBQ==", + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.5", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" + "chownr": "1.1.4", + "fs-minipass": "1.2.7", + "minipass": "2.9.0", + "minizlib": "1.3.3", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.2", + "yallist": "3.1.1" } }, "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } } }, @@ -1677,16 +1647,16 @@ "resolved": "https://registry.npmjs.org/nodegit/-/nodegit-0.23.1.tgz", "integrity": "sha512-k1bCg260yZlmfLGYPrinqAQNbQ6PnBJhKq6wWfe94yIO3WnOOyoSuweAkAGPR6FhrGq2LkCKHdJaitwhKCEGyA==", "requires": { - "fs-extra": "^7.0.0", - "json5": "^2.1.0", - "lodash": "^4.17.11", - "nan": "^2.11.1", - "node-gyp": "^3.8.0", - "node-pre-gyp": "^0.11.0", - "promisify-node": "~0.3.0", - "ramda": "^0.25.0", - "request-promise-native": "^1.0.5", - "tar-fs": "^1.16.3" + "fs-extra": "7.0.1", + "json5": "2.1.1", + "lodash": "4.17.15", + "nan": "2.14.0", + "node-gyp": "3.8.0", + "node-pre-gyp": "0.11.0", + "promisify-node": "0.3.0", + "ramda": "0.25.0", + "request-promise-native": "1.0.8", + "tar-fs": "1.16.3" }, "dependencies": { "fs-extra": { @@ -1694,9 +1664,9 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "graceful-fs": "4.2.3", + "jsonfile": "4.0.0", + "universalify": "0.1.2" } }, "jsonfile": { @@ -1704,7 +1674,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "requires": { - "graceful-fs": "^4.1.6" + "graceful-fs": "4.2.3" } } } @@ -1714,7 +1684,7 @@ "resolved": "https://registry.npmjs.org/nodegit-promise/-/nodegit-promise-4.0.0.tgz", "integrity": "sha1-VyKxhPLfcycWEGSnkdLoQskWezQ=", "requires": { - "asap": "~2.0.3" + "asap": "2.0.6" } }, "nopt": { @@ -1722,30 +1692,30 @@ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "requires": { - "abbrev": "1" + "abbrev": "1.1.1" } }, "npm-bundled": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", - "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" - }, - "npm-packlist": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", - "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" + "npm-normalize-package-bin": "1.0.1" } }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" + }, + "npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", "requires": { - "path-key": "^2.0.0" + "ignore-walk": "3.0.3", + "npm-bundled": "1.1.1", + "npm-normalize-package-bin": "1.0.1" } }, "npmlog": { @@ -1753,10 +1723,10 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" + "are-we-there-yet": "1.1.5", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" } }, "number-is-nan": { @@ -1774,6 +1744,12 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -1786,20 +1762,20 @@ "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" + "define-properties": "1.1.3", + "function-bind": "1.1.1", + "has-symbols": "1.0.1", + "object-keys": "1.1.1" } }, "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" + "define-properties": "1.1.3", + "es-abstract": "1.17.4" } }, "once": { @@ -1807,7 +1783,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { - "wrappy": "1" + "wrappy": "1.0.2" } }, "optimist": { @@ -1815,8 +1791,8 @@ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" + "minimist": "0.0.10", + "wordwrap": "0.0.3" }, "dependencies": { "minimist": { @@ -1832,16 +1808,16 @@ } }, "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "word-wrap": "1.2.3" } }, "os-homedir": { @@ -1849,17 +1825,6 @@ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -1870,35 +1835,17 @@ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" } }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", "dev": true, "requires": { - "p-try": "^2.0.0" + "p-try": "2.2.0" } }, "p-locate": { @@ -1907,7 +1854,7 @@ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "requires": { - "p-limit": "^2.0.0" + "p-limit": "2.2.2" } }, "p-try": { @@ -1927,16 +1874,10 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", "dev": true, "requires": { "isarray": "0.0.1" @@ -1966,9 +1907,9 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "promise-polyfill": { "version": "6.1.0", @@ -1980,7 +1921,7 @@ "resolved": "https://registry.npmjs.org/promisify-node/-/promisify-node-0.3.0.tgz", "integrity": "sha1-tLVaz5D6p9K4uQyjlomQhsAwYM8=", "requires": { - "nodegit-promise": "~4.0.0" + "nodegit-promise": "4.0.0" } }, "pseudomap": { @@ -1989,17 +1930,17 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { - "version": "1.1.32", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", - "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" }, "pump": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "end-of-stream": "1.4.4", + "once": "1.4.0" } }, "punycode": { @@ -2022,69 +1963,69 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" + "deep-extend": "0.6.0", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" } }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "core-util-is": "1.0.2", + "inherits": "2.0.4", + "isarray": "1.0.0", + "process-nextick-args": "2.0.1", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" } }, "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.9.1", + "caseless": "0.12.0", + "combined-stream": "1.0.8", + "extend": "3.0.2", + "forever-agent": "0.6.1", + "form-data": "2.3.3", + "har-validator": "5.1.3", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.26", + "oauth-sign": "0.9.0", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "tough-cookie": "2.5.0", + "tunnel-agent": "0.6.0", + "uuid": "3.4.0" } }, "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", "requires": { - "lodash": "^4.17.11" + "lodash": "4.17.15" } }, "request-promise-native": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", - "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", "requires": { - "request-promise-core": "1.1.2", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" + "request-promise-core": "1.1.3", + "stealthy-require": "1.1.1", + "tough-cookie": "2.5.0" } }, "require-directory": { @@ -2105,11 +2046,11 @@ "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" }, "rimraf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", - "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "requires": { - "glob": "^7.1.3" + "glob": "7.1.6" } }, "safe-buffer": { @@ -2137,21 +2078,6 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, "shelljs": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.4.0.tgz", @@ -2163,18 +2089,18 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "sinon": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz", - "integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", + "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", "dev": true, "requires": { - "@sinonjs/commons": "^1.4.0", - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/samsam": "^3.3.1", - "diff": "^3.5.0", - "lolex": "^4.0.1", - "nise": "^1.4.10", - "supports-color": "^5.5.0" + "@sinonjs/commons": "1.7.1", + "@sinonjs/formatio": "3.2.2", + "@sinonjs/samsam": "3.3.3", + "diff": "3.5.0", + "lolex": "4.2.0", + "nise": "1.5.3", + "supports-color": "5.5.0" }, "dependencies": { "has-flag": { @@ -2189,7 +2115,7 @@ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" } } } @@ -2200,7 +2126,7 @@ "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", "optional": true, "requires": { - "amdefine": ">=0.0.4" + "amdefine": "1.0.1" } }, "split": { @@ -2208,7 +2134,7 @@ "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", "requires": { - "through": "2" + "through": "2.3.8" } }, "sprintf-js": { @@ -2221,15 +2147,15 @@ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" + "asn1": "0.2.4", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.2", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.2", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "safer-buffer": "2.1.2", + "tweetnacl": "0.14.5" } }, "stealthy-require": { @@ -2242,9 +2168,29 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "1.1.3", + "function-bind": "1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "1.1.3", + "function-bind": "1.1.1" } }, "string_decoder": { @@ -2252,7 +2198,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "5.1.2" } }, "strip-ansi": { @@ -2260,15 +2206,9 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { - "ansi-regex": "^2.0.0" + "ansi-regex": "2.1.1" } }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -2279,7 +2219,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", "requires": { - "has-flag": "^1.0.0" + "has-flag": "1.0.0" } }, "tar": { @@ -2287,9 +2227,9 @@ "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" + "block-stream": "0.0.9", + "fstream": "1.0.12", + "inherits": "2.0.4" } }, "tar-fs": { @@ -2297,10 +2237,25 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", "requires": { - "chownr": "^1.0.1", - "mkdirp": "^0.5.1", - "pump": "^1.0.0", - "tar-stream": "^1.1.2" + "chownr": "1.1.4", + "mkdirp": "0.5.1", + "pump": "1.0.3", + "tar-stream": "1.6.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + } } }, "tar-stream": { @@ -2308,21 +2263,21 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", "requires": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" + "bl": "1.2.2", + "buffer-alloc": "1.2.0", + "end-of-stream": "1.4.4", + "fs-constants": "1.0.0", + "readable-stream": "2.3.7", + "to-buffer": "1.1.1", + "xtend": "4.0.2" } }, "temp": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.0.tgz", - "integrity": "sha512-YfUhPQCJoNQE5N+FJQcdPz63O3x3sdT4Xju69Gj4iZe0lBKOtnAMi0SLj9xKhGkcGhsxThvTJ/usxtFPo438zQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.1.tgz", + "integrity": "sha512-WMuOgiua1xb5R56lE0eH6ivpVmg/lq2OHm4+LtT/xtEtPQ+sz6N3bBM6WZ5FvO1lO4IKIOb43qnhoc4qxP5OeA==", "requires": { - "rimraf": "~2.6.2" + "rimraf": "2.6.3" }, "dependencies": { "rimraf": { @@ -2330,7 +2285,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "requires": { - "glob": "^7.1.3" + "glob": "7.1.6" } } } @@ -2340,7 +2295,7 @@ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", "requires": { - "any-promise": "^1.0.0" + "any-promise": "1.3.0" } }, "thenify-all": { @@ -2348,7 +2303,7 @@ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", "requires": { - "thenify": ">= 3.1.0 < 4" + "thenify": "3.3.0" } }, "through": { @@ -2366,23 +2321,16 @@ "resolved": "https://registry.npmjs.org/to-function/-/to-function-2.0.6.tgz", "integrity": "sha1-fVbNnDuS+o29eyLoPVGSTedA68U=", "requires": { - "component-props": "*" + "component-props": "1.1.1" } }, "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } + "psl": "1.7.0", + "punycode": "2.1.1" } }, "tunnel-agent": { @@ -2390,7 +2338,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { - "safe-buffer": "^5.0.1" + "safe-buffer": "5.1.2" } }, "tweetnacl": { @@ -2403,7 +2351,7 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "requires": { - "prelude-ls": "~1.1.2" + "prelude-ls": "1.1.2" } }, "type-detect": { @@ -2412,13 +2360,13 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" }, "uglify-js": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.5.tgz", - "integrity": "sha512-7L3W+Npia1OCr5Blp4/Vw83tK1mu5gnoIURtT1fUVfQ3Kf8WStWV6NJz0fdoBJZls0KlweruRTLVe6XLafmy5g==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.0.tgz", + "integrity": "sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ==", "optional": true, "requires": { - "commander": "~2.20.3", - "source-map": "~0.6.1" + "commander": "2.20.3", + "source-map": "0.6.1" }, "dependencies": { "source-map": { @@ -2444,7 +2392,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "requires": { - "punycode": "^2.1.0" + "punycode": "2.1.1" } }, "util-deprecate": { @@ -2453,18 +2401,18 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { - "assert-plus": "^1.0.0", + "assert-plus": "1.0.0", "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" + "extsprintf": "1.3.0" } }, "walk": { @@ -2472,7 +2420,7 @@ "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.14.tgz", "integrity": "sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg==", "requires": { - "foreachasync": "^3.0.0" + "foreachasync": "3.0.0" } }, "which": { @@ -2480,7 +2428,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "requires": { - "isexe": "^2.0.0" + "isexe": "2.0.0" } }, "which-module": { @@ -2494,22 +2442,62 @@ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "requires": { - "string-width": "^1.0.2 || 2" + "string-width": "1.0.2" } }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" }, "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "dev": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "ansi-styles": "3.2.1", + "string-width": "3.1.0", + "strip-ansi": "5.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "7.0.3", + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "5.2.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "4.1.0" + } + } } }, "wrappy": { @@ -2518,9 +2506,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { "version": "4.0.0", @@ -2534,22 +2522,21 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, "yargs": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", - "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", "dev": true, "requires": { - "cliui": "^4.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" + "cliui": "5.0.0", + "find-up": "3.0.0", + "get-caller-file": "2.0.5", + "require-directory": "2.1.1", + "require-main-filename": "2.0.0", + "set-blocking": "2.0.0", + "string-width": "3.1.0", + "which-module": "2.0.0", + "y18n": "4.0.0", + "yargs-parser": "13.1.1" }, "dependencies": { "ansi-regex": { @@ -2570,9 +2557,9 @@ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "7.0.3", + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "5.2.0" } }, "strip-ansi": { @@ -2581,105 +2568,30 @@ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "4.1.0" } } } }, "yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", "dev": true, "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "camelcase": "5.3.1", + "decamelize": "1.2.0" } }, "yargs-unparser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", - "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", "dev": true, "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.11", - "yargs": "^12.0.5" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } + "flat": "4.1.0", + "lodash": "4.17.15", + "yargs": "13.3.0" } } } diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index 14cab55cc..069cd7b2c 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -967,3 +967,54 @@ a }); }); }); + +describe("resolveUrlConflicts", function() { + const cases = { + "choose our urls": { + ancestors: { a: "a", b: "b", c: "c" }, + ours: { a: "../a", c: "c", d: "d" }, + theirs: { a: "a", b: "b", c: "c" }, + expected: { a: "../a", c: "c", d: "d" }, + numConflict: 0 + }, + "choose their urls": { + ancestors: { a: "a", b: "b", c: "c" }, + theirs: { a: "../a", c: "c", d: "d" }, + ours: { a: "a", b: "b", c: "c" }, + expected: { a: "../a", c: "c", d: "d" }, + numConflict: 0 + }, + "choose new and consensus": { + ancestors: { a: "a", b: "b", c: "c" }, + ours: { a: "../a", c: "c", d: "d" }, + theirs: { a: "../a", c: "c", d: "d" }, + expected: { a: "../a", c: "c", d: "d" }, + numConflict: 0 + }, + conflict: { + ancestors: { a: "a" }, + ours: { a: "x" }, + theirs: { a: "y" }, + expected: {}, + numConflict: 1 + } + }; + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it( + caseName, + function() { + const result = CherryPickUtil.resolveUrlsConflicts( + c.ancestors, + c.ours, + c.theirs + ); + assert.equal( + Object.keys(result.conflicts).length, + c.numConflict + ); + assert.deepEqual(result.urls, c.expected); + } + ); + }); +}); From 5c1ecd77339dfe33e013418c7e70494f823c9fc0 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Tue, 3 Mar 2020 11:34:27 -0500 Subject: [PATCH 276/402] Express fetch error during merging more explicitly --- node/lib/git-meta.js | 1 + node/lib/util/git_util.js | 2 +- node/lib/util/merge_util.js | 16 ++++++++++++---- node/lib/util/user_error.js | 13 ++++++++++++- node/test/util/git_util.js | 1 + 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js index 9588910a7..7502fc86e 100755 --- a/node/lib/git-meta.js +++ b/node/lib/git-meta.js @@ -100,6 +100,7 @@ function configureSubcommand(parser, commandName, module, skipNodeGit) { if (error instanceof UserError) { console.error(error.message); + process.exit(error.code); } else { console.error(error.stack); diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index c354f24a7..431e6c3a1 100755 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -581,7 +581,7 @@ ${sha}:${syntheticName}`; yield repo.getCommit(sha); } catch (e) { - throw new UserError(e.message); + throw new UserError(e.message, UserError.CODES.FETCH_ERROR); } return true; }); diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 5b73940c4..a8919b403 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -230,9 +230,17 @@ exports.mergeSubmodule = co.wrap(function *(metaIndex, const isHalfOpened = yield opener.isHalfOpened(subName); const forceBare = openOption === SUB_OPEN_OPTION.FORCE_BARE; const theirSha = change.newSha; - yield fetcher.fetchSha(subRepo, subName, theirSha); - if (null !== change.ourSha) { - yield fetcher.fetchSha(subRepo, subName, change.ourSha); + try { + yield fetcher.fetchSha(subRepo, subName, theirSha); + if (null !== change.ourSha) { + yield fetcher.fetchSha(subRepo, subName, change.ourSha); + } + } catch (e) { + console.log( + `Unable to fetch changes in submodule '${subName}', ` + + "abort merging." + ); + throw e; } const theirCommit = yield subRepo.getCommit(theirSha); @@ -536,7 +544,7 @@ const mergeStepMergeSubmodules = co.wrap(function *(context) { } // finishing merge for interactive merges - // 1. close unnecessaried opened submodules + // 1. close unnecessarily opened submodules // 2. write the index to the meta repo or the staging we've done earlier // will go away if (!forceBare) { diff --git a/node/lib/util/user_error.js b/node/lib/util/user_error.js index 249e3ef30..d8813eb7b 100644 --- a/node/lib/util/user_error.js +++ b/node/lib/util/user_error.js @@ -30,6 +30,11 @@ */ "use strict"; +const ERROR_CODES = Object.freeze({ + GENERIC: -1, + FETCH_ERROR: -128, +}); + /** * This module contains the `util.UserError` class. */ @@ -44,11 +49,17 @@ * @class UserError */ class UserError extends Error { - constructor(message) { + constructor(message, code) { super(message); this.message = message; this.name = "UserError"; + this.code = code; + if (!this.code) { + this.code = ERROR_CODES.GENERIC; + } } } +UserError.CODES = ERROR_CODES; + module.exports = UserError; diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index 9f0d18ca9..1949b5c50 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -738,6 +738,7 @@ describe("GitUtil", function () { } catch (e) { assert.instanceOf(e, UserError); + assert.equal(e.code, UserError.CODES.FETCH_ERROR); return; } assert(false, "Bad sha, should have failed"); From 5cdb7245fa7932100ca207bcd7a5428634243d05 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Wed, 4 Mar 2020 09:50:57 -0500 Subject: [PATCH 277/402] Revert node_js version bump --- .travis.yml | 2 +- node/package-lock.json | 1656 +++++++++++++++++++++------------------- node/package.json | 2 +- 3 files changed, 874 insertions(+), 786 deletions(-) diff --git a/.travis.yml b/.travis.yml index de044a25b..4a2ae80b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "8.11.1" + - "6.9.1" env: - CC=gcc-5 diff --git a/node/package-lock.json b/node/package-lock.json index 2d4d9bca0..7879e543e 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -5,33 +5,33 @@ "requires": true, "dependencies": { "@sinonjs/commons": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz", - "integrity": "sha512-Debi3Baff1Qu1Unc3mjJ96MgpbwTn43S1+9yJ0llWygPwDNu2aaWBD6yc9y/Z8XDRNhx7U+u2UDg2OGQXkclUQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", + "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", "dev": true, "requires": { "type-detect": "4.0.8" } }, "@sinonjs/formatio": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", - "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", "dev": true, "requires": { - "@sinonjs/commons": "1.7.1", - "@sinonjs/samsam": "3.3.3" + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" } }, "@sinonjs/samsam": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", - "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.2.tgz", + "integrity": "sha512-ILO/rR8LfAb60Y1Yfp9vxfYAASK43NFC2mLzpvLUbCQY/Qu8YwReboseu8aheCEkyElZF2L2T9mHcR2bgdvZyA==", "dev": true, "requires": { - "@sinonjs/commons": "1.7.1", - "array-from": "2.1.1", - "lodash": "4.17.15" + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" } }, "@sinonjs/text-encoding": { @@ -46,14 +46,14 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "ajv": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", - "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", "requires": { - "fast-deep-equal": "3.1.1", - "fast-json-stable-stringify": "2.1.0", - "json-schema-traverse": "0.4.1", - "uri-js": "4.2.2" + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "amdefine": { @@ -79,7 +79,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "1.9.3" + "color-convert": "^1.9.0" } }, "any-promise": { @@ -97,8 +97,8 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.7" + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" } }, "argparse": { @@ -106,7 +106,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "requires": { - "sprintf-js": "1.0.3" + "sprintf-js": "~1.0.2" } }, "array-from": { @@ -125,7 +125,7 @@ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", "requires": { - "safer-buffer": "2.1.2" + "safer-buffer": "~2.1.0" } }, "assert-plus": { @@ -154,9 +154,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", - "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "balanced-match": { "version": "1.0.0", @@ -168,7 +168,7 @@ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "requires": { - "tweetnacl": "0.14.5" + "tweetnacl": "^0.14.3" } }, "binary-search": { @@ -181,8 +181,8 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "requires": { - "readable-stream": "2.3.7", - "safe-buffer": "5.1.2" + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" } }, "block-stream": { @@ -190,7 +190,7 @@ "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", "requires": { - "inherits": "2.0.4" + "inherits": "~2.0.0" } }, "brace-expansion": { @@ -198,7 +198,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, @@ -213,8 +213,8 @@ "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", "requires": { - "buffer-alloc-unsafe": "1.1.0", - "buffer-fill": "1.0.0" + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" } }, "buffer-alloc-unsafe": { @@ -243,12 +243,12 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", "requires": { - "assertion-error": "1.1.0", - "check-error": "1.0.2", - "deep-eql": "3.0.1", - "get-func-name": "2.0.0", - "pathval": "1.1.0", - "type-detect": "4.0.8" + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" } }, "chalk": { @@ -257,9 +257,9 @@ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.5.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "dependencies": { "has-flag": { @@ -274,7 +274,7 @@ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } } } @@ -289,15 +289,15 @@ "resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz", "integrity": "sha1-RzChHvYQ+tRQuPIjx50x172tgHQ=", "requires": { - "cross-spawn": "4.0.2", - "node-version": "1.2.0", - "promise-polyfill": "6.1.0" + "cross-spawn": "^4.0.2", + "node-version": "^1.0.0", + "promise-polyfill": "^6.0.1" } }, "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" }, "circular-json": { "version": "0.5.9", @@ -311,24 +311,24 @@ "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", "requires": { "exit": "0.1.2", - "glob": "7.1.6" + "glob": "^7.1.1" } }, "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", "dev": true, "requires": { - "string-width": "3.1.0", - "strip-ansi": "5.2.0", - "wrap-ansi": "5.1.0" + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, "is-fullwidth-code-point": { @@ -338,23 +338,22 @@ "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "dev": true, "requires": { - "emoji-regex": "7.0.3", - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "5.2.0" + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { - "ansi-regex": "4.1.0" + "ansi-regex": "^3.0.0" } } } @@ -394,7 +393,7 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "requires": { - "delayed-stream": "1.0.0" + "delayed-stream": "~1.0.0" } }, "commander": { @@ -418,7 +417,7 @@ "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", "requires": { - "date-now": "0.1.4" + "date-now": "^0.1.4" } }, "console-control-strings": { @@ -436,8 +435,8 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", "requires": { - "lru-cache": "4.1.5", - "which": "1.3.1" + "lru-cache": "^4.0.1", + "which": "^1.2.9" } }, "dashdash": { @@ -445,7 +444,7 @@ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "date-now": { @@ -458,7 +457,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.1" } }, "decamelize": { @@ -472,7 +471,7 @@ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", "requires": { - "type-detect": "4.0.8" + "type-detect": "^4.0.0" } }, "deep-extend": { @@ -490,7 +489,7 @@ "resolved": "https://registry.npmjs.org/deepcopy/-/deepcopy-2.0.0.tgz", "integrity": "sha512-d5ZK7pJw7F3k6M5vqDjGiiUS9xliIyWkdzBjnPhnSeRGjkYOGZMCFkdKVwV/WiHOe0NwzB8q+iDo7afvSf0arA==", "requires": { - "type-detect": "4.0.8" + "type-detect": "^4.0.8" } }, "deeper": { @@ -504,7 +503,7 @@ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "requires": { - "object-keys": "1.1.1" + "object-keys": "^1.0.12" } }, "delayed-stream": { @@ -529,12 +528,12 @@ "dev": true }, "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz", + "integrity": "sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==", "requires": { - "domelementtype": "2.0.1", - "entities": "2.0.0" + "domelementtype": "^2.0.1", + "entities": "^2.0.0" }, "dependencies": { "domelementtype": { @@ -559,7 +558,7 @@ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", "requires": { - "domelementtype": "1.3.1" + "domelementtype": "1" } }, "domutils": { @@ -567,8 +566,8 @@ "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", "requires": { - "dom-serializer": "0.2.2", - "domelementtype": "1.3.1" + "dom-serializer": "0", + "domelementtype": "1" } }, "ecc-jsbn": { @@ -576,8 +575,8 @@ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "requires": { - "jsbn": "0.1.1", - "safer-buffer": "2.1.2" + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" } }, "emoji-regex": { @@ -587,11 +586,11 @@ "dev": true }, "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", "requires": { - "once": "1.4.0" + "once": "^1.4.0" } }, "entities": { @@ -600,33 +599,28 @@ "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" }, "es-abstract": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", - "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", "dev": true, "requires": { - "es-to-primitive": "1.2.1", - "function-bind": "1.1.1", - "has": "1.0.3", - "has-symbols": "1.0.1", - "is-callable": "1.1.5", - "is-regex": "1.0.5", - "object-inspect": "1.7.0", - "object-keys": "1.1.1", - "object.assign": "4.1.0", - "string.prototype.trimleft": "2.1.1", - "string.prototype.trimright": "2.1.1" + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" } }, "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", "dev": true, "requires": { - "is-callable": "1.1.5", - "is-date-object": "1.0.2", - "is-symbol": "1.0.3" + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" } }, "escape-string-regexp": { @@ -640,11 +634,11 @@ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", "requires": { - "esprima": "2.7.3", - "estraverse": "1.9.3", - "esutils": "2.0.3", - "optionator": "0.8.3", - "source-map": "0.2.0" + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" } }, "esprima": { @@ -662,6 +656,42 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } + } + }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -678,14 +708,14 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "fast-levenshtein": { "version": "2.0.6", @@ -698,7 +728,7 @@ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { - "locate-path": "3.0.0" + "locate-path": "^3.0.0" } }, "flat": { @@ -707,7 +737,7 @@ "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", "dev": true, "requires": { - "is-buffer": "2.0.4" + "is-buffer": "~2.0.3" } }, "foreachasync": { @@ -725,9 +755,9 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.8", - "mime-types": "2.1.26" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" } }, "fs-constants": { @@ -740,16 +770,16 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.1.2.tgz", "integrity": "sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU=", "requires": { - "graceful-fs": "4.2.3", - "jsonfile": "2.4.0" + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0" } }, "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz", + "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", "requires": { - "minipass": "2.9.0" + "minipass": "^2.2.1" } }, "fs-promise": { @@ -757,10 +787,10 @@ "resolved": "https://registry.npmjs.org/fs-promise/-/fs-promise-2.0.3.tgz", "integrity": "sha1-9k5PhUvPaJqovdy6JokW2z20aFQ=", "requires": { - "any-promise": "1.3.0", - "fs-extra": "2.1.2", - "mz": "2.7.0", - "thenify-all": "1.6.0" + "any-promise": "^1.3.0", + "fs-extra": "^2.0.0", + "mz": "^2.6.0", + "thenify-all": "^1.6.0" } }, "fs.realpath": { @@ -773,31 +803,18 @@ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", "requires": { - "graceful-fs": "4.2.3", - "inherits": "2.0.4", - "mkdirp": "0.5.1", - "rimraf": "2.7.1" + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" }, "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { - "glob": "7.1.6" + "glob": "^7.1.3" } } } @@ -813,14 +830,14 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.3" + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" } }, "get-caller-file": { @@ -834,38 +851,59 @@ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.4", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, "group-by": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/group-by/-/group-by-0.0.1.tgz", "integrity": "sha1-hXYgV19nFHhvjYa7Gf0T4YjdaKQ=", "requires": { - "to-function": "2.0.6" + "to-function": "*" } }, "growl": { @@ -875,14 +913,14 @@ "dev": true }, "handlebars": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.3.tgz", - "integrity": "sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg==", - "requires": { - "neo-async": "2.6.1", - "optimist": "0.6.1", - "source-map": "0.6.1", - "uglify-js": "3.8.0" + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz", + "integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==", + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" }, "dependencies": { "source-map": { @@ -902,8 +940,8 @@ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "requires": { - "ajv": "6.12.0", - "har-schema": "2.0.0" + "ajv": "^6.5.5", + "har-schema": "^2.0.0" } }, "has": { @@ -912,7 +950,7 @@ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { - "function-bind": "1.1.1" + "function-bind": "^1.1.1" } }, "has-flag": { @@ -921,9 +959,9 @@ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" }, "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", "dev": true }, "has-unicode": { @@ -942,11 +980,11 @@ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", "requires": { - "domelementtype": "1.3.1", - "domhandler": "2.3.0", - "domutils": "1.5.1", - "entities": "1.0.0", - "readable-stream": "1.1.14" + "domelementtype": "1", + "domhandler": "2.3", + "domutils": "1.5", + "entities": "1.0", + "readable-stream": "1.1" }, "dependencies": { "isarray": { @@ -959,10 +997,10 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.4", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "string_decoder": { @@ -977,9 +1015,9 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.16.1" + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" } }, "iconv-lite": { @@ -987,15 +1025,15 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "requires": { - "safer-buffer": "2.1.2" + "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "requires": { - "minimatch": "3.0.4" + "minimatch": "^3.0.4" } }, "inflight": { @@ -1003,36 +1041,42 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, "is-buffer": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", "dev": true }, "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", "dev": true }, "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", "dev": true }, "is-fullwidth-code-point": { @@ -1040,25 +1084,31 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { - "number-is-nan": "1.0.1" + "number-is-nan": "^1.0.0" } }, "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", "dev": true, "requires": { - "has": "1.0.3" + "has": "^1.0.1" } }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", "dev": true, "requires": { - "has-symbols": "1.0.1" + "has-symbols": "^1.0.0" } }, "is-typedarray": { @@ -1086,20 +1136,20 @@ "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", "requires": { - "abbrev": "1.0.9", - "async": "1.5.2", - "escodegen": "1.8.1", - "esprima": "2.7.3", - "glob": "5.0.15", - "handlebars": "4.7.3", - "js-yaml": "3.13.1", - "mkdirp": "0.5.1", - "nopt": "3.0.6", - "once": "1.4.0", - "resolve": "1.1.7", - "supports-color": "3.2.3", - "which": "1.3.1", - "wordwrap": "1.0.0" + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" }, "dependencies": { "abbrev": { @@ -1112,24 +1162,11 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", "requires": { - "inflight": "1.0.6", - "inherits": "2.0.4", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } } } @@ -1139,8 +1176,8 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "requires": { - "argparse": "1.0.10", - "esprima": "4.0.1" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, "dependencies": { "esprima": { @@ -1156,18 +1193,18 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jshint": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.11.0.tgz", - "integrity": "sha512-ooaD/hrBPhu35xXW4gn+o3SOuzht73gdBuffgJzrZBJZPGgGiiTvJEgTyxFvBO2nz0+X1G6etF8SzUODTlLY6Q==", - "requires": { - "cli": "1.0.1", - "console-browserify": "1.1.0", - "exit": "0.1.2", - "htmlparser2": "3.8.3", - "lodash": "4.17.15", - "minimatch": "3.0.4", - "shelljs": "0.3.0", - "strip-json-comments": "1.0.4" + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.10.2.tgz", + "integrity": "sha512-e7KZgCSXMJxznE/4WULzybCMNXNAd/bf5TSrvVEq78Q/K8ZwFpmBqQeDtNiHc3l49nV4E/+YeHU/JZjSUIrLAA==", + "requires": { + "cli": "~1.0.0", + "console-browserify": "1.1.x", + "exit": "0.1.x", + "htmlparser2": "3.8.x", + "lodash": "~4.17.11", + "minimatch": "~3.0.2", + "shelljs": "0.3.x", + "strip-json-comments": "1.0.x" }, "dependencies": { "shelljs": { @@ -1198,11 +1235,11 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json5": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", - "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", "requires": { - "minimist": "1.2.0" + "minimist": "^1.2.0" } }, "jsonfile": { @@ -1210,7 +1247,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", "requires": { - "graceful-fs": "4.2.3" + "graceful-fs": "^4.1.6" } }, "jsprim": { @@ -1225,18 +1262,27 @@ } }, "just-extend": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz", - "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", "dev": true }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" } }, "locate-path": { @@ -1245,8 +1291,8 @@ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "requires": { - "p-locate": "3.0.0", - "path-exists": "3.0.0" + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" } }, "lodash": { @@ -1260,13 +1306,13 @@ "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", "dev": true, "requires": { - "chalk": "2.4.2" + "chalk": "^2.0.1" } }, "lolex": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", - "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.1.0.tgz", + "integrity": "sha512-BYxIEXiVq5lGIXeVHnsFzqa1TxN5acnKnPCdlZSpzm8viNEOhiigupA4vTQ9HEFQ6nLTQ9wQOgBknJgzUYQ9Aw==", "dev": true }, "lru-cache": { @@ -1274,29 +1320,55 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" } }, "mime-db": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", - "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" }, "mime-types": { - "version": "2.1.26", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", - "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", "requires": { - "mime-db": "1.43.0" + "mime-db": "1.40.0" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { - "brace-expansion": "1.1.11" + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -1305,38 +1377,48 @@ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "requires": { - "safe-buffer": "5.1.2", - "yallist": "3.1.1" + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" }, "dependencies": { "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" } } }, "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "requires": { - "minipass": "2.9.0" + "minipass": "^2.2.1" } }, "mkdirp": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", - "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==" + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } }, "mocha": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.2.tgz", - "integrity": "sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", + "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", "dev": true, "requires": { "ansi-colors": "3.2.3", @@ -1359,9 +1441,9 @@ "supports-color": "6.0.0", "which": "1.3.1", "wide-align": "1.1.3", - "yargs": "13.3.0", - "yargs-parser": "13.1.1", - "yargs-unparser": "1.6.0" + "yargs": "13.2.2", + "yargs-parser": "13.0.0", + "yargs-unparser": "1.5.0" }, "dependencies": { "glob": { @@ -1370,12 +1452,12 @@ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.4", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "has-flag": { @@ -1384,34 +1466,13 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, "supports-color": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", "dev": true, "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } } } @@ -1421,22 +1482,22 @@ "resolved": "https://registry.npmjs.org/mocha-jshint/-/mocha-jshint-2.3.1.tgz", "integrity": "sha1-MD8n5TOThVnSDyakEj1by1ZFpUY=", "requires": { - "jshint": "2.11.0", - "minimatch": "3.0.4", - "shelljs": "0.4.0", - "uniq": "1.0.1" + "jshint": "^2.8.0", + "minimatch": "^3.0.0", + "shelljs": "^0.4.0", + "uniq": "^1.0.1" } }, "mocha-parallel-tests": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/mocha-parallel-tests/-/mocha-parallel-tests-2.3.0.tgz", - "integrity": "sha512-GdU55VkOCXK2aBmjFzBE11RsSolA71WJppucU3BrTbMUpGIWnTidrje5Hvdr+hOolynEiv6fHWyoNT41mq4HkA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mocha-parallel-tests/-/mocha-parallel-tests-2.1.2.tgz", + "integrity": "sha512-FatFg3MHLio9ir1oP6J0HNEo6R5344JC1y+We90iALdiT9F9xNPN0KbGXxRNlGlSl0GodfSESKbRzBvT9ctgIw==", "dev": true, "requires": { - "circular-json": "0.5.9", - "debug": "4.1.1", - "uuid": "3.4.0", - "yargs": "13.3.0" + "circular-json": "^0.5.9", + "debug": "^4.1.1", + "uuid": "^3.3.2", + "yargs": "^13.2.2" }, "dependencies": { "debug": { @@ -1445,24 +1506,24 @@ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.1" } } } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, "mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "requires": { - "any-promise": "1.3.0", - "object-assign": "4.1.1", - "thenify-all": "1.6.0" + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, "nan": { @@ -1471,13 +1532,13 @@ "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, "needle": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.2.tgz", - "integrity": "sha512-DUzITvPVDUy6vczKKYTnWc/pBZ0EnjMJnQ3y+Jo5zfKFimJs7S3HFCxCRZYB9FUZcrzUQr3WsmvZgddMEIZv6w==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", "requires": { - "debug": "3.2.6", - "iconv-lite": "0.4.24", - "sax": "1.2.4" + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" } }, "neo-async": { @@ -1485,28 +1546,23 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, "nise": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", - "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.0.tgz", + "integrity": "sha512-Z3sfYEkLFzFmL8KY6xnSJLRxwQwYBjOXi/24lb62ZnZiGA0JUzGGTI6TBIgfCSMIDl9Jlu8SRmHNACLTemDHww==", "dev": true, "requires": { - "@sinonjs/formatio": "3.2.2", - "@sinonjs/text-encoding": "0.7.1", - "just-extend": "4.1.0", - "lolex": "5.1.2", - "path-to-regexp": "1.8.0" - }, - "dependencies": { - "lolex": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", - "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", - "dev": true, - "requires": { - "@sinonjs/commons": "1.7.1" - } - } + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^4.1.0", + "path-to-regexp": "^1.7.0" } }, "node-environment-flags": { @@ -1515,14 +1571,14 @@ "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", "dev": true, "requires": { - "object.getownpropertydescriptors": "2.1.0", - "semver": "5.7.1" + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true } } @@ -1532,39 +1588,26 @@ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", "requires": { - "fstream": "1.0.12", - "glob": "7.1.6", - "graceful-fs": "4.2.3", - "mkdirp": "0.5.1", - "nopt": "3.0.6", - "npmlog": "4.1.2", - "osenv": "0.1.5", - "request": "2.88.2", - "rimraf": "2.7.1", - "semver": "5.3.0", - "tar": "2.2.2", - "which": "1.3.1" + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" }, "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { - "glob": "7.1.6" + "glob": "^7.1.3" } } } @@ -1574,38 +1617,25 @@ "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.3.2", - "nopt": "4.0.1", - "npm-packlist": "1.4.8", - "npmlog": "4.1.2", - "rc": "1.2.8", - "rimraf": "2.7.1", - "semver": "5.3.0", - "tar": "4.4.13" + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" }, "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, "nopt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" + "abbrev": "1", + "osenv": "^0.1.4" } }, "rimraf": { @@ -1613,27 +1643,27 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { - "glob": "7.1.6" + "glob": "^7.1.3" } }, "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.9.tgz", + "integrity": "sha512-xisFa7Q2i3HOgfn+nmnWLGHD6Tm23hxjkx6wwGmgxkJFr6wxwXnJOdJYcZjL453PSdF0+bemO03+flAzkIdLBQ==", "requires": { - "chownr": "1.1.4", - "fs-minipass": "1.2.7", - "minipass": "2.9.0", - "minizlib": "1.3.3", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.2", - "yallist": "3.1.1" + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.5", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" } }, "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" } } }, @@ -1647,16 +1677,16 @@ "resolved": "https://registry.npmjs.org/nodegit/-/nodegit-0.23.1.tgz", "integrity": "sha512-k1bCg260yZlmfLGYPrinqAQNbQ6PnBJhKq6wWfe94yIO3WnOOyoSuweAkAGPR6FhrGq2LkCKHdJaitwhKCEGyA==", "requires": { - "fs-extra": "7.0.1", - "json5": "2.1.1", - "lodash": "4.17.15", - "nan": "2.14.0", - "node-gyp": "3.8.0", - "node-pre-gyp": "0.11.0", - "promisify-node": "0.3.0", - "ramda": "0.25.0", - "request-promise-native": "1.0.8", - "tar-fs": "1.16.3" + "fs-extra": "^7.0.0", + "json5": "^2.1.0", + "lodash": "^4.17.11", + "nan": "^2.11.1", + "node-gyp": "^3.8.0", + "node-pre-gyp": "^0.11.0", + "promisify-node": "~0.3.0", + "ramda": "^0.25.0", + "request-promise-native": "^1.0.5", + "tar-fs": "^1.16.3" }, "dependencies": { "fs-extra": { @@ -1664,9 +1694,9 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "requires": { - "graceful-fs": "4.2.3", - "jsonfile": "4.0.0", - "universalify": "0.1.2" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" } }, "jsonfile": { @@ -1674,7 +1704,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "requires": { - "graceful-fs": "4.2.3" + "graceful-fs": "^4.1.6" } } } @@ -1684,7 +1714,7 @@ "resolved": "https://registry.npmjs.org/nodegit-promise/-/nodegit-promise-4.0.0.tgz", "integrity": "sha1-VyKxhPLfcycWEGSnkdLoQskWezQ=", "requires": { - "asap": "2.0.6" + "asap": "~2.0.3" } }, "nopt": { @@ -1692,30 +1722,30 @@ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "requires": { - "abbrev": "1.1.1" + "abbrev": "1" } }, "npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" + }, + "npm-packlist": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", + "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "requires": { - "npm-normalize-package-bin": "1.0.1" + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" } }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, "requires": { - "ignore-walk": "3.0.3", - "npm-bundled": "1.1.1", - "npm-normalize-package-bin": "1.0.1" + "path-key": "^2.0.0" } }, "npmlog": { @@ -1723,10 +1753,10 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "requires": { - "are-we-there-yet": "1.1.5", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" } }, "number-is-nan": { @@ -1744,12 +1774,6 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, - "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "dev": true - }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -1762,20 +1786,20 @@ "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", "dev": true, "requires": { - "define-properties": "1.1.3", - "function-bind": "1.1.1", - "has-symbols": "1.0.1", - "object-keys": "1.1.1" + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" } }, "object.getownpropertydescriptors": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", - "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", "dev": true, "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.17.4" + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" } }, "once": { @@ -1783,7 +1807,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "optimist": { @@ -1791,8 +1815,8 @@ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "requires": { - "minimist": "0.0.10", - "wordwrap": "0.0.3" + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" }, "dependencies": { "minimist": { @@ -1808,16 +1832,16 @@ } }, "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "2.0.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "word-wrap": "1.2.3" + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" } }, "os-homedir": { @@ -1825,6 +1849,17 @@ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -1835,17 +1870,35 @@ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" } }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, "p-limit": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", "dev": true, "requires": { - "p-try": "2.2.0" + "p-try": "^2.0.0" } }, "p-locate": { @@ -1854,7 +1907,7 @@ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "requires": { - "p-limit": "2.2.2" + "p-limit": "^2.0.0" } }, "p-try": { @@ -1874,10 +1927,16 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", "dev": true, "requires": { "isarray": "0.0.1" @@ -1907,9 +1966,9 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "promise-polyfill": { "version": "6.1.0", @@ -1921,7 +1980,7 @@ "resolved": "https://registry.npmjs.org/promisify-node/-/promisify-node-0.3.0.tgz", "integrity": "sha1-tLVaz5D6p9K4uQyjlomQhsAwYM8=", "requires": { - "nodegit-promise": "4.0.0" + "nodegit-promise": "~4.0.0" } }, "pseudomap": { @@ -1930,17 +1989,17 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", - "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" + "version": "1.1.32", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", + "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" }, "pump": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", "requires": { - "end-of-stream": "1.4.4", - "once": "1.4.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, "punycode": { @@ -1963,69 +2022,69 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "requires": { - "deep-extend": "0.6.0", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" } }, "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.4", - "isarray": "1.0.0", - "process-nextick-args": "2.0.1", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.9.1", - "caseless": "0.12.0", - "combined-stream": "1.0.8", - "extend": "3.0.2", - "forever-agent": "0.6.1", - "form-data": "2.3.3", - "har-validator": "5.1.3", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.26", - "oauth-sign": "0.9.0", - "performance-now": "2.1.0", - "qs": "6.5.2", - "safe-buffer": "5.1.2", - "tough-cookie": "2.5.0", - "tunnel-agent": "0.6.0", - "uuid": "3.4.0" + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" } }, "request-promise-core": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", - "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", + "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", "requires": { - "lodash": "4.17.15" + "lodash": "^4.17.11" } }, "request-promise-native": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", - "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", + "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", "requires": { - "request-promise-core": "1.1.3", - "stealthy-require": "1.1.1", - "tough-cookie": "2.5.0" + "request-promise-core": "1.1.2", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" } }, "require-directory": { @@ -2046,11 +2105,11 @@ "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" }, "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", "requires": { - "glob": "7.1.6" + "glob": "^7.1.3" } }, "safe-buffer": { @@ -2078,6 +2137,21 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, "shelljs": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.4.0.tgz", @@ -2089,18 +2163,18 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "sinon": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", - "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz", + "integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==", "dev": true, "requires": { - "@sinonjs/commons": "1.7.1", - "@sinonjs/formatio": "3.2.2", - "@sinonjs/samsam": "3.3.3", - "diff": "3.5.0", - "lolex": "4.2.0", - "nise": "1.5.3", - "supports-color": "5.5.0" + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.1", + "diff": "^3.5.0", + "lolex": "^4.0.1", + "nise": "^1.4.10", + "supports-color": "^5.5.0" }, "dependencies": { "has-flag": { @@ -2115,7 +2189,7 @@ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } } } @@ -2126,7 +2200,7 @@ "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", "optional": true, "requires": { - "amdefine": "1.0.1" + "amdefine": ">=0.0.4" } }, "split": { @@ -2134,7 +2208,7 @@ "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", "requires": { - "through": "2.3.8" + "through": "2" } }, "sprintf-js": { @@ -2147,15 +2221,15 @@ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "requires": { - "asn1": "0.2.4", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.2", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.2", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "safer-buffer": "2.1.2", - "tweetnacl": "0.14.5" + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" } }, "stealthy-require": { @@ -2168,29 +2242,9 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "1.1.3", - "function-bind": "1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "1.1.3", - "function-bind": "1.1.1" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" } }, "string_decoder": { @@ -2198,7 +2252,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "~5.1.0" } }, "strip-ansi": { @@ -2206,9 +2260,15 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -2219,7 +2279,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", "requires": { - "has-flag": "1.0.0" + "has-flag": "^1.0.0" } }, "tar": { @@ -2227,9 +2287,9 @@ "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.12", - "inherits": "2.0.4" + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" } }, "tar-fs": { @@ -2237,25 +2297,10 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", "requires": { - "chownr": "1.1.4", - "mkdirp": "0.5.1", - "pump": "1.0.3", - "tar-stream": "1.6.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - } + "chownr": "^1.0.1", + "mkdirp": "^0.5.1", + "pump": "^1.0.0", + "tar-stream": "^1.1.2" } }, "tar-stream": { @@ -2263,21 +2308,21 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", "requires": { - "bl": "1.2.2", - "buffer-alloc": "1.2.0", - "end-of-stream": "1.4.4", - "fs-constants": "1.0.0", - "readable-stream": "2.3.7", - "to-buffer": "1.1.1", - "xtend": "4.0.2" + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" } }, "temp": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.1.tgz", - "integrity": "sha512-WMuOgiua1xb5R56lE0eH6ivpVmg/lq2OHm4+LtT/xtEtPQ+sz6N3bBM6WZ5FvO1lO4IKIOb43qnhoc4qxP5OeA==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.0.tgz", + "integrity": "sha512-YfUhPQCJoNQE5N+FJQcdPz63O3x3sdT4Xju69Gj4iZe0lBKOtnAMi0SLj9xKhGkcGhsxThvTJ/usxtFPo438zQ==", "requires": { - "rimraf": "2.6.3" + "rimraf": "~2.6.2" }, "dependencies": { "rimraf": { @@ -2285,7 +2330,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "requires": { - "glob": "7.1.6" + "glob": "^7.1.3" } } } @@ -2295,7 +2340,7 @@ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", "requires": { - "any-promise": "1.3.0" + "any-promise": "^1.0.0" } }, "thenify-all": { @@ -2303,7 +2348,7 @@ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", "requires": { - "thenify": "3.3.0" + "thenify": ">= 3.1.0 < 4" } }, "through": { @@ -2321,16 +2366,23 @@ "resolved": "https://registry.npmjs.org/to-function/-/to-function-2.0.6.tgz", "integrity": "sha1-fVbNnDuS+o29eyLoPVGSTedA68U=", "requires": { - "component-props": "1.1.1" + "component-props": "*" } }, "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "requires": { - "psl": "1.7.0", - "punycode": "2.1.1" + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } } }, "tunnel-agent": { @@ -2338,7 +2390,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "^5.0.1" } }, "tweetnacl": { @@ -2351,7 +2403,7 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "requires": { - "prelude-ls": "1.1.2" + "prelude-ls": "~1.1.2" } }, "type-detect": { @@ -2360,13 +2412,13 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" }, "uglify-js": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.0.tgz", - "integrity": "sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ==", + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.5.tgz", + "integrity": "sha512-7L3W+Npia1OCr5Blp4/Vw83tK1mu5gnoIURtT1fUVfQ3Kf8WStWV6NJz0fdoBJZls0KlweruRTLVe6XLafmy5g==", "optional": true, "requires": { - "commander": "2.20.3", - "source-map": "0.6.1" + "commander": "~2.20.3", + "source-map": "~0.6.1" }, "dependencies": { "source-map": { @@ -2392,7 +2444,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "requires": { - "punycode": "2.1.1" + "punycode": "^2.1.0" } }, "util-deprecate": { @@ -2401,18 +2453,18 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { - "assert-plus": "1.0.0", + "assert-plus": "^1.0.0", "core-util-is": "1.0.2", - "extsprintf": "1.3.0" + "extsprintf": "^1.2.0" } }, "walk": { @@ -2420,7 +2472,7 @@ "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.14.tgz", "integrity": "sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg==", "requires": { - "foreachasync": "3.0.0" + "foreachasync": "^3.0.0" } }, "which": { @@ -2428,7 +2480,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "requires": { - "isexe": "2.0.0" + "isexe": "^2.0.0" } }, "which-module": { @@ -2442,62 +2494,22 @@ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "requires": { - "string-width": "1.0.2" + "string-width": "^1.0.2 || 2" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" - }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { - "ansi-styles": "3.2.1", - "string-width": "3.1.0", - "strip-ansi": "5.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "7.0.3", - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "5.2.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "4.1.0" - } - } + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" } }, "wrappy": { @@ -2506,9 +2518,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, "y18n": { "version": "4.0.0", @@ -2522,21 +2534,22 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, "yargs": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", - "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", + "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", "dev": true, "requires": { - "cliui": "5.0.0", - "find-up": "3.0.0", - "get-caller-file": "2.0.5", - "require-directory": "2.1.1", - "require-main-filename": "2.0.0", - "set-blocking": "2.0.0", - "string-width": "3.1.0", - "which-module": "2.0.0", - "y18n": "4.0.0", - "yargs-parser": "13.1.1" + "cliui": "^4.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.0.0" }, "dependencies": { "ansi-regex": { @@ -2557,9 +2570,9 @@ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, "requires": { - "emoji-regex": "7.0.3", - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "5.2.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" } }, "strip-ansi": { @@ -2568,30 +2581,105 @@ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "4.1.0" + "ansi-regex": "^4.1.0" } } } }, "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", + "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", "dev": true, "requires": { - "camelcase": "5.3.1", - "decamelize": "1.2.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } }, "yargs-unparser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", - "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", + "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", "dev": true, "requires": { - "flat": "4.1.0", - "lodash": "4.17.15", - "yargs": "13.3.0" + "flat": "^4.1.0", + "lodash": "^4.17.11", + "yargs": "^12.0.5" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } } } } diff --git a/node/package.json b/node/package.json index 674b15767..4026b1b86 100644 --- a/node/package.json +++ b/node/package.json @@ -44,7 +44,7 @@ "devDependencies": { "deepcopy": "", "istanbul": "", - "mkdirp": "", + "mkdirp": "0.5.1", "mocha": "^6.1.4", "mocha-jshint": "", "mocha-parallel-tests": "^2.1.2", From 939724ea4adb27a974b3887251abb5b21930059b Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 1 Apr 2020 09:43:41 -0400 Subject: [PATCH 278/402] If our sha is a descendant of their sha, we need to make sure that our sha gets added to the index. This is handled elsewhere in non-bare merges, but in bare merges, we need to manually handle it. --- node/lib/util/merge_util.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index a8919b403..6af379d8b 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -255,6 +255,7 @@ exports.mergeSubmodule = co.wrap(function *(metaIndex, // See if up-to-date if (yield NodeGit.Graph.descendantOf(subRepo, ourSha, theirSha)) { + yield CherryPickUtil.addSubmoduleCommit(metaIndex, subName, ourSha); return result; // RETURN } From 257b5538b318f5424b1838f24274dca621929fab Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 31 Mar 2020 20:46:52 -0400 Subject: [PATCH 279/402] merge-bare creates a commit but doesn't update HEAD, so it doesn't need to run the hooks --- node/lib/cmd/merge_bare.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/node/lib/cmd/merge_bare.js b/node/lib/cmd/merge_bare.js index f11930ee0..17f040afc 100755 --- a/node/lib/cmd/merge_bare.js +++ b/node/lib/cmd/merge_bare.js @@ -131,9 +131,4 @@ Could not resolve ${colors.red(theirCommitName)} to a commit.`); if (null !== result.metaCommit) { console.log(result.metaCommit); } - // Run post-merge hook if merge successfully. - // Fixme: --squash is not supported yet, once supported, need to parse 0/1 - // as arg into the post-merge hook, 1 means it is a squash merge, 0 means - // not. - yield Hook.execHook(repo, "post-merge", ["0"]); }); From 99284115b52bcc8dcf59e05d827ae2f622db8619 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 31 Mar 2020 16:32:13 -0400 Subject: [PATCH 280/402] Support --do-not-recurse for merge and merge-bare --- node/lib/cmd/merge.js | 19 +++++++++++++++++++ node/lib/cmd/merge_bare.js | 20 ++++++++++++++++++++ node/lib/util/merge_common.js | 14 +++++++++++++- node/lib/util/merge_util.js | 26 +++++++++++++++++++++++--- node/test/util/merge_bare.js | 1 + node/test/util/merge_full_open.js | 7 ++----- node/test/util/merge_util.js | 28 +++++++++++++++++++++++----- 7 files changed, 101 insertions(+), 14 deletions(-) diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index 4be42ebb1..056dc5e5f 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -75,6 +75,13 @@ exports.configureParser = function (parser) { action: "storeConst", constant: true, }); + parser.addArgument(["--do-not-recurse"], { + action: "append", + type: "string", + help: "treat all possible conflicts in a dir and its subdirectories" + + " as actual, without attempting to recursively merge inside the" + + " submodules", + }); parser.addArgument(["commit"], { type: "string", help: "the commitish to merge", @@ -168,12 +175,24 @@ Merge of '${commitName}' `; return GitUtil.editMessage(repo, message); }; + const doNotRecurse = []; + for (const prefix of args.do_not_recurse || []) { + let noSlashPrefix; + if (prefix.endsWith("/")) { + noSlashPrefix = prefix.substring(0, prefix.length - 1); + } else { + noSlashPrefix = prefix; + } + doNotRecurse.push(noSlashPrefix); + } + const commit = yield repo.getCommit(commitish.id()); const result = yield MergeUtil.merge(repo, null, commit, mode, Open.SUB_OPEN_OPTION.FORCE_OPEN, + doNotRecurse, args.message, editMessage); if (null !== result.errorMessage) { diff --git a/node/lib/cmd/merge_bare.js b/node/lib/cmd/merge_bare.js index 17f040afc..55341f3f6 100755 --- a/node/lib/cmd/merge_bare.js +++ b/node/lib/cmd/merge_bare.js @@ -73,6 +73,14 @@ exports.configureParser = function (parser) { action: "storeConst", constant: true, }); + + parser.addArgument(["--do-not-recurse"], { + action: "append", + type: "string", + help: "treat all possible conflicts in a dir and its subdirectories" + + " as actual, without attempting to recursively merge inside the" + + " submodules", + }); }; /** @@ -115,6 +123,17 @@ Could not resolve ${colors.red(ourCommitName)} to a commit.`); Could not resolve ${colors.red(theirCommitName)} to a commit.`); } + const doNotRecurse = []; + for (const prefix of args.do_not_recurse || []) { + let noSlashPrefix; + if (prefix.endsWith("/")) { + noSlashPrefix = prefix.substring(0, prefix.length - 1); + } else { + noSlashPrefix = prefix; + } + doNotRecurse.push(noSlashPrefix); + } + const ourCommit = yield repo.getCommit(ourCommitish.id()); const theirCommit = yield repo.getCommit(theirCommitish.id()); const noopEditor = function() {}; @@ -123,6 +142,7 @@ Could not resolve ${colors.red(theirCommitName)} to a commit.`); theirCommit, mode, Open.SUB_OPEN_OPTION.FORCE_BARE, + doNotRecurse, args.message, noopEditor); if (null !== result.errorMessage) { diff --git a/node/lib/util/merge_common.js b/node/lib/util/merge_common.js index 0010bf83d..26c005b38 100755 --- a/node/lib/util/merge_common.js +++ b/node/lib/util/merge_common.js @@ -62,6 +62,7 @@ class MergeContext { * @param {NodeGit.Commit} theirCommit * @param {MergeCommon.MODE} mode * @param {Open.SUB_OPEN_OPTION} openOption + * @param {[String]} doNotRecurse * @param {String|null} commitMessage * @param {() -> Promise(String)} editMessage */ @@ -70,6 +71,7 @@ class MergeContext { theirCommit, mode, openOption, + doNotRecurse, commitMessage, editMessage, authorName, @@ -92,6 +94,7 @@ class MergeContext { this.d_theirCommit = theirCommit; this.d_mode = mode; this.d_openOption = openOption; + this.d_doNotRecurse = doNotRecurse; this.d_commitMessage = commitMessage; this.d_editMessage = editMessage; this.d_opener = new Open.Opener(metaRepo, ourCommit); @@ -139,6 +142,14 @@ class MergeContext { return this.d_openOption; } + + /** + * @property {[String]} + */ + get doNotRecurse() { + return this.d_doNotRecurse; + } + /** * @property {MODE} */ @@ -169,7 +180,8 @@ MergeContext.prototype.getChanges = co.wrap(function *() { this.d_metaRepo, yield this.getChangeIndex(), yield this.getOurCommit(), - this.d_theirCommit); + this.d_theirCommit, + this.d_doNotRecurse); } return this.d_changes; }); diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 6af379d8b..f988ea3a1 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -39,6 +39,7 @@ const NodeGit = require("nodegit"); const Checkout = require("./checkout"); const CherryPickUtil = require("./cherry_pick_util"); const ConfigUtil = require("./config_util"); +const ConflictUtil = require("./conflict_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); const MergeCommon = require("./merge_common"); @@ -56,6 +57,9 @@ const SubmoduleConfigUtil = require("./submodule_config_util"); const UserError = require("./user_error"); const CommitAndRef = SequencerState.CommitAndRef; +const Conflict = ConflictUtil.Conflict; +const ConflictEntry = ConflictUtil.ConflictEntry; +const FILEMODE = NodeGit.TreeEntry.FILEMODE; const MERGE = SequencerState.TYPE.MERGE; const MergeContext = MergeCommon.MergeContext; const MergeStepResult = MergeCommon.MergeStepResult; @@ -189,7 +193,7 @@ exports.makeMetaCommit = co.wrap(function *(repo, }); /** - * Merge the specified `subName` and update the in memeory `metaindex`. + * Merge the specified `subName` and update the in memory `metaindex`. * * @async * @param {NodeGit.Index} metaIndex index of the meta repo @@ -477,9 +481,10 @@ const mergeStepMergeSubmodules = co.wrap(function *(context) { const committer = yield context.getCommitter(); const theirCommit = context.theirCommit; const theirCommitSha = theirCommit.id().tostrS(); + const doNotRecurse = context.doNotRecurse; let conflictMessage = ""; - // abort merge if conflicted under FROCE_BARE mode + // abort merge if conflicted under FORCE_BARE mode if (forceBare && Object.keys(changes.conflicts).length > 0) { conflictMessage = getBareMergeConflictsMessage(changes.conflicts); return MergeStepResult.error(conflictMessage); // RETURN @@ -509,6 +514,19 @@ const mergeStepMergeSubmodules = co.wrap(function *(context) { commits: {}, }; const mergeSubmoduleRunner = co.wrap(function *(subName) { + for (const prefix of doNotRecurse) { + if (subName.startsWith(prefix + "/") || subName === prefix) { + const change = changes.changes[subName]; + const sha = change.newSha; + merges.conflicts[subName] = sha; + merges.conflictPaths[subName] = [""]; + const old = new ConflictEntry(FILEMODE.COMMIT, change.oldSha); + const our = new ConflictEntry(FILEMODE.COMMIT, change.ourSha); + const new_ = new ConflictEntry(FILEMODE.COMMIT, change.newSha); + changes.conflicts[subName] = new Conflict(old, our, new_); + return; + } + } const subResult = yield exports.mergeSubmodule(index, subName, @@ -625,6 +643,7 @@ exports.merge = co.wrap(function *(repo, theirCommit, mode, openOption, + doNotRecurse, commitMessage, editMessage) { // pack and validate merging objects @@ -633,6 +652,7 @@ exports.merge = co.wrap(function *(repo, theirCommit, mode, openOption, + doNotRecurse, commitMessage, editMessage, process.env.GIT_AUTHOR_NAME, @@ -681,7 +701,7 @@ const checkForConflicts = function (index) { const entry = entries[i]; const stage = NodeGit.Index.entryStage(entry); if (RepoStatus.STAGE.OURS === stage && - NodeGit.TreeEntry.FILEMODE.COMMIT !== entry.mode) { + FILEMODE.COMMIT !== entry.mode) { throw new UserError("Meta-repo has conflicts."); } } diff --git a/node/test/util/merge_bare.js b/node/test/util/merge_bare.js index a1299bbc4..b46df8f7a 100644 --- a/node/test/util/merge_bare.js +++ b/node/test/util/merge_bare.js @@ -344,6 +344,7 @@ x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; theirCommit, mode, openOption, + [], message, defaultEditor); const errorMessage = c.errorMessage || null; diff --git a/node/test/util/merge_full_open.js b/node/test/util/merge_full_open.js index 88ff85d91..e0f955b3a 100644 --- a/node/test/util/merge_full_open.js +++ b/node/test/util/merge_full_open.js @@ -407,12 +407,9 @@ x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; commit, mode, openOption, + [], message, - editMessage, - null, - null, - null, - null); + editMessage); const errorMessage = c.errorMessage || null; assert.equal(result.errorMessage, errorMessage); if (upToDate) { diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index af6f356cf..296d81207 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -339,6 +339,26 @@ x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, fromCommit: "4", expected: "x=E:Cx-3,4 s=Sa:s;Os Cs-b,c c=c!H=s;Bmaster=x", }, + "non-ffmerge with non-ffwd submodule change, unrelated dnr": { + initial: ` +a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + doNotRecurse: ["fake"], + expected: "x=E:Cx-3,4 s=Sa:s;Os Cs-b,c c=c!H=s;Bmaster=x", + }, + "non-ffmerge with non-ffwd submodule change, sub is dnr": { + initial: ` +a=Aa:Cb-a;Cc-a;Bfoo=b;Bbar=c| +x=U:C3-2 s=Sa:b;C4-2 s=Sa:c;Bmaster=3;Bfoo=4;Os`, + fromCommit: "4", + doNotRecurse: ["s"], + fails: true, + errorMessage: `\ +Submodule ${colors.red("s")} is conflicted. +`, + expected: `x=E:Qmessage\n#M 3: 4: 0 4;I *s=S:1*S:b*S:c` + }, "submodule commit is up-to-date": { initial:` a=Aa:Cb-a;Cc-b;Bfoo=b;Bbar=c| @@ -470,17 +490,15 @@ x=S:C2-1 r=Sa:1,s=Sa:1,t=Sa:1; const editMessage = c.editMessage || defaultEditor; const openOption = Open.SUB_OPEN_OPTION.ALLOW_BARE; + const doNotRecurse = c.doNotRecurse || []; const result = yield MergeUtil.merge(x, null, commit, mode, openOption, + doNotRecurse, message, - editMessage, - null, - null, - null, - null); + editMessage); const errorMessage = c.errorMessage || null; assert.equal(result.errorMessage, errorMessage); From a646afe9d18588559bf7b88c8e7617a7514b4c2c Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 1 Apr 2020 11:34:57 -0400 Subject: [PATCH 281/402] add option to merge{,-bare} to read commit message from a file --- node/lib/cmd/merge.js | 17 ++++++++++++++++- node/lib/cmd/merge_bare.js | 20 +++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index 056dc5e5f..12bd216b9 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -31,6 +31,7 @@ "use strict"; const co = require("co"); +const fs = require("fs-promise"); /** * This module contains methods for implementing the `merge` command. @@ -60,6 +61,11 @@ exports.configureParser = function (parser) { help: "commit message", required: false, }); + parser.addArgument(["-F", "--message-file"], { + type: "string", + help: "commit message file name", + required: false, + }); parser.addArgument(["--ff"], { help: "allow fast-forward merges; this is the default", action: "storeConst", @@ -124,6 +130,10 @@ exports.executeableSubcommand = co.wrap(function *(args) { const MODE = MergeCommon.MODE; let mode = MODE.NORMAL; + if (args.message && args.message_file) { + throw new UserError("Cannot use -m and -F together."); + } + if (args.ff + args.continue + args.abort + args.no_ff + args.ff_only > 1) { throw new UserError( "Cannot use ff, no-ff, ff-only, abort, or continue together."); @@ -186,6 +196,11 @@ Merge of '${commitName}' doNotRecurse.push(noSlashPrefix); } + let message = args.message; + if (args.message_file) { + message = yield fs.readFile(args.message_file, "utf8"); + } + const commit = yield repo.getCommit(commitish.id()); const result = yield MergeUtil.merge(repo, null, @@ -193,7 +208,7 @@ Merge of '${commitName}' mode, Open.SUB_OPEN_OPTION.FORCE_OPEN, doNotRecurse, - args.message, + message, editMessage); if (null !== result.errorMessage) { throw new UserError(result.errorMessage); diff --git a/node/lib/cmd/merge_bare.js b/node/lib/cmd/merge_bare.js index 55341f3f6..c619f3727 100755 --- a/node/lib/cmd/merge_bare.js +++ b/node/lib/cmd/merge_bare.js @@ -31,6 +31,7 @@ "use strict"; const co = require("co"); +const fs = require("fs-promise"); /** * This module contains methods for implementing the `merge-bare` command. @@ -56,7 +57,12 @@ exports.configureParser = function (parser) { parser.addArgument(["-m", "--message"], { type: "string", help: "commit message", - required: true, + required: false, + }); + parser.addArgument(["-F", "--message-file"], { + type: "string", + help: "commit message file name", + required: false, }); parser.addArgument(["ourCommit"], { @@ -97,10 +103,13 @@ exports.executeableSubcommand = co.wrap(function *(args) { const MergeUtil = require("../util/merge_util"); const MergeCommon = require("../util/merge_common"); const GitUtil = require("../util/git_util"); - const Hook = require("../util/hook"); const Open = require("../util/open"); const UserError = require("../util/user_error"); + if (Boolean(args.message) + Boolean(args.message_file) !== 1) { + throw new UserError("Use exactly one of -m and -F."); + } + const repo = yield GitUtil.getCurrentRepo(); const mode = args.no_ff ? MergeCommon.MODE.FORCE_COMMIT : @@ -134,6 +143,11 @@ Could not resolve ${colors.red(theirCommitName)} to a commit.`); doNotRecurse.push(noSlashPrefix); } + let message = args.message; + if (args.message_file) { + message = yield fs.readFile(args.message_file, "utf8"); + } + const ourCommit = yield repo.getCommit(ourCommitish.id()); const theirCommit = yield repo.getCommit(theirCommitish.id()); const noopEditor = function() {}; @@ -143,7 +157,7 @@ Could not resolve ${colors.red(theirCommitName)} to a commit.`); mode, Open.SUB_OPEN_OPTION.FORCE_BARE, doNotRecurse, - args.message, + message, noopEditor); if (null !== result.errorMessage) { throw new UserError(result.errorMessage); From ac874081f797733aee2bb1157da583a77428eb2b Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Wed, 20 May 2020 14:19:11 -0400 Subject: [PATCH 282/402] Skip url changes check in normal merge. After #752, merge is able to smart merge submdoule urls, but we kept url change warnings to normal merge for backward compatibility. Now it is time to lift che check and allow smart url merge for normal merge as well --- node/lib/util/merge_util.js | 6 +----- node/test/util/merge_full_open.js | 10 ++++++++-- node/test/util/merge_util.js | 10 ++++++++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index f988ea3a1..cdbe0e58e 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -352,8 +352,7 @@ ${colors.green(theirSha)}.`); /** * Perform preparation work before merge, including * 1. locate merge base - * 2. ensureNoURLChanges see also {CherryPickUtil.ensureNoURLChanges} - * 3. check if working dir is clean (non-bare repo) + * 2. check if working dir is clean (non-bare repo) * 3. check if two merging commits are the same or if their commit * is an ancestor of ours, both cases are no-op. * @@ -385,9 +384,6 @@ const mergeStepPrepare = co.wrap(function *(context) { } if (!forceBare) { - yield CherryPickUtil.ensureNoURLChanges(metaRepo, - theirCommit, - baseCommit); const status = yield StatusUtil.getRepoStatus(metaRepo); const statusError = StatusUtil.checkReadiness(status); if (null !== statusError) { diff --git a/node/test/util/merge_full_open.js b/node/test/util/merge_full_open.js index e0f955b3a..eadba1192 100644 --- a/node/test/util/merge_full_open.js +++ b/node/test/util/merge_full_open.js @@ -99,12 +99,18 @@ describe("MergeFullOpen", function () { "url changes": { initial: "a=B|b=B|x=U:C3-2 s=Sb:1;Bfoo=3", fromCommit: "3", - fails: true, + expected: "a=B|x=E:Bmaster=3", }, "ancestor url changes": { initial: "a=B|b=B|x=U:C4-3 q=Sa:1;C3-2 s=Sb:1;Bfoo=4", fromCommit: "4", - fails: true, + expected: "a=B|x=E:Bmaster=4", + }, + "genuine url merge conflicts": { + initial: "a=B|b=B|c=B|" + + "x=U:C3-2 s=Sc:1;C4-2 s=Sb:1;Bmaster=3;Bfoo=4", + fromCommit: "4", + fails: true }, "dirty": { initial: "a=B|x=U:C3-1 t=Sa:1;Bfoo=3;Os W README.md=8", diff --git a/node/test/util/merge_util.js b/node/test/util/merge_util.js index 296d81207..4fa50657d 100644 --- a/node/test/util/merge_util.js +++ b/node/test/util/merge_util.js @@ -166,12 +166,18 @@ x=U:C3-2 s=Sa:a;Bfoo=3;Os W a=b`, "url changes": { initial: "a=B|b=B|x=U:C3-2 s=Sb:1;Bfoo=3", fromCommit: "3", - fails: true, + expected: "a=B|x=E:Bmaster=3" }, "ancestor url changes": { initial: "a=B|b=B|x=U:C4-3 q=Sa:1;C3-2 s=Sb:1;Bfoo=4", fromCommit: "4", - fails: true, + expected: "a=B|x=E:Bmaster=4" + }, + "genuine url merge conflicts": { + initial: "a=B|b=B|c=B|" + + "x=U:C3-2 s=Sc:1;C4-2 s=Sb:1;Bmaster=3;Bfoo=4", + fromCommit: "4", + fails: true }, "dirty": { initial: "a=B|x=U:C3-1 t=Sa:1;Bfoo=3;Os W README.md=8", From 73b9a886b3ca90f5fd18f1042496ea1260e1c173 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 26 May 2020 13:17:40 -0400 Subject: [PATCH 283/402] Pull: do not check remote name We don't care if the remote they give is a *named* remote -- we just care if we can fetch from it. Fetching calls out to regular git, which knows how to handle not only remote names but also remote URLs. And fetching is about the first thing we do, so we'll find out quickly whether the remote is valid. --- node/lib/util/pull.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/node/lib/util/pull.js b/node/lib/util/pull.js index 90f165f92..307988466 100644 --- a/node/lib/util/pull.js +++ b/node/lib/util/pull.js @@ -55,15 +55,6 @@ exports.pull = co.wrap(function *(metaRepo, remoteName, source) { assert.isString(remoteName); assert.isString(source); - // First do some sanity checking on the repos to see if they have a remote - // with `remoteName` and are clean. - - const validRemote = yield GitUtil.isValidRemoteName(metaRepo, remoteName); - - if (!validRemote) { - throw new UserError(`Invalid remote name ${colors.red(remoteName)}.`); - } - const status = yield StatusUtil.getRepoStatus(metaRepo); // Just fetch the meta-repo; rebase will trigger necessary fetches in From 9295a1576f563cbbc7d5bd9ec305131bac67848c Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 26 May 2020 16:46:56 -0400 Subject: [PATCH 284/402] Pull: use FETCH_HEAD after fetch since raw URL fetches do not update remote tracking branches --- node/lib/util/pull.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/node/lib/util/pull.js b/node/lib/util/pull.js index 307988466..28c11f39d 100644 --- a/node/lib/util/pull.js +++ b/node/lib/util/pull.js @@ -32,7 +32,6 @@ const assert = require("chai").assert; const co = require("co"); -const colors = require("colors"); const NodeGit = require("nodegit"); const ConfigUtil = require("./config_util"); @@ -61,16 +60,9 @@ exports.pull = co.wrap(function *(metaRepo, remoteName, source) { // sub-repos. yield GitUtil.fetchBranch(metaRepo, remoteName, source); - const remoteBranch = yield GitUtil.findRemoteBranch(metaRepo, - remoteName, - source); - if (null === remoteBranch) { - throw new UserError(`The meta-repo does not have a branch named \ -${colors.red(source)} in the remote ${colors.yellow(remoteName)}.`); - } - const remoteCommitId = remoteBranch.target(); - const remoteCommit = yield NodeGit.Commit.lookup(metaRepo, remoteCommitId); + const ref = yield NodeGit.Reference.lookup(metaRepo, "FETCH_HEAD"); + const remoteCommit = yield NodeGit.Commit.lookup(metaRepo, ref.target()); const result = yield RebaseUtil.rebase(metaRepo, remoteCommit, status); if (null !== result.errorMessage) { From 0a51aa50773171738a2585a0374361a3d2b53985 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 3 Jun 2020 15:23:22 -0400 Subject: [PATCH 285/402] only call post-open if any subs opened --- node/lib/cmd/open.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 93ba390d1..d4a2eba1d 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -157,8 +157,10 @@ Opening ${colors.blue(name)} on ${colors.green(shas[idx])}.`); yield SparseCheckoutUtil.writeMetaIndex(repo, index); - // Run post-open-submodule hook with submodules which opened successfully. - yield Hook.execHook(repo, "post-open-submodule", subsOpenSuccessfully); + if (subsOpenSuccessfully.length) { + // Run post-open-submodule hook with successfully-opened submodules + yield Hook.execHook(repo, "post-open-submodule", subsOpenSuccessfully); + } if (failed) { process.exit(1); From 8c9d28299730049aa5aa2671e6cc8e2a479bf1ad Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 3 Jun 2020 15:10:42 -0400 Subject: [PATCH 286/402] submodule commit hook failures should be silent --- node/lib/util/commit.js | 7 ++----- node/test/util/commit.js | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 5b1c37def..a7cbe9d33 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -2323,11 +2323,8 @@ exports.execSubmodulePrecommitHooks = co.wrap(function *(repo, ); const isOk = yield Hook.execHook(subRepo, "pre-commit"); if (!isOk) { - throw new Error( - "Error occurred when running pre-commit hook of " + - "submodule: " + - submoduleName - ); + // hooks are responsible for printing their own message + throw new Error(""); } } } diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 81c0041c2..be91fc797 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -3342,11 +3342,8 @@ x=U:Cbar\n#x-2 s=Sa:s;Bmaster=x;Os Cbar\n#s-1 README.md=foo, a=b`, return Commit.execSubmodulePrecommitHooks().catch(err => { assert.instanceOf(err, Error); - assert( - err.message, - "Error occurred when running pre-commit hook of " + - "submodule: b" - ); + // hooks are responsible for printing their own message + assert.equal(err.message, ""); }); }); From 8fe2c279f3534595645c8fcb6bf7201543fb0602 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 3 Jun 2020 13:50:28 -0400 Subject: [PATCH 287/402] Improve message for direct submodule conflicts --- node/lib/util/merge_util.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index cdbe0e58e..f9a618300 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -116,7 +116,8 @@ const getBareMergeConflictsMessage = function(conflicts) { errorMessage += `\tconflicted: ${name}/${path}\n`; } } else { - errorMessage += `Merge conflict in submodule '${name}':\n`; + errorMessage += `Merge conflict in submodule '${name}' itself +(e.g. delete/modify or add/modify)\n`; } } errorMessage += "\nAutomatic merge failed\n"; From 0fe08d6211c0edcd40da6c50a34e95e5bf40c9ff Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 15 Jun 2020 13:12:50 -0400 Subject: [PATCH 288/402] Improve language usage --- node/lib/git-meta.js | 4 +-- node/lib/util/stitch_util.js | 25 +++++++-------- node/lib/util/synthetic_branch_util.js | 42 +++++++++++++------------- node/test/util/stitch_util.js | 19 ++++++------ node/test/util/synthetic-branch.js | 8 ++--- 5 files changed, 50 insertions(+), 48 deletions(-) diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js index 7502fc86e..686b628ba 100755 --- a/node/lib/git-meta.js +++ b/node/lib/git-meta.js @@ -167,7 +167,7 @@ Object.keys(commands).sort().forEach(name => { configureSubcommand(subParser, name, cmd, (undefined !== optimized[name])); }); -const blacklist = new Set([ +const do_not_forward = new Set([ "--help", "--version", "-h", @@ -204,7 +204,7 @@ const blacklist = new Set([ // sub-parser level. if (2 < process.argv.length && - !blacklist.has(process.argv[2]) && + !do_not_forward.has(process.argv[2]) && !(process.argv[2] in commands)) { const name = process.argv[2]; const args = process.argv.slice(3); diff --git a/node/lib/util/stitch_util.js b/node/lib/util/stitch_util.js index 5d2ff0635..66350452c 100644 --- a/node/lib/util/stitch_util.js +++ b/node/lib/util/stitch_util.js @@ -49,7 +49,7 @@ const FILEMODE = NodeGit.TreeEntry.FILEMODE; /** * @property {String} */ -exports.whitelistNoteRef = "refs/notes/stitched/whitelist"; +exports.allowedToFailNoteRef = "refs/notes/stitched/allowed_to_fail"; /** * Return a set of meta-repo commits that are allowed to fail. @@ -57,11 +57,12 @@ exports.whitelistNoteRef = "refs/notes/stitched/whitelist"; * @param {NodeGit.Repository} repo * @return {Set} */ -exports.readWhitelist = co.wrap(function *(repo) { +exports.readAllowedToFailList = co.wrap(function *(repo) { assert.instanceOf(repo, NodeGit.Repository); const notes = - yield BulkNotesUtil.readNotes(repo, exports.whitelistNoteRef); + yield BulkNotesUtil.readNotes(repo, + exports.allowedToFailNoteRef); return new Set(Object.keys(notes)); }); @@ -534,8 +535,8 @@ exports.sameInAnyOtherParent = co.wrap(function *(repo, commit, name, sha) { * Then do not generate a commit; instead, return the first parent (and an * empty `subCommits` map), or null if there are no parents. * - * Allow commits in the specified `whitelist` to reference invalid submodule - * commits; skip those (submodule) commits. + * Allow commits in the specified `allowed_to_fail` to reference invalid + * submodule commits; skip those (submodule) commits. * * @param {NodeGit.Repository} repo * @param {NodeGit.Commit} commit @@ -544,7 +545,7 @@ exports.sameInAnyOtherParent = co.wrap(function *(repo, commit, name, sha) { * @param {(String) => Boolean} keepAsSubmodule * @param {(String) => String|null} adjustPath * @param {Bool} skipEmpty - * @param {Set of String} whitelist + * @param {Set of String} allowed_to_fail * @return {Object} * @return {NodeGit.Commit} [return.stitchedCommit] * @return {Object} return.subCommits path to NodeGit.Commit @@ -556,7 +557,7 @@ exports.writeStitchedCommit = co.wrap(function *(repo, keepAsSubmodule, adjustPath, skipEmpty, - whitelist) { + allowed_to_fail) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); assert.isObject(subChanges); @@ -564,7 +565,7 @@ exports.writeStitchedCommit = co.wrap(function *(repo, assert.isFunction(keepAsSubmodule); assert.isFunction(adjustPath); assert.isBoolean(skipEmpty); - assert.instanceOf(whitelist, Set); + assert.instanceOf(allowed_to_fail, Set); let updateModules = false; // if any kept subs added or removed const changes = {}; // changes and additions @@ -576,13 +577,13 @@ exports.writeStitchedCommit = co.wrap(function *(repo, } catch (e) { const metaSha = commit.id().tostrS(); - if (whitelist.has(metaSha)) { + if (allowed_to_fail.has(metaSha)) { return; // RETURN } throw new UserError(`\ On meta-commit ${metaSha}, ${name} is missing ${sha}. To add to allow this submodule change to be skipped, run: -git notes --ref ${exports.whitelistNoteRef} add -m skip ${metaSha}`); +git notes --ref ${exports.allowedToFailNoteRef} add -m skip ${metaSha}`); } const subTreeId = subCommit.treeId(); changes[name] = new TreeUtil.Change(subTreeId, FILEMODE.TREE); @@ -1010,7 +1011,7 @@ ${name}`; records = {}; }); - const whitelist = yield exports.readWhitelist(repo); + const allowed_to_fail = yield exports.readAllowedToFailList(repo); for (let i = 0; i < commitsToStitch.length; ++i) { const next = commitsToStitch[i]; @@ -1034,7 +1035,7 @@ ${name}`; options.keepAsSubmodule, adjustPath, skipEmpty, - whitelist); + allowed_to_fail); records[nextSha] = result; const newCommit = result.stitchedCommit; const newSha = null === newCommit ? null : newCommit.id().tostrS(); diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index c21b2d9a8..4166b3c88 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -54,23 +54,23 @@ function identity(v) { return v; } -function SyntheticBranchConfig(urlWhitelistPattern, pathWhitelistPattern) { - if (urlWhitelistPattern.length > 0) { - const urlWhitelistRE = new RegExp(urlWhitelistPattern); - this.urlWhitelistTest = function(url) { - return urlWhitelistRE.test(url); +function SyntheticBranchConfig(urlSkipPattern, pathSkipPattern) { + if (urlSkipPattern.length > 0) { + const urlSkipRE = new RegExp(urlSkipPattern); + this.urlSkipTest = function(url) { + return urlSkipRE.test(url); }; } else { - this.urlWhitelistTest = function() { return false; }; + this.urlSkipTest = function() { return false; }; } - if (pathWhitelistPattern.length > 0) { - const pathWhitelistRE = new RegExp(pathWhitelistPattern); - this.pathWhitelistTest = function(path) { - return pathWhitelistRE.test(path); + if (pathSkipPattern.length > 0) { + const pathSkipRE = new RegExp(pathSkipPattern); + this.pathSkipTest = function(path) { + return pathSkipRE.test(path); }; } else { - this.pathWhitelistTest = function() { return false; }; + this.pathSkipTest = function() { return false; }; } } @@ -117,8 +117,8 @@ exports.urlToLocalPath = function *(repo, url) { }; /** - * Check that a given path is on the path synthetic-ref-check whitelist, if - * such a whitelist exists. + * Check that a given path is on the path synthetic-ref-check skiplist, if + * such a skiplist exists. * @async * @param {SyntheticBranchConfig} cfg The configuration for * synthetic_branch_util @@ -128,12 +128,12 @@ exports.urlToLocalPath = function *(repo, url) { function skipCheckForPath(cfg, path) { assert.instanceOf(cfg, SyntheticBranchConfig); assert.isString(path); - return cfg.pathWhitelistTest(path); + return cfg.pathSkipTest(path); } /** - * Check that a given URL is on the URLs synthetic-ref-check whitelist, if - * such a whitelist exists. + * Check that a given URL is on the URLs synthetic-ref-check skiplist, if + * such a skiplist exists. * @async * @param {SyntheticBranchConfig} cfg The configuration for * synthetic_branch_util @@ -143,7 +143,7 @@ function skipCheckForPath(cfg, path) { function skipCheckForURL(cfg, url) { assert.instanceOf(cfg, SyntheticBranchConfig); assert.isString(url); - return cfg.urlWhitelistTest(url); + return cfg.urlSkipTest(url); } /** @@ -212,15 +212,15 @@ function* checkSubmodules(repo, commit) { assert.instanceOf(commit, NodeGit.Commit); const config = yield repo.config(); - const urlWhitelistPattern = ( + const urlSkipPattern = ( yield ConfigUtil.getConfigString( config, "gitmeta.skipsyntheticrefpattern")) || ""; - const pathWhitelistPattern = ( + const pathSkipPattern = ( yield ConfigUtil.getConfigString( config, "gitmeta.skipsyntheticrefpathpattern")) || ""; - const cfg = new SyntheticBranchConfig(urlWhitelistPattern, - pathWhitelistPattern); + const cfg = new SyntheticBranchConfig(urlSkipPattern, + pathSkipPattern); const parent = yield GitUtil.getParentCommit(repo, commit); const names = yield computeChangedSubmodules(repo, diff --git a/node/test/util/stitch_util.js b/node/test/util/stitch_util.js index 501314057..24aee64c8 100644 --- a/node/test/util/stitch_util.js +++ b/node/test/util/stitch_util.js @@ -132,14 +132,14 @@ const getCommitChanges = co.wrap(function *(repo, commit) { }); describe("StitchUtil", function () { -describe("readWhitelist", function () { +describe("readAllowedToFailList", function () { it("breathing", co.wrap(function *() { const written = yield RepoASTTestUtil.createRepo(` -S:N refs/notes/stitched/whitelist 1=`); +S:N refs/notes/stitched/allowed_to_fail 1=`); const repo = written.repo; const head = yield repo.getHeadCommit(); const headSha = head.id().tostrS(); - const result = yield StitchUtil.readWhitelist(repo); + const result = yield StitchUtil.readAllowedToFailList(repo); assert(result.has(headSha)); })); }); @@ -953,13 +953,13 @@ a=B|b=B:Cb-1;Bb=b|x=U:C3-2 s=Sa:b;H=3`, keepAsSubmodule: () => false, fails: true, }, - "missing commit, whitelisted": { + "missing commit, in allowed_to_fail list": { input: ` a=B|b=B:Cb-1;Bb=b|x=U:C3-2 s=Sa:b;H=3`, commit: "3", parents: [], keepAsSubmodule: () => false, - whitelist: ["3"], + allowed_to_fail: ["3"], subCommits: {}, expected: `x=E:Cs ;Bstitched=s`, }, @@ -1000,7 +1000,8 @@ x=S:C2-1 s=Sa:a;C3-1 t=Sa:a;C4-2,3 t=Sa:a,u=Sa:a;H=4;Ba=a`, const skipEmpty = c.skipEmpty || false; const changes = yield getCommitChanges(x, commit); - const whitelist = new Set((c.whitelist || []).map(c => { + const allowed_list = c.allowed_to_fail || []; + const allowed_to_fail = new Set((allowed_list).map(c => { return revMap[c]; })); const stitch = yield StitchUtil.writeStitchedCommit( @@ -1011,7 +1012,7 @@ x=S:C2-1 s=Sa:a;C3-1 t=Sa:a;C4-2,3 t=Sa:a,u=Sa:a;H=4;Ba=a`, c.keepAsSubmodule, adjustPath, skipEmpty, - whitelist); + allowed_to_fail); const subCommits = {}; for (let path in stitch.subCommits) { const commit = stitch.subCommits[path]; @@ -1055,7 +1056,7 @@ x=S:C2-1 s=Sa:a;C3-1 t=Sa:a;C4-2,3 t=Sa:a,u=Sa:a;H=4;Ba=a`, const repo = written.repo; const head = yield repo.getHeadCommit(); const changes = yield getCommitChanges(repo, head); - const whitelist = new Set(); + const allowed_to_fail = new Set(); const stitch = yield StitchUtil.writeStitchedCommit(repo, head, changes, @@ -1063,7 +1064,7 @@ x=S:C2-1 s=Sa:a;C3-1 t=Sa:a;C4-2,3 t=Sa:a,u=Sa:a;H=4;Ba=a`, () => false, (x) => x, false, - whitelist); + allowed_to_fail); const expected = StitchUtil.makeStitchCommitMessage(head, stitch.subCommits); const stitchedCommit = stitch.stitchedCommit; diff --git a/node/test/util/synthetic-branch.js b/node/test/util/synthetic-branch.js index 8fab5c59b..032741baa 100644 --- a/node/test/util/synthetic-branch.js +++ b/node/test/util/synthetic-branch.js @@ -47,7 +47,7 @@ describe("synthetic-branch", function () { const config = yield x.config(); yield config.setString("gitmeta.subreporootpath", "../../"); yield config.setString("gitmeta.skipsyntheticrefpattern", - "whitelisted"); + "skip"); const head = yield x.getHeadCommit(); @@ -152,12 +152,12 @@ describe("synthetic-branch", function () { "|s=S:C8-7;C7-1;Bmaster=8", fails: true }, - // This one should fail, but is in whitelisted URIs, so it passes. + // This one should fail, but is in skipped URIs, so it passes. "with a submodule but no synthetic branch but ignored": { - input: "x=S:C2-1;C3-2 y=S/whitelisted:7;Bmaster=3" + + input: "x=S:C2-1;C3-2 y=S/skip:7;Bmaster=3" + "|y=S:C4-1;Bmaster=4" + "|u=S:C7-1;Bmaster=7", - expected: "x=S:C2-1;C3-2 y=S/whitelisted:7;Bmaster=3;" + + expected: "x=S:C2-1;C3-2 y=S/skip:7;Bmaster=3;" + "N refs/notes/git-meta/subrepo-check 2=ok;" + "N refs/notes/git-meta/subrepo-check 3=ok" + "|y=S:C4-1;Bmaster=4|u=S:C7-1;Bmaster=7" From e1b1a77e7df0e5a4dc769965785980c8ad14df34 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Tue, 7 Jul 2020 00:23:21 -0400 Subject: [PATCH 289/402] If merge failed in a submodule, print what submodule it was. --- node/lib/util/do_work_queue.js | 40 +++++++++++++++++++++++---------- node/lib/util/merge_util.js | 4 +++- node/lib/util/stitch_util.js | 4 +++- node/test/util/do_work_queue.js | 24 +++++++++++++++++++- 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/node/lib/util/do_work_queue.js b/node/lib/util/do_work_queue.js index a728e4e92..4ae4f2c69 100644 --- a/node/lib/util/do_work_queue.js +++ b/node/lib/util/do_work_queue.js @@ -36,27 +36,34 @@ const co = require("co"); /** * Call the specified `getWork` function to create a promise to do work for * each element in the specified `queue`, limiting the amount of parallel work - * to the optionally specified `limit`, if provided, or 20 otherwise. Return - * an array containing the result of the work *in the order that it was + * to the optionally specified `options.limit`, if provided, or 20 otherwise. + * Return an array containing the result of the work *in the order that it was * received*, which may not be the same as the order in which the work was - * completed. + * completed. If `options.failMsg` is provided, it will print an error message + * with element name if the work of the element fails. * * @async * @param {Array} queue * @param {(_, Number) => Promise} getWork - * @param {Number} [limit] - * @param {Array} + * @param {Object} [options] + * @param {Number} options.limit + * @param {String} options.failMsg */ -exports.doInParallel = co.wrap(function *(queue, getWork, limit) { +exports.doInParallel = co.wrap(function *(queue, getWork, options) { assert.isArray(queue); assert.isFunction(getWork); - if (undefined === limit) { - limit = 20; + let limit = 20; + if (options && options.limit) { + assert.isNumber(options.limit); + limit = options.limit; } - else { - assert.isNumber(limit); + let failMsg = ""; + if (options && options.failMsg) { + assert.isString(options.failMsg); + failMsg = options.failMsg; } + const total = queue.length; const result = new Array(total); let next = 0; @@ -64,8 +71,17 @@ exports.doInParallel = co.wrap(function *(queue, getWork, limit) { const doWork = co.wrap(function *() { while (next !== total) { const current = next++; - const currentResult = yield getWork(queue[current], current); - result[current] = currentResult; + try { + const currentResult = yield getWork(queue[current], current); + result[current] = currentResult; + } catch(err) { + if (failMsg) { + console.log( + `'${queue[current]}': ${failMsg}` + ); + } + throw err; + } } }); diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index f9a618300..a697108bd 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -778,7 +778,9 @@ exports.continue = co.wrap(function *(repo) { yield index.conflictRemove(subPath); }); const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo); - yield DoWorkQueue.doInParallel(openSubs, continueSub); + yield DoWorkQueue.doInParallel(openSubs, + continueSub, + {failMsg: "Merge in submodule failed."}); yield SparseCheckoutUtil.writeMetaIndex(repo, index); if ("" !== errorMessage) { diff --git a/node/lib/util/stitch_util.js b/node/lib/util/stitch_util.js index 66350452c..ebe9abb1f 100644 --- a/node/lib/util/stitch_util.js +++ b/node/lib/util/stitch_util.js @@ -977,7 +977,9 @@ ${name}`; yield exports.fetchSubCommits(repo, url, subFetches); console.timeEnd(fetchTimeMessage); }); - yield DoWorkQueue.doInParallel(subNames, doFetch, options.numParallel); + yield DoWorkQueue.doInParallel(subNames, + doFetch, + {limit: options.numParallel}); } console.log("Now stitching"); diff --git a/node/test/util/do_work_queue.js b/node/test/util/do_work_queue.js index 62c204482..5a97af1f8 100644 --- a/node/test/util/do_work_queue.js +++ b/node/test/util/do_work_queue.js @@ -81,8 +81,30 @@ describe("DoWorkQueue", function () { return i * 2; }); } - const result = yield DoWorkQueue.doInParallel(work, getWork, 1); + const result = yield DoWorkQueue.doInParallel(work, + getWork, + {limit: 1}); assert.equal(result.length, NUM_TO_DO); assert.deepEqual(result, expected); })); + + it("sub work failure", co.wrap(function *() { + let work = ["success", "fail"]; + function getWork(name, index) { + if ("fail" === name) { + throw new Error("deliberate error"); + } + return waitSomeTime(index); + } + try { + yield DoWorkQueue.doInParallel( + work, + getWork, + {limit: 1, failMsg: "getWork failed"} + ); + assert.fail("should have failed"); + } catch (error) { + assert.equal("deliberate error", error.message); + } + })); }); From 05871af549939c61e88fa1101031cc26378e1a37 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Wed, 8 Jul 2020 00:01:50 -0400 Subject: [PATCH 290/402] Do not crash even if a submodule is corruptted and its HEAD points to a nonexistent commit --- node/lib/util/merge_util.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index f9a618300..8c31827ba 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -838,6 +838,13 @@ exports.abort = co.wrap(function *(repo) { const subRepo = yield SubmoduleUtil.getRepo(repo, subName); yield resetMerge(subRepo); const subHead = yield subRepo.getHeadCommit(); + if (subHead === null) { + throw new UserError( + `HEAD not found in submodule ${subName}. ` + + "It is likely broken, please try to recover it first." + + "Hint: try to close and then reopen it." + ); + } if (subHead.id().tostrS() !== shas[subName]) { const commit = yield subRepo.getCommit(shas[subName]); yield NodeGit.Reset.reset(subRepo, From c4199d63166ab6419d560723333dd509b59a6eac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2020 05:57:31 +0000 Subject: [PATCH 291/402] Bump lodash from 4.17.15 to 4.17.19 in /node Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19) Signed-off-by: dependabot[bot] --- node/package-lock.json | 126 ++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 77 deletions(-) diff --git a/node/package-lock.json b/node/package-lock.json index 7879e543e..109da56ea 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -396,12 +396,6 @@ "delayed-stream": "~1.0.0" } }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "optional": true - }, "component-props": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/component-props/-/component-props-1.1.1.tgz", @@ -528,9 +522,9 @@ "dev": true }, "dom-serializer": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz", - "integrity": "sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", "requires": { "domelementtype": "^2.0.1", "entities": "^2.0.0" @@ -542,9 +536,9 @@ "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" }, "entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", - "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", + "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==" } } }, @@ -913,16 +907,22 @@ "dev": true }, "handlebars": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz", - "integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==", + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", "requires": { + "minimist": "^1.2.5", "neo-async": "^2.6.0", - "optimist": "^0.6.1", "source-map": "^0.6.1", - "uglify-js": "^3.1.4" + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" }, "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -1193,9 +1193,9 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jshint": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.10.2.tgz", - "integrity": "sha512-e7KZgCSXMJxznE/4WULzybCMNXNAd/bf5TSrvVEq78Q/K8ZwFpmBqQeDtNiHc3l49nV4E/+YeHU/JZjSUIrLAA==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.11.1.tgz", + "integrity": "sha512-WXWePB8ssAH3DlD05IoqolsY6arhbll/1+i2JkRPpihQAuiNaR/gSt8VKIcxpV5m6XChP0hCwESQUqpuQMA8Tg==", "requires": { "cli": "~1.0.0", "console-browserify": "1.1.x", @@ -1296,9 +1296,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "log-symbols": { "version": "2.2.0", @@ -1542,9 +1542,9 @@ } }, "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "nice-try": { "version": "1.0.5", @@ -1810,38 +1810,17 @@ "wrappy": "1" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - } - } - }, "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "requires": { "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", + "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", - "wordwrap": "~1.0.0" + "word-wrap": "~1.2.3" } }, "os-homedir": { @@ -2105,9 +2084,9 @@ "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" }, "rimraf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", - "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "requires": { "glob": "^7.1.3" } @@ -2318,9 +2297,9 @@ } }, "temp": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.0.tgz", - "integrity": "sha512-YfUhPQCJoNQE5N+FJQcdPz63O3x3sdT4Xju69Gj4iZe0lBKOtnAMi0SLj9xKhGkcGhsxThvTJ/usxtFPo438zQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.1.tgz", + "integrity": "sha512-WMuOgiua1xb5R56lE0eH6ivpVmg/lq2OHm4+LtT/xtEtPQ+sz6N3bBM6WZ5FvO1lO4IKIOb43qnhoc4qxP5OeA==", "requires": { "rimraf": "~2.6.2" }, @@ -2336,9 +2315,9 @@ } }, "thenify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", - "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "requires": { "any-promise": "^1.0.0" } @@ -2412,22 +2391,10 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" }, "uglify-js": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.5.tgz", - "integrity": "sha512-7L3W+Npia1OCr5Blp4/Vw83tK1mu5gnoIURtT1fUVfQ3Kf8WStWV6NJz0fdoBJZls0KlweruRTLVe6XLafmy5g==", - "optional": true, - "requires": { - "commander": "~2.20.3", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true - } - } + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.0.tgz", + "integrity": "sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA==", + "optional": true }, "uniq": { "version": "1.0.1", @@ -2497,6 +2464,11 @@ "string-width": "^1.0.2 || 2" } }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", From 326b18a6ca6435478d43eb278bc1139b0dfcb087 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 16 Jul 2020 16:37:20 -0400 Subject: [PATCH 292/402] rename confusingly-named function --- node/lib/cmd/open.js | 2 +- node/lib/util/add.js | 2 +- node/lib/util/add_submodule.js | 2 +- node/lib/util/checkout.js | 2 +- node/lib/util/cherry_pick_util.js | 2 +- node/lib/util/close_util.js | 3 ++- node/lib/util/commit.js | 4 ++-- node/lib/util/merge_util.js | 6 +++--- node/lib/util/open.js | 12 ++++++------ node/lib/util/reset.js | 2 +- node/lib/util/rm.js | 2 +- node/lib/util/sparse_checkout_util.js | 2 +- node/lib/util/stash_util.js | 3 ++- node/lib/util/submodule_config_util.js | 5 +++-- node/lib/util/submodule_rebase_util.js | 2 +- 15 files changed, 27 insertions(+), 24 deletions(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index d4a2eba1d..5c47e9123 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -155,7 +155,7 @@ Opening ${colors.blue(name)} on ${colors.green(shas[idx])}.`); // Make sure the index entries are updated in case we're in sparse mode. - yield SparseCheckoutUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); if (subsOpenSuccessfully.length) { // Run post-open-submodule hook with successfully-opened submodules diff --git a/node/lib/util/add.js b/node/lib/util/add.js index fb163d4c0..05175ef1a 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -116,6 +116,6 @@ exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update) { if (0 !== toAdd.length) { const index = yield repo.index(); yield toAdd.map(filename => index.addByPath(filename)); - yield SparseCheckoutUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); } }); diff --git a/node/lib/util/add_submodule.js b/node/lib/util/add_submodule.js index da44be91a..98e7ef589 100644 --- a/node/lib/util/add_submodule.js +++ b/node/lib/util/add_submodule.js @@ -66,7 +66,7 @@ exports.addSubmodule = co.wrap(function *(repo, url, filename, importArg) { const urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); urls[filename] = url; yield SubmoduleConfigUtil.writeUrls(repo, index, urls); - yield SparseCheckoutUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); const metaUrl = yield GitUtil.getOriginUrl(repo); const templatePath = yield SubmoduleConfigUtil.getTemplatePath(repo); diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index a7c62f7c7..87a843b9a 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -235,7 +235,7 @@ exports.checkoutCommit = co.wrap(function *(repo, commit, force) { repo.setHeadDetached(commit); }); yield DoWorkQueue.doInParallel(Object.keys(subs), doCheckout); - yield SparseCheckoutUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); }); /** diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 274de091e..50c63d6a5 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -746,7 +746,7 @@ exports.rewriteCommit = co.wrap(function *(repo, commit) { errorMessage: errorMessage === "" ? null : errorMessage, newMetaCommit: null, }; - yield SparseCheckoutUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); const nChanges = Object.keys(picks.commits) .map(name => Object.keys( picks.commits[name]).length + picks.ffwds[name] ? 1 : 0 diff --git a/node/lib/util/close_util.js b/node/lib/util/close_util.js index 97d43b622..af2032ef0 100644 --- a/node/lib/util/close_util.js +++ b/node/lib/util/close_util.js @@ -102,7 +102,8 @@ Pass ${colors.magenta("--force")} to close it anyway. // Write out the meta index to update SKIP_WORKTREE flags for closed // submodules. - yield SparseCheckoutUtil.writeMetaIndex(repo, yield repo.index()); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, + yield repo.index()); // Run post-close-submodule hook with submodules which closed successfully. yield Hook.execHook(repo, "post-close-submodule", subsClosedSuccessfully); diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index a7cbe9d33..ff906e234 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -370,7 +370,7 @@ const stageOpenSubmodules = co.wrap(function *(repo, index, submodules) { yield index.addByPath(name); } })); - yield SparseCheckoutUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); }); /** @@ -633,7 +633,7 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message) { } } - yield SparseCheckoutUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); // Use 'TreeUtil' to create a new tree having the required paths. diff --git a/node/lib/util/merge_util.js b/node/lib/util/merge_util.js index 223b7c721..27775a1ad 100644 --- a/node/lib/util/merge_util.js +++ b/node/lib/util/merge_util.js @@ -565,7 +565,7 @@ const mergeStepMergeSubmodules = co.wrap(function *(context) { // will go away if (!forceBare) { yield CherryPickUtil.closeSubs(opener, merges); - yield SparseCheckoutUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); } if ("" !== conflictMessage) { @@ -781,7 +781,7 @@ exports.continue = co.wrap(function *(repo) { yield DoWorkQueue.doInParallel(openSubs, continueSub, {failMsg: "Merge in submodule failed."}); - yield SparseCheckoutUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); if ("" !== errorMessage) { throw new UserError(errorMessage); @@ -859,7 +859,7 @@ exports.abort = co.wrap(function *(repo) { }); yield DoWorkQueue.doInParallel(openSubs, abortSub); yield index.conflictCleanup(); - yield SparseCheckoutUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); yield resetMerge(repo); yield SequencerStateUtil.cleanSequencerState(repo.path()); }); diff --git a/node/lib/util/open.js b/node/lib/util/open.js index 021c114cc..3c0fd2c79 100644 --- a/node/lib/util/open.js +++ b/node/lib/util/open.js @@ -62,9 +62,9 @@ exports.SUB_OPEN_OPTION = SUB_OPEN_OPTION; * is provided, use it to configure the newly-opened submodule's repository. * * Note that after opening one or more submodules, - * `SparseCheckoutUtil.writeMetaIndex` must be called so that `SKIP_WORKTREE` - * is *unset*; since this operation is expensive, we cannot do it automatically - * each time a submodule is opened. + * `SparseCheckoutUtil.setSparseBitsAndWriteIndex` must be called so that + * `SKIP_WORKTREE` is *unset*; since this operation is expensive, we cannot do + * it automatically each time a submodule is opened. * * @async * @param {SubmoduleFetcher} fetcher @@ -314,9 +314,9 @@ Opening ${colors.blue(subName)} on ${colors.green(sha)}.`); * - open normal repo otherwise * * Note that after opening one or more submodules, - * `SparseCheckoutUtil.writeMetaIndex` must be called so that `SKIP_WORKTREE` - * is *unset*; since this operation is expensive, we cannot do it automatically - * each time a submodule is opened. + * `SparseCheckoutUtil.setSparseBitsAndWriteIndex` must be called so that + * `SKIP_WORKTREE` is *unset*; since this operation is expensive, we cannot do + * it automatically each time a submodule is opened. * * @param {String} subName * @param {SUB_OPEN_OPTION} openOption diff --git a/node/lib/util/reset.js b/node/lib/util/reset.js index 0322d4346..a559be6b2 100644 --- a/node/lib/util/reset.js +++ b/node/lib/util/reset.js @@ -276,7 +276,7 @@ exports.reset = co.wrap(function *(repo, commit, type) { // Write the index in case we've had to stage submodule changes. - yield SparseCheckoutUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); }); /** diff --git a/node/lib/util/rm.js b/node/lib/util/rm.js index 7c78a4e9e..2036c3fa7 100644 --- a/node/lib/util/rm.js +++ b/node/lib/util/rm.js @@ -432,7 +432,7 @@ exports.rmPaths = co.wrap(function *(repo, paths, options) { // https://github.com/nodegit/nodegit/issues/1487 if (toRemove.length !== 0) { yield index.removeAll(toRemove); - yield SparseCheckoutUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); } // close to-be-deleted submodules diff --git a/node/lib/util/sparse_checkout_util.js b/node/lib/util/sparse_checkout_util.js index 59e7947f8..581fc5d11 100644 --- a/node/lib/util/sparse_checkout_util.js +++ b/node/lib/util/sparse_checkout_util.js @@ -155,7 +155,7 @@ exports.removeFromSparseCheckoutFile = function (repo, filenames) { * @param {NodeGit.Repository} repo * @param {NodeGit.Index} index */ -exports.writeMetaIndex = co.wrap(function *(repo, index) { +exports.setSparseBitsAndWriteIndex = co.wrap(function *(repo, index) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(index, NodeGit.Index); diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 225700f85..bc240e788 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -356,7 +356,8 @@ ${colors.red(name)}`); result[name] = stashSha; } })); - yield SparseCheckoutUtil.writeMetaIndex(repo, yield repo.index()); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, + yield repo.index()); return result; }); diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js index d83c6f4f7..a298efe70 100644 --- a/node/lib/util/submodule_config_util.js +++ b/node/lib/util/submodule_config_util.js @@ -143,8 +143,9 @@ exports.clearSubmoduleConfigEntry = * De-initialize the repositories having the specified `submoduleNames` in the * specified `repo`. * - * Note that after calling this method, `SparseCheckoutUtil.writeMetaIndex` - * must be called to update the SKIP_WORKTREE flags for closed submodules. + * Note that after calling this method, + * `SparseCheckoutUtil.setSparseBitsAndWriteIndex` must be called to update + * the SKIP_WORKTREE flags for closed submodules. * * @async * @param {NodeGit.Repository} repo diff --git a/node/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js index 379ce4043..efa7dca6a 100644 --- a/node/lib/util/submodule_rebase_util.js +++ b/node/lib/util/submodule_rebase_util.js @@ -367,7 +367,7 @@ ${colors.green(rebaseInfo.onto)}.`); } }); yield DoWorkQueue.doInParallel(Object.keys(subs), continueSub); - yield SparseCheckoutUtil.writeMetaIndex(repo, index); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); const result = { errorMessage: "" === errorMessage ? null : errorMessage, commits: commits, From 68bca90947ad0625548364966bf2be03093fe781 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 17 Jul 2020 14:13:57 -0400 Subject: [PATCH 293/402] spelling --- node/lib/util/commit.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index ff906e234..c448ef4fb 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -109,7 +109,7 @@ const getAmendStatusForRepo = co.wrap(function *(repo, all) { }); /** - * This class reprents the meta-data associated with a commit. + * This class represents the meta-data associated with a commit. */ class CommitMetaData { /** @@ -450,7 +450,7 @@ exports.shouldCommit = function (status, skipMeta, subMessages) { * commit and the shas of any commits generated in submodules. The behavior is * undefined if there are entries in `.gitmodules` for submodules having no * commits, or if `null === message && undefined === subMessages`. The - * behavior is undefined unless there is somthing to commit. + * behavior is undefined unless there is something to commit. * * @async * @param {NodeGit.Repository} metaRepo @@ -1216,7 +1216,7 @@ exports.amendMetaRepo = co.wrap(function *(repo, // and so some things that should be changed will not be in it. // We cannot call `Index.addAll` because it will stage // untracked files. Therefore, we need to use our normal - // status routine to examime the workdir and stage changed + // status routine to examine the workdir and stage changed // files. const workdir = actualStatus.workdir; From 6bd74a728b95df34829a4d9d3dec637c9f7382b4 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 21 Jul 2020 16:54:26 -0400 Subject: [PATCH 294/402] Give pre-commit hooks in submodules access to temp index --- node/lib/cmd/commit.js | 27 +- node/lib/util/commit.js | 399 +++++++++++++------------- node/lib/util/git_util.js | 26 ++ node/lib/util/hook.js | 7 +- node/lib/util/sparse_checkout_util.js | 17 +- node/package-lock.json | 116 -------- node/package.json | 1 - node/test/util/commit.js | 192 ++++++------- 8 files changed, 339 insertions(+), 446 deletions(-) diff --git a/node/lib/cmd/commit.js b/node/lib/cmd/commit.js index 7f33fea3b..8072988d9 100644 --- a/node/lib/cmd/commit.js +++ b/node/lib/cmd/commit.js @@ -120,25 +120,13 @@ const doCommit = co.wrap(function *(args) { const repo = yield GitUtil.getCurrentRepo(); const cwd = process.cwd(); - if (!args.no_verify) { - try { - yield Commit.execSubmodulePrecommitHooks(repo, - cwd, - args.all, - args.file, - args.interactive); - } catch (e) { - console.error("Failed to commit. \n" + e.message); - process.exit(1); - } - } - yield Commit.doCommitCommand(repo, cwd, args.message, args.all, args.file, args.interactive, + args.no_verify, GitUtil.editMessage); yield Hook.execHook(repo, "post-commit"); }); @@ -158,19 +146,6 @@ const doAmend = co.wrap(function *(args) { const repo = yield GitUtil.getCurrentRepo(); const cwd = process.cwd(); - if (!args.no_verify) { - try { - yield Commit.execSubmodulePrecommitHooks(repo, - cwd, - args.all, - args.file, - args.interactive); - } catch (e) { - console.error("Failed to commit. \n" + e.message); - process.exit(1); - } - } - yield Commit.doAmendCommand(repo, cwd, args.message, diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index c448ef4fb..d30c380d4 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -45,9 +45,11 @@ const NodeGit = require("nodegit"); const path = require("path"); const ConfigUtil = require("./config_util"); +const CherryPickUtil = require("./cherry_pick_util.js"); const DoWorkQueue = require("../util/do_work_queue"); const DiffUtil = require("./diff_util"); const GitUtil = require("./git_util"); +const Hook = require("../util/hook"); const Open = require("./open"); const RepoStatus = require("./repo_status"); const PrintStatusUtil = require("./print_status_util"); @@ -194,55 +196,61 @@ exports.stageChange = co.wrap(function *(index, path, change) { } }); +const runHooks = co.wrap(function *(repo, index) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(index, NodeGit.Index); + + const tempIndexPath = repo.path() + "index.gmtmp"; + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index, + tempIndexPath); + + try { + if (Hook.hasHook(repo, "pre-commit")) { + const isOk = yield Hook.execHook(repo, "pre-commit", [], + { GIT_INDEX_FILE: tempIndexPath}); + yield GitUtil.overwriteIndexFromFile(index, tempIndexPath); + + if (!isOk) { + // hooks are responsible for printing their own message + throw new Error(""); + } + } + } finally { + yield fs.unlink(tempIndexPath); + } +}); + + /** - * Commit changes in the specified `repo`. If the specified `doAll` is true, - * stage files indicated that they are to be committed in the `staged` section. - * Use the specified `message` as the commit message. If there are no files to - * commit and `false === force`, do nothing and return null; otherwise, return - * the created commit object. Ignore submodules. Use the specified - * `signature` to identify the commit creator. + * Prepare a temp index in the specified `repo`, then run the hooks, + * and read back the index. If the specified `doAll` is true, stage + * files indicated that they are to be committed in the `staged` + * section. Ignore submodules. * * @async * @param {NodeGit.Repository} repo * @param {RepoStatus} repoStatus * @param {Boolean} doAll - * @param {String} message - * @param {Boolean} force - * @param {NodeGit.Signature} signature * @return {NodeGit.Oid|null} */ -const commitRepo = co.wrap(function *(repo, - changes, - doAll, - message, - force, - signature) { +const prepareIndexAndRunHooks = co.wrap(function *(repo, + changes, + doAll) { assert.instanceOf(repo, NodeGit.Repository); assert.isObject(changes); - assert.isBoolean(doAll); - assert.isString(message); - assert.isBoolean(force); - assert.instanceOf(signature, NodeGit.Signature); - const doCommit = 0 !== Object.keys(changes).length || force; + const index = yield repo.index(); // If we're auto-staging files, loop through workdir and stage them. if (doAll) { - const index = yield repo.index(); for (let path in changes) { yield exports.stageChange(index, path, changes[path]); } - yield index.write(); - } - if (doCommit) { - return yield repo.createCommitOnHead( - [], - signature, - signature, - exports.ensureEolOnLastLine(message)); + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); } - return null; + + yield runHooks(repo, index); }); const editorMessagePrefix = `\ @@ -438,6 +446,22 @@ exports.shouldCommit = function (status, skipMeta, subMessages) { return false; }; +const updateHead = co.wrap(function *(repo, sha) { + // Now we need to put the commit on head. We need to unstage + // the changes we've just committed, otherwise we see + // conflicts with the workdir. We do a SOFT reset because we + // don't want to affect index changes for paths you didn't + // touch. + + // This will return the same one that we modified above + const index = yield repo.index(); + // First, we write the new index to the actual index file. + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); + + const commit = yield repo.getCommit(sha); + yield NodeGit.Reset.reset(repo, commit, NodeGit.Reset.TYPE.SOFT); +}); + /** * Create a commit across modified repositories and the specified `metaRepo` * with the specified `message`; if `null === message`, do not create a commit @@ -487,8 +511,8 @@ exports.commit = co.wrap(function *(metaRepo, // Commit submodules. If any changes, remember this so we know to generate // a commit in the meta-repo whether or not the meta-repo has its own // workdir changes. - const subCommits = {}; + const subRepos = {}; const commitSubmodule = co.wrap(function *(name) { let subMessage = message; @@ -503,21 +527,51 @@ exports.commit = co.wrap(function *(metaRepo, } const status = submodules[name]; const repoStatus = (status.workdir && status.workdir.status) || null; + if (null !== repoStatus && 0 !== Object.keys(repoStatus.staged).length) { const subRepo = yield SubmoduleUtil.getRepo(metaRepo, name); - const commit = yield commitRepo(subRepo, - repoStatus.staged, - all, - subMessage, - false, - signature); + + yield prepareIndexAndRunHooks(subRepo, + repoStatus.staged, + all); + + const headCommit = yield subRepo.getHeadCommit(); + const parents = []; + if (headCommit !== null) { + parents.push(headCommit); + } + const index = yield subRepo.index(); + const tree = yield index.writeTree(); + + const commit = yield subRepo.createCommit( + null, + signature, + signature, + exports.ensureEolOnLastLine(subMessage), + tree, + parents); + + subRepos[name] = subRepo; subCommits[name] = commit.tostrS(); } }); + const index = yield metaRepo.index(); + yield DoWorkQueue.doInParallel(Object.keys(submodules), commitSubmodule); + if (all) { + for (const subName of Object.keys(metaStatus.staged)) { + exports.stageChange(index, subName, metaStatus.staged[subName]); + } + } + + for (const subName of Object.keys(subCommits)) { + const subRepo = subRepos[subName]; + const sha = subCommits[subName]; + yield updateHead(subRepo, sha); + } const result = { metaCommit: null, submoduleCommits: subCommits, @@ -527,16 +581,19 @@ exports.commit = co.wrap(function *(metaRepo, return result; // RETURN } - const index = yield metaRepo.index(); + // TODO temp index and meta pre-commit hook yield stageOpenSubmodules(metaRepo, index, submodules); - result.metaCommit = yield commitRepo(metaRepo, - metaStatus.staged, - all, - message, - true, - signature); + const tree = yield index.writeTree(); + const headCommit = yield metaRepo.getHeadCommit(); + result.metaCommit = yield metaRepo.createCommit( + "HEAD", + signature, + signature, + exports.ensureEolOnLastLine(message), + tree, + [headCommit]); return result; }); @@ -564,12 +621,14 @@ const isExecutable = co.wrap(function *(repo, filename) { }); /** - * Write a commit for the specified `repo` having the specified - * `status` using the specified commit `message` and return the ID of the new - * commit. Note that this method records staged commits for submodules but - * does not recurse into their repositories. Note also that changes that would - * involve altering `.gitmodules` -- additions, removals, and URL changes -- - * are ignored. + * Create a temp index and run the pre-commit hooks for the specified + * `repo` having the specified `status` using the specified commit + * `message` and return the ID of the new commit. Note that this + * method records staged commits for submodules but does not recurse + * into their repositories. Note also that changes that would involve + * altering `.gitmodules` -- additions, removals, and URL changes -- + * are ignored. HEAD and the main on-disk index file are not changed, + * although the in-memory index is altered. * * @param {NodeGit.Repository} repo * @param {RepoStatus} status @@ -592,6 +651,7 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message) { // Therefore, all of our files must be staged. const index = yield repo.index(); + yield index.readTree(yield headCommit.getTree()); // First, handle "normal" file changes. @@ -626,19 +686,17 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message) { changes[subName] = new Change(id, FILEMODE.COMMIT); // Stage this submodule if it's open. - - if (null !== sub.workdir) { - yield index.addByPath(subName); - } + yield CherryPickUtil.addSubmoduleCommit(index, subName, + sub.index.sha); } } - yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); + yield runHooks(repo, index); // Use 'TreeUtil' to create a new tree having the required paths. - const baseTree = yield headCommit.getTree(); - const tree = yield TreeUtil.writeTree(repo, baseTree, changes); + const treeId = yield index.writeTree(); + const tree = yield NodeGit.Tree.lookup(repo, treeId); // Create a commit with this tree. @@ -655,17 +713,31 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message) { parents.length, parents); - // Now we need to put the commit on head. We need to unstage the changes - // we've just committed, otherwise we see conflicts with the workdir. We - // do a SOFT reset because we don't want to affect index changes for paths - // you didn't touch. + //restore the index + // Now, reload the index to get rid of the changes we made to it. + // In theory, this should be index.read(true), but that doesn't + // work for some reason. + yield GitUtil.overwriteIndexFromFile(index, index.path()); + + // ...and apply the changes from the commit that we just made. + // I'm pretty sure this doesn't do rename/copy detection, but the nodegit + // API docs are pretty vague, as are the libgit2 docs. + const commit = yield NodeGit.Commit.lookup(repo, commitId); + const diffs = yield commit.getDiff(); + const diff = diffs[0]; + for (let i = 0; i < diff.numDeltas(); i++) { + const delta = diff.getDelta(i); + const newFile = delta.newFile(); + if (GitUtil.isZero(newFile.id())) { + index.removeByPath(delta.oldFile().path()); + } else { + index.addByPath(newFile.path()); + } + } - const commit = yield repo.getCommit(commitId); - yield NodeGit.Reset.reset(repo, commit, NodeGit.Reset.TYPE.SOFT); - return commitId.tostrS(); + return commitId; }); - /** * Commit changes to the files indicated as staged by the specified `status` * in the specified `repo`, applying the specified commit `message`. Return an @@ -710,7 +782,8 @@ exports.commitPaths = co.wrap(function *(repo, status, message) { const wdStatus = workdir.status; const subRepo = yield SubmoduleUtil.getRepo(repo, subName); - const sha = yield exports.writeRepoPaths(subRepo, wdStatus, message); + const oid = yield exports.writeRepoPaths(subRepo, wdStatus, message); + const sha = oid.tostrS(); subCommits[subName] = sha; const oldIndex = sub.index; const Submodule = RepoStatus.Submodule; @@ -722,18 +795,26 @@ exports.commitPaths = co.wrap(function *(repo, status, message) { headCommit: sha, }), Submodule.COMMIT_RELATION.SAME) }); + committedSubs[subName].repo = subRepo; }); yield DoWorkQueue.doInParallel(Object.keys(subs), writeSubPaths); + for (const subName of Object.keys(committedSubs)) { + const sub = committedSubs[subName]; + yield updateHead(sub.repo, sub.index.sha); + } + // We need a `RepoStatus` object containing only the set of the submodules // to commit to pass to `writeRepoPaths`. const pathStatus = status.copy({ submodules: committedSubs, }); - const id = yield exports.writeRepoPaths(repo, pathStatus, message); + const commit = yield repo.getCommit(id); + yield NodeGit.Reset.reset(repo, commit, NodeGit.Reset.TYPE.SOFT); + return { metaCommit: id, submoduleCommits: subCommits, @@ -1077,14 +1158,15 @@ exports.getAmendStatus = co.wrap(function *(repo, options) { }); /** - * Amend the specified `repo`, using the specified commit `message`, and return - * the sha of the created commit. + * Create a commit in the specified `repo`, based on the HEAD commit, + * using the specified commit `message`, and return the sha of the + * created commit. * * @param {NodeGit.Repository} repo * @param {String} message - * @return {String} + * @return {NodeGit.Oid} */ -exports.amendRepo = co.wrap(function *(repo, message) { +exports.createAmendCommit = co.wrap(function *(repo, message) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(message); @@ -1093,10 +1175,17 @@ exports.amendRepo = co.wrap(function *(repo, message) { const treeId = yield index.writeTree(); const tree = yield NodeGit.Tree.lookup(repo, treeId); const termedMessage = exports.ensureEolOnLastLine(message); - const id = yield head.amend("HEAD", null, null, null, termedMessage, tree); - return id.tostrS(); + const id = yield repo.createCommit( + null, + head.author(), + head.committer(), + termedMessage, + tree, + head.parents()); + return id; }); + /** * Amend the specified meta `repo` and the shas of the created commits. Amend * the head of `repo` and submodules listed in the specified `subsToAmend` @@ -1151,9 +1240,9 @@ exports.amendMetaRepo = co.wrap(function *(repo, }); const subCommits = {}; + const subRepos = {}; const subs = status.submodules; const amendSubSet = new Set(subsToAmend); - yield Object.keys(subs).map(co.wrap(function *(subName) { // If we're providing specific sub messages, use it if provided and // skip committing the submodule otherwise. @@ -1165,6 +1254,7 @@ exports.amendMetaRepo = co.wrap(function *(repo, return; // RETURN } } + subMessage = exports.ensureEolOnLastLine(subMessage); const subStatus = subs[subName]; // We're working on only open repos (they would have been opened @@ -1186,10 +1276,12 @@ exports.amendMetaRepo = co.wrap(function *(repo, } const subRepo = yield SubmoduleUtil.getRepo(repo, subName); + subRepos[subName] = subRepo; + const head = yield subRepo.getHeadCommit(); + const subIndex = yield subRepo.index(); // If the submodule is to be amended, we don't do the normal commit // process. - if (doAmend) { // First, we check to see if this submodule needs to have its last // commit stripped. That will be the case if we have no files @@ -1197,7 +1289,6 @@ exports.amendMetaRepo = co.wrap(function *(repo, assert.isNotNull(repoStatus); if (0 === numStaged) { - const head = yield subRepo.getHeadCommit(); const parent = yield GitUtil.getParentCommit(subRepo, head); const TYPE = NodeGit.Reset.TYPE; const type = all ? TYPE.HARD : TYPE.MIXED; @@ -1205,7 +1296,6 @@ exports.amendMetaRepo = co.wrap(function *(repo, return; // RETURN } - const subIndex = yield subRepo.index(); if (all) { const actualStatus = yield StatusUtil.getRepoStatus(subRepo, { showMetaChanges: true, @@ -1226,30 +1316,56 @@ exports.amendMetaRepo = co.wrap(function *(repo, yield exports.stageChange(subIndex, path, change); } } - yield subIndex.write(); + yield runHooks(subRepo, subIndex); } - subCommits[subName] = yield exports.amendRepo(subRepo, subMessage); + subCommits[subName] = yield exports.createAmendCommit(subRepo, + subMessage); return; // RETURN } - const commit = yield commitRepo(subRepo, - staged, - all, - subMessage, - false, - signature); + if (all) { + const actualStatus = yield StatusUtil.getRepoStatus(subRepo, { + showMetaChanges: true, + }); + + const workdir = actualStatus.workdir; + for (let path in actualStatus.workdir) { + const change = workdir[path]; + if (RepoStatus.FILESTATUS.ADDED !== change) { + yield exports.stageChange(subIndex, path, change); + } + } + } + + yield runHooks(subRepo, subIndex); + const tree = yield subIndex.writeTree(); + const commit = yield subRepo.createCommit( + null, + signature, + signature, + subMessage, + tree, + [head]); + if (null !== commit) { - subCommits[subName] = commit.tostrS(); + subCommits[subName] = commit; } })); + for (const subName of Object.keys(subCommits)) { + const subCommit = subCommits[subName]; + const subRepo = subRepos[subName]; + yield updateHead(subRepo, subCommit.tostrS()); + } + let metaCommit = null; if (null !== message) { const index = yield repo.index(); yield stageOpenSubmodules(repo, index, subs); yield stageFiles(repo, status.staged, index); - metaCommit = yield exports.amendRepo(repo, message); + metaCommit = yield exports.createAmendCommit(repo, message); + yield updateHead(repo, metaCommit.tostrS()); } return { metaCommit: metaCommit, @@ -1985,6 +2101,7 @@ exports.doCommitCommand = co.wrap(function *(repo, all, paths, interactive, + noVerify, editMessage) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(cwd); @@ -1994,6 +2111,7 @@ exports.doCommitCommand = co.wrap(function *(repo, assert.isBoolean(all); assert.isArray(paths); assert.isBoolean(interactive); + assert.isBoolean(noVerify); assert.isFunction(editMessage); const workdir = repo.workdir(); @@ -2223,110 +2341,3 @@ it empty. You can remove the commit entirely with "git meta reset HEAD^".`); subMessages); }); -const submoduleHasChangesToCommit = function(status, submoduleName) { - const submodule = status.submodules[submoduleName]; - if (!submodule) { - return false; - } - const { workdir } = submodule; - return null !== workdir && !workdir.status.isIndexClean(); -}; - -const submoduleHasPrecommitHook = co.wrap(function *(repo, submoduleName) { - const Hook = require("../util/hook"); - const SubmoduleUtil = require("../util/submodule_util"); - - const subRepo = yield SubmoduleUtil.getRepo(repo, submoduleName); - return Hook.hasHook(subRepo, "pre-commit"); -}); - -const filterInParallel = co.wrap(function *(collection, asyncPredicate) { - const DoWorkQueue = require("../util/do_work_queue"); - - const booleans = yield DoWorkQueue.doInParallel(collection, - asyncPredicate); - return collection.filter((item, index) => booleans[index]); -}); - -/** - * Get the names of submodules that should run 'pre-commit' hook against. - * This means the submodule must have a 'pre-commit' hook as well as have - * changes to commit - * - * @async - * @param {NodeGit.Repository} repo - * @param {RepoStatus} repoStatus - * @return {String[]} - */ -exports.getSubmoduleNamesForPrecommitCheck = co.wrap(function *(repo, - repoStatus) { - const SubmoduleUtil = require("../util/submodule_util"); - - const allSubmoduleNames = yield SubmoduleUtil.getSubmoduleNames(repo); - return yield filterInParallel( - allSubmoduleNames, - co.wrap(function *(submoduleName) { - return submoduleHasChangesToCommit(repoStatus, submoduleName) && - (yield submoduleHasPrecommitHook(repo, submoduleName)); - }) - ); -}); - -/** - * Execute 'pre-commit' hooks of submodules. Only execute when a submodule - * has the hook and has changes to commit. - * If there is an error occurred, throw an error - * If there are hooks to be run and interactive mode is on, a warning will be - * raised to communicate that 'pre-commit' hooks were skipped. - * - * @async - * @param {NodeGit.Repository} repo - * @param {String} cwd - * @param {Boolean} all - * @param {String[]} paths - * @param {Boolean} interactive - * @return {void} - */ -exports.execSubmodulePrecommitHooks = co.wrap(function *(repo, - cwd, - all, - paths, - interactive) { - const Hook = require("../util/hook"); - const SubmoduleUtil = require("../util/submodule_util"); - - const repoStatus = yield exports.getCommitStatus(repo, cwd, { - all, - paths, - }); - - const submoduleNames = yield exports.getSubmoduleNamesForPrecommitCheck( - repo, - repoStatus - ); - - if (submoduleNames.length > 0) { - if (interactive) { - // "interactive mode is not supported because pre-commit hooks - // happen before we offer the commit message for editing, - // and in interactive mode, we don't know which hooks actually - // need to be run until the editing is done." - console.warn( - "Warning. pre-commit hooks skipped when using option [-i]" - ); - } else { - // execute pre-commit hook for each submodule - for (const submoduleName of submoduleNames) { - const subRepo = yield SubmoduleUtil.getRepo( - repo, - submoduleName - ); - const isOk = yield Hook.execHook(subRepo, "pre-commit"); - if (!isOk) { - // hooks are responsible for printing their own message - throw new Error(""); - } - } - } - } -}); diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 431e6c3a1..ea75abfed 100755 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -997,3 +997,29 @@ exports.getReference = co.wrap(function *(repo, name) { } return null; }); + +/** + * Read an index file, and overwrite an in-memory index with its contents. + * + * @param {NodeGit.Index} index + * @param {String|undefined} path if set, whence to read the index + */ +exports.overwriteIndexFromFile = co.wrap(function*(index, path) { + assert.instanceOf(index, NodeGit.Index); + assert.isString(path); + + // TODO: in theory, it might be possible to just check the checksum + // on the index to avoid reloading it in the common case where nothing + // has changed. + const newIndex = yield NodeGit.Index.open(path); + yield index.removeAll(); + for (const e of newIndex.entries()) { + yield index.add(e); + } +}); + + +// This is documented as oid.isZero() but that doesn't actually work. +exports.isZero = function(oid) { + return oid.tostrS() === "0000000000000000000000000000000000000000"; +}; diff --git a/node/lib/util/hook.js b/node/lib/util/hook.js index 10e0295d3..af26dbf39 100644 --- a/node/lib/util/hook.js +++ b/node/lib/util/hook.js @@ -61,7 +61,7 @@ exports.hasHook = function (repo, name) { * @param {String[]} args * @return {Boolean} */ -exports.execHook = co.wrap(function*(repo, name, args=[]) { +exports.execHook = co.wrap(function*(repo, name, args=[], env={}) { assert.isString(name); const rootDirectory = repo.path(); @@ -74,7 +74,10 @@ exports.execHook = co.wrap(function*(repo, name, args=[]) { try { process.chdir(repo.workdir()); - yield spawn(absPath, args, { stdio: "inherit" }); + const subEnv = {}; + Object.assign(subEnv, process.env); + Object.assign(subEnv, env); + yield spawn(absPath, args, { stdio: "inherit", env: subEnv }); return true; } catch (e) { if (e.code === "EACCES") { diff --git a/node/lib/util/sparse_checkout_util.js b/node/lib/util/sparse_checkout_util.js index 581fc5d11..cae476417 100644 --- a/node/lib/util/sparse_checkout_util.js +++ b/node/lib/util/sparse_checkout_util.js @@ -154,8 +154,10 @@ exports.removeFromSparseCheckoutFile = function (repo, filenames) { * * @param {NodeGit.Repository} repo * @param {NodeGit.Index} index + * @param {String|undefined} path if set, where to write the index */ -exports.setSparseBitsAndWriteIndex = co.wrap(function *(repo, index) { +exports.setSparseBitsAndWriteIndex = co.wrap(function *(repo, index, + path = undefined) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(index, NodeGit.Index); @@ -177,5 +179,18 @@ exports.setSparseBitsAndWriteIndex = co.wrap(function *(repo, index) { } } } + // This is a horrible hack that we need because nodegit doesn't + // have a way to write the index to anywhere other than the + // location from whence it was opened. + if (path !== undefined) { + const indexPath = repo.path() + "index"; + yield fs.copy(indexPath, path); + const newIndex = yield NodeGit.Index.open(path); + yield newIndex.removeAll(); + for (const e of index.entries()) { + yield newIndex.add(e); + } + index = newIndex; + } yield index.write(); }); diff --git a/node/package-lock.json b/node/package-lock.json index 109da56ea..04386f54b 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -4,42 +4,6 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@sinonjs/commons": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", - "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/formatio": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", - "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" - } - }, - "@sinonjs/samsam": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.2.tgz", - "integrity": "sha512-ILO/rR8LfAb60Y1Yfp9vxfYAASK43NFC2mLzpvLUbCQY/Qu8YwReboseu8aheCEkyElZF2L2T9mHcR2bgdvZyA==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.0.2", - "array-from": "^2.1.1", - "lodash": "^4.17.11" - } - }, - "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", - "dev": true - }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -109,12 +73,6 @@ "sprintf-js": "~1.0.2" } }, - "array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", - "dev": true - }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -1261,12 +1219,6 @@ "verror": "1.10.0" } }, - "just-extend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", - "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", - "dev": true - }, "lcid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", @@ -1309,12 +1261,6 @@ "chalk": "^2.0.1" } }, - "lolex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.1.0.tgz", - "integrity": "sha512-BYxIEXiVq5lGIXeVHnsFzqa1TxN5acnKnPCdlZSpzm8viNEOhiigupA4vTQ9HEFQ6nLTQ9wQOgBknJgzUYQ9Aw==", - "dev": true - }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -1552,19 +1498,6 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, - "nise": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.0.tgz", - "integrity": "sha512-Z3sfYEkLFzFmL8KY6xnSJLRxwQwYBjOXi/24lb62ZnZiGA0JUzGGTI6TBIgfCSMIDl9Jlu8SRmHNACLTemDHww==", - "dev": true, - "requires": { - "@sinonjs/formatio": "^3.1.0", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "lolex": "^4.1.0", - "path-to-regexp": "^1.7.0" - } - }, "node-environment-flags": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", @@ -1912,23 +1845,6 @@ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true }, - "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "dev": true, - "requires": { - "isarray": "0.0.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } - } - }, "pathval": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", @@ -2141,38 +2057,6 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, - "sinon": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz", - "integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.4.0", - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/samsam": "^3.3.1", - "diff": "^3.5.0", - "lolex": "^4.0.1", - "nise": "^1.4.10", - "supports-color": "^5.5.0" - }, - "dependencies": { - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "source-map": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", diff --git a/node/package.json b/node/package.json index 4026b1b86..f7899530a 100644 --- a/node/package.json +++ b/node/package.json @@ -48,7 +48,6 @@ "mocha": "^6.1.4", "mocha-jshint": "", "mocha-parallel-tests": "^2.1.2", - "sinon": "^7.3.2", "temp": "" } } diff --git a/node/test/util/commit.js b/node/test/util/commit.js index be91fc797..19915c648 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -32,7 +32,7 @@ const assert = require("chai").assert; const co = require("co"); -const sinon = require("sinon"); +const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); @@ -44,7 +44,6 @@ const StatusUtil = require("../../lib/util/status_util"); const SubmoduleUtil = require("../../lib/util/submodule_util"); const TestUtil = require("../../lib/util/test_util"); const UserError = require("../../lib/util/user_error"); -const Hook = require("../../lib/util/hook"); function mapCommitResult(commitResult) { @@ -1427,9 +1426,13 @@ x=N:Cfoo\n#x README.md=hello world;*=master;Bmaster=x`, it(caseName, co.wrap(function *() { const amend = co.wrap(function *(repos) { const repo = repos.x; - const newSha = yield Commit.amendRepo(repo, c.message); + const newOid = yield Commit.createAmendCommit(repo, + c.message); + const newCommit = yield NodeGit.Commit.lookup(repo, + newOid); + yield GitUtil.updateHead(repo, newCommit, "amend"); const commitMap = {}; - commitMap[newSha] = "x"; + commitMap[newOid.tostrS()] = "x"; return { commitMap: commitMap }; }); yield RepoASTTestUtil.testMultiRepoManipulator(c.input, @@ -2015,6 +2018,10 @@ x=S:C2-1 q/r/s=Sa:1;Bmaster=2;Oq/r/s H=a`, const result = yield Commit.writeRepoPaths(repo, status, message); + const commit = yield repo.getCommit(result); + yield NodeGit.Reset.reset(repo, commit, + NodeGit.Reset.TYPE.SOFT); + const commitMap = {}; commitMap[result] = "x"; return { commitMap: commitMap, }; @@ -3129,6 +3136,7 @@ x=U:Cfoo\n#x-2 s=Sa:s;Os Cbar\n#s-1 a=b!H=s;Bmaster=x`, c.all || false, c.paths || [], c.interactive || false, + false, editor); if (undefined !== result) { return { @@ -3287,114 +3295,86 @@ x=U:Cbar\n#x-2 s=Sa:s;Bmaster=x;Os Cbar\n#s-1 README.md=foo, a=b`, }); }); - describe("execSubmodulePrecommitHooks", () => { - let sandbox; - const noop = () => Promise.resolve(); - const repoMap = { - a: "submodule-a", - b: "submodule-b" - }; + describe("submoduleHooksAreRun", function () { + const addNewFileHook = `#!/bin/bash +echo -n bar > addedbyhook +git add addedbyhook +`; + const message = "msg"; - beforeEach(() => { - sandbox = sinon.createSandbox(); - }); + it("runs the hook on amend", co.wrap(function*() { + const initial = ` +a=B:Ca-1 README.md=foo;Bmaster=a| +x=U:C3-2 s=Sa:a;Bmaster=3;Os`; + const expected = ` +x=U:Cmsg\n#x-2 s=Sa:s;Bmaster=x;Os Cmsg\n#s-1 README.md=foo,addedbyhook=bar`; - afterEach(() => { - sandbox.restore(); - }); + const f = co.wrap(function*(repos) { + const repo = repos.x; + const cwd = repo.workdir(); + + const hookPath = repo.path() + "modules/s/hooks/pre-commit"; + fs.writeFileSync(hookPath, addNewFileHook); + yield fs.chmod(hookPath, 493); //0755 + const editor = () => assert(false, "no editor"); + + const result = yield Commit.doAmendCommand( + repo, + cwd, + message, + true, // all + false, // interactive + editor); + + return { + commitMap: mapCommitResult(result), + }; - it( - "should run 'pre-commit' hooks of submodules", - co.wrap(function*() { - sandbox - .stub(Commit, "getSubmoduleNamesForPrecommitCheck") - .callsFake(() => ["a", "b"]); - sandbox.stub(Commit, "getCommitStatus").callsFake(noop); - sandbox - .stub(SubmoduleUtil, "getRepo") - .callsFake((repo, submoduleName) => - Promise.resolve(repoMap[submoduleName]) - ); - - const execHookSpy = sandbox - .stub(Hook, "execHook") - .callsFake(() => Promise.resolve(true)); - yield Commit.execSubmodulePrecommitHooks(); - assert.isTrue(execHookSpy.calledWith(repoMap.a)); - assert.isTrue(execHookSpy.calledWith(repoMap.b)); - }) - ); - it("should throw if execHook return false", function() { - sandbox - .stub(Commit, "getSubmoduleNamesForPrecommitCheck") - .callsFake(() => ["a", "b"]); - sandbox.stub(Commit, "getCommitStatus").callsFake(noop); - sandbox - .stub(SubmoduleUtil, "getRepo") - .callsFake((repo, submoduleName) => - Promise.resolve(repoMap[submoduleName]) - ); - - sandbox.stub(Hook, "execHook").callsFake((repo, name) => { - assert.isTrue("pre-commit" === name); - return Promise.resolve(repo !== repoMap.b); // fail on 'b' }); - return Commit.execSubmodulePrecommitHooks().catch(err => { - assert.instanceOf(err, Error); - // hooks are responsible for printing their own message - assert.equal(err.message, ""); + yield RepoASTTestUtil.testMultiRepoManipulator(initial, + expected, + f, + false); + })); + + it("runs the hook on commit-with-paths", co.wrap(function*() { + const initial = "a=B:Ca-1;Bm=a|x=U:Os I q=r"; + const expected = `x=E:Cmsg\n#x-2 s=Sa:s;Os Cmsg +#s-1 addedbyhook=bar,q=r!H=s;Bmaster=x +`; + + const f = co.wrap(function*(repos) { + const repo = repos.x; + + const hookPath = repo.path() + "modules/s/hooks/pre-commit"; + fs.writeFileSync(hookPath, addNewFileHook); + yield fs.chmod(hookPath, 493); //0755 + + const status = yield Commit.getCommitStatus( + repo, + repo.workdir(), { + showMetaChanges: true, + paths: ["s"], + }); + + const result = yield Commit.commitPaths(repo, + status, + message); + const commitMap = {}; + commitMap[result.metaCommit] = "x"; + Object.keys(result.submoduleCommits).forEach(name => { + const sha = result.submoduleCommits[name]; + commitMap[sha] = name; + }); + return { commitMap: commitMap }; }); - }); - describe("interactive mode", () => { - const execSubmodulePrecommitHooksInteractive = () => - Commit.execSubmodulePrecommitHooks( - null, - null, - null, - null, - true - ); - it( - "should warn if there are hooks to run but skipped", - co.wrap(function*() { - sandbox - .stub(Commit, "getSubmoduleNamesForPrecommitCheck") - .callsFake(() => ["a", "b"]); - sandbox.stub(Commit, "getCommitStatus").callsFake(noop); - sandbox - .stub(SubmoduleUtil, "getRepo") - .callsFake((repo, submoduleName) => - Promise.resolve(repoMap[submoduleName]) - ); - - const consoleWarnSpy = sandbox - .stub(console, "warn") - .callsFake(noop); - yield execSubmodulePrecommitHooksInteractive(); - assert.isTrue( - consoleWarnSpy.calledWith( - "Warning. pre-commit hooks skipped when using " + - "option [-i]" - ) - ); - }) - ); - it( - "should not warn if there is no hooks", - co.wrap(function*() { - sandbox - .stub(Commit, "getSubmoduleNamesForPrecommitCheck") - .callsFake(() => []); - sandbox.stub(Commit, "getCommitStatus").callsFake(noop); - const consoleWarnSpy = sandbox - .stub(console, "warn") - .callsFake(noop); - yield execSubmodulePrecommitHooksInteractive(); - assert.isFalse(consoleWarnSpy.called); - }) - ); - }); + yield RepoASTTestUtil.testMultiRepoManipulator(initial, + expected, + f, + false); + })); + }); }); From fc668306fa69236828229996bb1bce3c7c8f2a1f Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 27 Jul 2020 16:23:56 -0400 Subject: [PATCH 295/402] re-plumb no-verify thru --- node/lib/cmd/commit.js | 3 ++- node/lib/util/commit.js | 52 ++++++++++++++++++++++++++++------------ node/test/util/commit.js | 12 ++++++---- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/node/lib/cmd/commit.js b/node/lib/cmd/commit.js index 8072988d9..bfbf61d16 100644 --- a/node/lib/cmd/commit.js +++ b/node/lib/cmd/commit.js @@ -151,7 +151,8 @@ const doAmend = co.wrap(function *(args) { args.message, args.all, args.interactive, - args.no_edit ? null : GitUtil.editMessage); + args.no_edit ? null : GitUtil.editMessage, + args.no_verify); yield Hook.execHook(repo, "post-commit"); }); diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index d30c380d4..2e41b23e9 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -231,13 +231,17 @@ const runHooks = co.wrap(function *(repo, index) { * @param {NodeGit.Repository} repo * @param {RepoStatus} repoStatus * @param {Boolean} doAll + * @param {Boolean} noVerify * @return {NodeGit.Oid|null} */ const prepareIndexAndRunHooks = co.wrap(function *(repo, changes, - doAll) { + doAll, + noVerify) { assert.instanceOf(repo, NodeGit.Repository); assert.isObject(changes); + assert.isBoolean(doAll); + assert.isBoolean(noVerify); const index = yield repo.index(); @@ -250,7 +254,9 @@ const prepareIndexAndRunHooks = co.wrap(function *(repo, yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); } - yield runHooks(repo, index); + if (!noVerify) { + yield runHooks(repo, index); + } }); const editorMessagePrefix = `\ @@ -490,7 +496,8 @@ exports.commit = co.wrap(function *(metaRepo, all, metaStatus, message, - subMessages) { + subMessages, + noVerify) { assert.instanceOf(metaRepo, NodeGit.Repository); assert.isBoolean(all); assert.instanceOf(metaStatus, RepoStatus); @@ -504,6 +511,7 @@ exports.commit = co.wrap(function *(metaRepo, } assert(null !== message || undefined !== subMessages, "if no meta message, sub messages must be specified"); + assert.isBoolean(noVerify); const signature = yield ConfigUtil.defaultSignature(metaRepo); const submodules = metaStatus.submodules; @@ -534,7 +542,8 @@ exports.commit = co.wrap(function *(metaRepo, yield prepareIndexAndRunHooks(subRepo, repoStatus.staged, - all); + all, + noVerify); const headCommit = yield subRepo.getHeadCommit(); const parents = []; @@ -635,7 +644,7 @@ const isExecutable = co.wrap(function *(repo, filename) { * @param {String} message * @return {String} */ -exports.writeRepoPaths = co.wrap(function *(repo, status, message) { +exports.writeRepoPaths = co.wrap(function *(repo, status, message, noVerify) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(status, RepoStatus); assert.isString(message); @@ -691,7 +700,9 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message) { } } - yield runHooks(repo, index); + if (!noVerify) { + yield runHooks(repo, index); + } // Use 'TreeUtil' to create a new tree having the required paths. @@ -752,10 +763,11 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message) { * @return {String} return.metaCommit * @return {Object} return.submoduleCommits map from sub name to commit id */ -exports.commitPaths = co.wrap(function *(repo, status, message) { +exports.commitPaths = co.wrap(function *(repo, status, message, noVerify) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(status, RepoStatus); assert.isString(message); + assert.isBoolean(noVerify); const subCommits = {}; // map from name to sha @@ -782,7 +794,8 @@ exports.commitPaths = co.wrap(function *(repo, status, message) { const wdStatus = workdir.status; const subRepo = yield SubmoduleUtil.getRepo(repo, subName); - const oid = yield exports.writeRepoPaths(subRepo, wdStatus, message); + const oid = yield exports.writeRepoPaths(subRepo, wdStatus, message, + noVerify); const sha = oid.tostrS(); subCommits[subName] = sha; const oldIndex = sub.index; @@ -1204,6 +1217,7 @@ exports.createAmendCommit = co.wrap(function *(repo, message) { * @param {Boolean} all * @param {String|null} message * @param {Object|null} subMessages + * @param {Boolean} noVerify * @return {Object} * @return {String} return.metaCommit sha of new commit on meta-repo * @return {Object} return.submoduleCommits from sub name to sha @@ -1213,7 +1227,8 @@ exports.amendMetaRepo = co.wrap(function *(repo, subsToAmend, all, message, - subMessages) { + subMessages, + noVerify) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(status, RepoStatus); assert.isArray(subsToAmend); @@ -1316,7 +1331,9 @@ exports.amendMetaRepo = co.wrap(function *(repo, yield exports.stageChange(subIndex, path, change); } } - yield runHooks(subRepo, subIndex); + if (!noVerify) { + yield runHooks(subRepo, subIndex); + } } subCommits[subName] = yield exports.createAmendCommit(subRepo, subMessage); @@ -1337,7 +1354,9 @@ exports.amendMetaRepo = co.wrap(function *(repo, } } - yield runHooks(subRepo, subIndex); + if (!noVerify) { + yield runHooks(subRepo, subIndex); + } const tree = yield subIndex.writeTree(); const commit = yield subRepo.createCommit( null, @@ -2198,14 +2217,15 @@ exports.doCommitCommand = co.wrap(function *(repo, return yield exports.commitPaths(repo, repoStatus, message, - subMessages); + noVerify); } else { return yield exports.commit(repo, all, repoStatus, message, - subMessages); + subMessages, + noVerify); } }); @@ -2237,7 +2257,8 @@ exports.doAmendCommand = co.wrap(function *(repo, message, all, interactive, - editMessage) { + editMessage, + noVerify) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(cwd); if (null !== message) { @@ -2338,6 +2359,7 @@ it empty. You can remove the commit entirely with "git meta reset HEAD^".`); Object.keys(subsToAmend), all, message, - subMessages); + subMessages, + noVerify); }); diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 19915c648..992ce594c 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -74,7 +74,8 @@ const committer = co.wrap(function *(doAll, message, repos, subMessages) { showMetaChanges: true, all: doAll, }); - const result = yield Commit.commit(x, doAll, status, message, subMessages); + const result = yield Commit.commit(x, doAll, status, message, subMessages, + false); return { commitMap: mapCommitResult(result), }; @@ -1617,7 +1618,8 @@ x=U:C3-2 s=Sa:b;Cx-3 s=Sa:s;Bmaster=x;Os Cs-b b=1`, subsToAmend, all, message, - subMessages); + subMessages, + false); return { commitMap: mapCommitResult(result) }; }); yield RepoASTTestUtil.testMultiRepoManipulator(c.input, @@ -2076,7 +2078,8 @@ x=S:C2-1 q/r/s=Sa:1;Bmaster=2;Oq/r/s H=a`, const message = c.message || "message"; const result = yield Commit.commitPaths(repo, status, - message); + message, + false); const commitMap = {}; commitMap[result.metaCommit] = "x"; Object.keys(result.submoduleCommits).forEach(name => { @@ -3360,7 +3363,8 @@ x=U:Cmsg\n#x-2 s=Sa:s;Bmaster=x;Os Cmsg\n#s-1 README.md=foo,addedbyhook=bar`; const result = yield Commit.commitPaths(repo, status, - message); + message, + false); const commitMap = {}; commitMap[result.metaCommit] = "x"; Object.keys(result.submoduleCommits).forEach(name => { From a375af1218b3f393ec116410d5ada74fe9182c84 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 27 Jul 2020 16:24:29 -0400 Subject: [PATCH 296/402] jsdoc --- node/lib/util/commit.js | 1 - 1 file changed, 1 deletion(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 2e41b23e9..4166c2b27 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -232,7 +232,6 @@ const runHooks = co.wrap(function *(repo, index) { * @param {RepoStatus} repoStatus * @param {Boolean} doAll * @param {Boolean} noVerify - * @return {NodeGit.Oid|null} */ const prepareIndexAndRunHooks = co.wrap(function *(repo, changes, From de7b4d910dfa2decf77a713e73a5c34a4a744354 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Thu, 30 Jul 2020 13:11:02 -0400 Subject: [PATCH 297/402] Allow git-meta concatenating multiple --message arguments like vanilla git does --- node/lib/cmd/commit-shadow.js | 4 +++- node/lib/cmd/commit.js | 8 +++++--- node/lib/cmd/merge.js | 3 ++- node/lib/cmd/merge_bare.js | 3 ++- node/lib/cmd/stash.js | 5 +++-- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/node/lib/cmd/commit-shadow.js b/node/lib/cmd/commit-shadow.js index 7e53a7825..9be8e8bc2 100644 --- a/node/lib/cmd/commit-shadow.js +++ b/node/lib/cmd/commit-shadow.js @@ -74,6 +74,7 @@ exports.configureParser = function (parser) { }); parser.addArgument(["-m", "--message"], { type: "string", + action: "append", required: true, help: "commit message for shadow commits", }); @@ -115,8 +116,9 @@ exports.executeableSubcommand = co.wrap(function *(args) { const incrementTimestamp = args.increment_timestamp || args.epoch_timestamp; const includedSubrepos = args.include_subrepos || []; + const message = args.message ? args.message.join("\n\n") : null; const result = yield StashUtil.makeShadowCommit(repo, - args.message, + message, incrementTimestamp, false, args.include_untracked, diff --git a/node/lib/cmd/commit.js b/node/lib/cmd/commit.js index bfbf61d16..30a5cd217 100644 --- a/node/lib/cmd/commit.js +++ b/node/lib/cmd/commit.js @@ -64,7 +64,7 @@ exports.configureParser = function (parser) { }); parser.addArgument(["-m", "--message"], { type: "string", - defaultValue: null, + action: "append", required: false, help: "commit message; if not specified will prompt" }); @@ -119,10 +119,11 @@ const doCommit = co.wrap(function *(args) { const repo = yield GitUtil.getCurrentRepo(); const cwd = process.cwd(); + const message = args.message ? args.message.join("\n\n") : null; yield Commit.doCommitCommand(repo, cwd, - args.message, + message, args.all, args.file, args.interactive, @@ -138,6 +139,7 @@ const doAmend = co.wrap(function *(args) { const UserError = require("../util/user_error"); const usingPaths = 0 !== args.file.length; + const message = args.message ? args.message.join("\n\n") : null; if (usingPaths) { throw new UserError("Paths not supported with amend yet."); @@ -148,7 +150,7 @@ const doAmend = co.wrap(function *(args) { yield Commit.doAmendCommand(repo, cwd, - args.message, + message, args.all, args.interactive, args.no_edit ? null : GitUtil.editMessage, diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js index 12bd216b9..b55407f72 100644 --- a/node/lib/cmd/merge.js +++ b/node/lib/cmd/merge.js @@ -59,6 +59,7 @@ exports.configureParser = function (parser) { parser.addArgument(["-m", "--message"], { type: "string", help: "commit message", + action: "append", required: false, }); parser.addArgument(["-F", "--message-file"], { @@ -196,7 +197,7 @@ Merge of '${commitName}' doNotRecurse.push(noSlashPrefix); } - let message = args.message; + let message = args.message ? args.message.join("\n\n") : null; if (args.message_file) { message = yield fs.readFile(args.message_file, "utf8"); } diff --git a/node/lib/cmd/merge_bare.js b/node/lib/cmd/merge_bare.js index c619f3727..69c652a0a 100755 --- a/node/lib/cmd/merge_bare.js +++ b/node/lib/cmd/merge_bare.js @@ -56,6 +56,7 @@ that is not pointed by any refs. It will also abort if there are merge conflicts exports.configureParser = function (parser) { parser.addArgument(["-m", "--message"], { type: "string", + action: "append", help: "commit message", required: false, }); @@ -143,7 +144,7 @@ Could not resolve ${colors.red(theirCommitName)} to a commit.`); doNotRecurse.push(noSlashPrefix); } - let message = args.message; + let message = args.message ? args.message.join("\n\n") : null; if (args.message_file) { message = yield fs.readFile(args.message_file, "utf8"); } diff --git a/node/lib/cmd/stash.js b/node/lib/cmd/stash.js index c1f3ec706..b07823eaf 100644 --- a/node/lib/cmd/stash.js +++ b/node/lib/cmd/stash.js @@ -56,7 +56,7 @@ exports.configureParser = function (parser) { parser.addArgument(["-m", "--message"], { type: "string", - defaultValue: null, + action: "append", required: false, help: "description; if not provided one will be generated", }); @@ -142,10 +142,11 @@ const doSave = co.wrap(function *(args) { console.warn("Nothing to stash."); return; // RETURN } + const message = args.message ? args.message.join("\n\n") : null; yield StashUtil.save(repo, status, includeUntracked || false, - args.message); + message); console.log("Saved working directory and index state."); }); From ad0828ecf595792a41751a90153b971f3be5768f Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 3 Aug 2020 13:21:03 -0400 Subject: [PATCH 298/402] precommit in meta too --- node/lib/util/commit.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 4166c2b27..bae5df62f 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -589,8 +589,10 @@ exports.commit = co.wrap(function *(metaRepo, return result; // RETURN } - // TODO temp index and meta pre-commit hook yield stageOpenSubmodules(metaRepo, index, submodules); + if (!noVerify) { + yield runHooks(metaRepo, index); + } const tree = yield index.writeTree(); const headCommit = yield metaRepo.getHeadCommit(); From 55299f221337914729c1b1323b25b4290b36f744 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 4 Aug 2020 12:09:34 -0400 Subject: [PATCH 299/402] Run hooks after opening a submodule everywhere, not just in git meta open. This has the side-effect that now we run the hook once per submodule rather than once, but that will be good once the repo has enough submodules that we start hitting arg max limits. --- node/lib/cmd/open.js | 6 ---- node/lib/util/open.js | 69 +++++++++++++++++++++++++------------------ 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 5c47e9123..f78b8a45a 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -80,7 +80,6 @@ exports.executeableSubcommand = co.wrap(function *(args) { const DoWorkQueue = require("../util/do_work_queue"); const GitUtil = require("../util/git_util"); - const Hook = require("../util/hook"); const Open = require("../util/open"); const SparseCheckoutUtil = require("../util/sparse_checkout_util"); const SubmoduleConfigUtil = require("../util/submodule_config_util"); @@ -157,11 +156,6 @@ Opening ${colors.blue(name)} on ${colors.green(shas[idx])}.`); yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); - if (subsOpenSuccessfully.length) { - // Run post-open-submodule hook with successfully-opened submodules - yield Hook.execHook(repo, "post-open-submodule", subsOpenSuccessfully); - } - if (failed) { process.exit(1); } diff --git a/node/lib/util/open.js b/node/lib/util/open.js index 3c0fd2c79..51e472275 100644 --- a/node/lib/util/open.js +++ b/node/lib/util/open.js @@ -38,6 +38,7 @@ const colors = require("colors"); const NodeGit = require("nodegit"); const GitUtil = require("./git_util"); +const Hook = require("../util/hook"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const SubmoduleUtil = require("./submodule_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -54,6 +55,36 @@ const SUB_OPEN_OPTION = { }; exports.SUB_OPEN_OPTION = SUB_OPEN_OPTION; +/** + * @class {Opener} + * class for opening and retrieving submodule repositories on-demand + */ +class Opener { + /** + * Create a new object for retreiving submodule repositories on-demand in + * the specified `repo`. + * + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} commit + */ + constructor(repo, commit) { + assert.instanceOf(repo, NodeGit.Repository); + if (null !== commit) { + assert.instanceOf(commit, NodeGit.Commit); + } + this.d_repo = repo; + this.d_commit = commit; + this.d_initialized = false; + } + + /** + * @property {NodeGit.Repository} the repo associated with this object + */ + get repo() { + return this.d_repo; + } +} + /** * Open the submodule having the specified `submoduleName` in the meta-repo * associated with the specified `fetcher`; fetch the specified `submoduleSha` @@ -86,10 +117,14 @@ exports.openOnCommit = co.wrap(function *(fetcher, if (null !== templatePath) { assert.isString(templatePath); } + const metaRepoUrl = yield fetcher.getMetaOriginUrl(); const metaRepo = fetcher.repo; const submoduleUrl = yield fetcher.getSubmoduleUrl(submoduleName); + + const wasOpen = yield new Opener(metaRepo, null).isOpen(submoduleName); + // Set up the submodule. const submoduleRepo = yield SubmoduleConfigUtil.initSubmoduleAndRepo( @@ -131,38 +166,14 @@ exports.openOnCommit = co.wrap(function *(fetcher, yield SparseCheckoutUtil.addToSparseCheckoutFile(metaRepo, submoduleName); } - return submoduleRepo; -}); -/** - * @class {Opener} - * class for opening and retrieving submodule repositories on-demand - */ -class Opener { - /** - * Create a new object for retreiving submodule repositories on-demand in - * the specified `repo`. - * - * @param {NodeGit.Repository} repo - * @param {NodeGit.Commit} commit - */ - constructor(repo, commit) { - assert.instanceOf(repo, NodeGit.Repository); - if (null !== commit) { - assert.instanceOf(commit, NodeGit.Commit); - } - this.d_repo = repo; - this.d_commit = commit; - this.d_initialized = false; + if (!wasOpen) { + // Run post-open-submodule hook with successfully-opened submodules + yield Hook.execHook(metaRepo, "post-open-submodule", [submoduleName]); } - /** - * @property {NodeGit.Repository} the repo associated with this object - */ - get repo() { - return this.d_repo; - } -} + return submoduleRepo; +}); Opener.prototype._initialize = co.wrap(function *() { if (null === this.d_commit) { From b512ddbb0c94f981db2851b67cbfe37a4af74e01 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 6 Aug 2020 14:14:42 -0400 Subject: [PATCH 300/402] git meta open -c --- node/lib/cmd/open.js | 67 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index f78b8a45a..7cc445435 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -34,7 +34,11 @@ * This module is the entry point for the `open` command. */ -const co = require("co"); +const assert = require("chai").assert; +const co = require("co"); +const NodeGit = require("nodegit") +const GitUtil = require("../util/git_util") +const UserError = require("../util/user_error"); /** * help text for the `open` command @@ -65,15 +69,70 @@ exports.configureParser = function (parser) { parser.addArgument(["path"], { type: "string", help: "open all submodules at or in 'path'", - nargs: "+", + nargs: "*", + }); + + // TODO: allow revlist specs instead of just single commits in -c, + // which is why we are not also giving it a long argument name of + // --committish, since that would be weird with a revlist. + parser.addArgument(["-c"], { + action: "store", + type: "string", + help: "open all submodules modified in a commit", }); }; +const parseArgs = co.wrap(function *(repo, args) { + assert.instanceOf(repo, NodeGit.Repository); + + args.path = Array.from(new Set(args.path)); + if (args.path.length > 0) { + if (args.c) { + // Of course, args.path and args.c should be mutually exclusive, + // but unfortunately, node's argparse doesn't support this. + throw new UserError("-c should take a single argument"); + } + return Array.from(new Set(args.path)); + } + const commitish = args.c; + if (commitish == null) { + throw new UserError("Please supply a submodule to open, or -c $commitish"); + } + const annotated = yield GitUtil.resolveCommitish(repo, commitish); + if (annotated === null) { + throw new UserError("Cannot resolve " + commitish + " to a commit"); + } + const commit = yield NodeGit.Commit.lookup(repo, annotated.id()); + const tree = yield commit.getTree(); + const parent = yield commit.parent(0); + let parentTree = null; + if (parent) { + let parentCommit = yield NodeGit.Commit.lookup(repo, parent.id()); + parentTree = yield parentCommit.getTree(); + } + const diff = yield NodeGit.Diff.treeToTree(repo, parentTree, tree); + + const out = new Set(); + for (let i = 0; i < diff.numDeltas(); i ++) { + const delta = diff.getDelta(i); + const newFile = delta.newFile(); + if (newFile.id().iszero()) { + continue; + } + if (newFile.mode() !== NodeGit.TreeEntry.FILEMODE.COMMIT) { + continue; + } + out.add(newFile.path()); + } + return Array.from(out); +}); + /** * Execute the `open` command according to the specified `args`. * * @param {Object} args * @param {String} args.path + * @param {String} args.c */ exports.executeableSubcommand = co.wrap(function *(args) { const colors = require("colors"); @@ -92,11 +151,11 @@ exports.executeableSubcommand = co.wrap(function *(args) { const cwd = process.cwd(); const subs = yield SubmoduleUtil.getSubmoduleNames(repo); - args.path = Array.from(new Set(args.path)); + const paths = yield parseArgs(repo, args); let subsToOpen = yield SubmoduleUtil.resolveSubmoduleNames(workdir, cwd, subs, - args.path); + paths); subsToOpen = Array.from(new Set(subsToOpen)); const index = yield repo.index(); const shas = yield SubmoduleUtil.getCurrentSubmoduleShas(index, From 79ac5ab8415d5d720b75b136ce6b332ca31adda3 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Tue, 18 Aug 2020 23:42:32 -0400 Subject: [PATCH 301/402] migrate to use yarn --- .travis.yml | 6 +- node/.gitignore | 1 + node/lib/cmd/open.js | 9 +- node/package-lock.json | 2542 ---------------------------------------- node/yarn.lock | 2183 ++++++++++++++++++++++++++++++++++ 5 files changed, 2192 insertions(+), 2549 deletions(-) delete mode 100644 node/package-lock.json create mode 100644 node/yarn.lock diff --git a/.travis.yml b/.travis.yml index 4a2ae80b0..45dd5860d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "6.9.1" + - 8 env: - CC=gcc-5 @@ -20,10 +20,10 @@ before_install: - cd node install: - - npm install + - yarn install script: - - npm test + - yarn test notifications: email: false diff --git a/node/.gitignore b/node/.gitignore index 25c74d353..ee7111194 100644 --- a/node/.gitignore +++ b/node/.gitignore @@ -1,2 +1,3 @@ /node_modules/ /coverage +/yarn/ diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 7cc445435..4109e23fb 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -36,8 +36,8 @@ const assert = require("chai").assert; const co = require("co"); -const NodeGit = require("nodegit") -const GitUtil = require("../util/git_util") +const NodeGit = require("nodegit"); +const GitUtil = require("../util/git_util"); const UserError = require("../util/user_error"); /** @@ -95,8 +95,9 @@ const parseArgs = co.wrap(function *(repo, args) { return Array.from(new Set(args.path)); } const commitish = args.c; - if (commitish == null) { - throw new UserError("Please supply a submodule to open, or -c $commitish"); + if (commitish === null) { + throw new UserError( + "Please supply a submodule to open, or -c $commitish"); } const annotated = yield GitUtil.resolveCommitish(repo, commitish); if (annotated === null) { diff --git a/node/package-lock.json b/node/package-lock.json deleted file mode 100644 index 04386f54b..000000000 --- a/node/package-lock.json +++ /dev/null @@ -1,2542 +0,0 @@ -{ - "name": "git-meta", - "version": "0.4.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "optional": true - }, - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "binary-search": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz", - "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==" - }, - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "requires": { - "inherits": "~2.0.0" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" - }, - "child-process-promise": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz", - "integrity": "sha1-RzChHvYQ+tRQuPIjx50x172tgHQ=", - "requires": { - "cross-spawn": "^4.0.2", - "node-version": "^1.0.0", - "promise-polyfill": "^6.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" - }, - "circular-json": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.9.tgz", - "integrity": "sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ==", - "dev": true - }, - "cli": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", - "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", - "requires": { - "exit": "0.1.2", - "glob": "^7.1.1" - } - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "component-props": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/component-props/-/component-props-1.1.1.tgz", - "integrity": "sha1-+bffm5kntubZfJvScqqGdnDzSUQ=" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "requires": { - "date-now": "^0.1.4" - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cross-spawn": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", - "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "deepcopy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/deepcopy/-/deepcopy-2.0.0.tgz", - "integrity": "sha512-d5ZK7pJw7F3k6M5vqDjGiiUS9xliIyWkdzBjnPhnSeRGjkYOGZMCFkdKVwV/WiHOe0NwzB8q+iDo7afvSf0arA==", - "requires": { - "type-detect": "^4.0.8" - } - }, - "deeper": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/deeper/-/deeper-2.1.0.tgz", - "integrity": "sha1-vFZOX3MXT98gHgiwADDooU2nQ2g=" - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - }, - "dependencies": { - "domelementtype": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", - "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" - }, - "entities": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==" - } - } - }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" - }, - "domhandler": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", - "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" - }, - "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", - "requires": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.2.0" - } - }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" - }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - } - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "flat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", - "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", - "dev": true, - "requires": { - "is-buffer": "~2.0.3" - } - }, - "foreachasync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", - "integrity": "sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs-extra": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.1.2.tgz", - "integrity": "sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU=", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0" - } - }, - "fs-minipass": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz", - "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", - "requires": { - "minipass": "^2.2.1" - } - }, - "fs-promise": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/fs-promise/-/fs-promise-2.0.3.tgz", - "integrity": "sha1-9k5PhUvPaJqovdy6JokW2z20aFQ=", - "requires": { - "any-promise": "^1.3.0", - "fs-extra": "^2.0.0", - "mz": "^2.6.0", - "thenify-all": "^1.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - }, - "dependencies": { - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" - }, - "group-by": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/group-by/-/group-by-0.0.1.tgz", - "integrity": "sha1-hXYgV19nFHhvjYa7Gf0T4YjdaKQ=", - "requires": { - "to-function": "*" - } - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "handlebars": { - "version": "4.7.6", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", - "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "htmlparser2": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", - "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", - "requires": { - "domelementtype": "1", - "domhandler": "2.3", - "domutils": "1.5", - "entities": "1.0", - "readable-stream": "1.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", - "dev": true - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, - "requires": { - "has-symbols": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", - "requires": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=" - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "dependencies": { - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - } - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "jshint": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.11.1.tgz", - "integrity": "sha512-WXWePB8ssAH3DlD05IoqolsY6arhbll/1+i2JkRPpihQAuiNaR/gSt8VKIcxpV5m6XChP0hCwESQUqpuQMA8Tg==", - "requires": { - "cli": "~1.0.0", - "console-browserify": "1.1.x", - "exit": "0.1.x", - "htmlparser2": "3.8.x", - "lodash": "~4.17.11", - "minimatch": "~3.0.2", - "shelljs": "0.3.x", - "strip-json-comments": "1.0.x" - }, - "dependencies": { - "shelljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", - "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=" - }, - "strip-json-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=" - } - } - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", - "requires": { - "minimist": "^1.2.0" - } - }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - } - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "requires": { - "mime-db": "1.40.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "minipass": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" - } - } - }, - "minizlib": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } - } - }, - "mocha": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", - "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", - "dev": true, - "requires": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "2.2.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "ms": "2.1.1", - "node-environment-flags": "1.0.5", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.2.2", - "yargs-parser": "13.0.0", - "yargs-unparser": "1.5.0" - }, - "dependencies": { - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "mocha-jshint": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/mocha-jshint/-/mocha-jshint-2.3.1.tgz", - "integrity": "sha1-MD8n5TOThVnSDyakEj1by1ZFpUY=", - "requires": { - "jshint": "^2.8.0", - "minimatch": "^3.0.0", - "shelljs": "^0.4.0", - "uniq": "^1.0.1" - } - }, - "mocha-parallel-tests": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mocha-parallel-tests/-/mocha-parallel-tests-2.1.2.tgz", - "integrity": "sha512-FatFg3MHLio9ir1oP6J0HNEo6R5344JC1y+We90iALdiT9F9xNPN0KbGXxRNlGlSl0GodfSESKbRzBvT9ctgIw==", - "dev": true, - "requires": { - "circular-json": "^0.5.9", - "debug": "^4.1.1", - "uuid": "^3.3.2", - "yargs": "^13.2.2" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, - "mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "requires": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" - }, - "needle": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", - "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node-environment-flags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", - "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - }, - "dependencies": { - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - } - } - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - }, - "dependencies": { - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - }, - "tar": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.9.tgz", - "integrity": "sha512-xisFa7Q2i3HOgfn+nmnWLGHD6Tm23hxjkx6wwGmgxkJFr6wxwXnJOdJYcZjL453PSdF0+bemO03+flAzkIdLBQ==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.5", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" - } - } - }, - "node-version": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/node-version/-/node-version-1.2.0.tgz", - "integrity": "sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ==" - }, - "nodegit": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/nodegit/-/nodegit-0.23.1.tgz", - "integrity": "sha512-k1bCg260yZlmfLGYPrinqAQNbQ6PnBJhKq6wWfe94yIO3WnOOyoSuweAkAGPR6FhrGq2LkCKHdJaitwhKCEGyA==", - "requires": { - "fs-extra": "^7.0.0", - "json5": "^2.1.0", - "lodash": "^4.17.11", - "nan": "^2.11.1", - "node-gyp": "^3.8.0", - "node-pre-gyp": "^0.11.0", - "promisify-node": "~0.3.0", - "ramda": "^0.25.0", - "request-promise-native": "^1.0.5", - "tar-fs": "^1.16.3" - }, - "dependencies": { - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" - } - } - } - }, - "nodegit-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/nodegit-promise/-/nodegit-promise-4.0.0.tgz", - "integrity": "sha1-VyKxhPLfcycWEGSnkdLoQskWezQ=", - "requires": { - "asap": "~2.0.3" - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "requires": { - "abbrev": "1" - } - }, - "npm-bundled": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", - "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" - }, - "npm-packlist": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", - "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "promise-polyfill": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", - "integrity": "sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=" - }, - "promisify-node": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/promisify-node/-/promisify-node-0.3.0.tgz", - "integrity": "sha1-tLVaz5D6p9K4uQyjlomQhsAwYM8=", - "requires": { - "nodegit-promise": "~4.0.0" - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "psl": { - "version": "1.1.32", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", - "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" - }, - "pump": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", - "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "ramda": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.25.0.tgz", - "integrity": "sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ==" - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", - "requires": { - "lodash": "^4.17.11" - } - }, - "request-promise-native": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", - "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", - "requires": { - "request-promise-core": "1.1.2", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shelljs": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.4.0.tgz", - "integrity": "sha1-GZ/p4t43nv0D00X/FAYlJeSzHsI=" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - }, - "split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "requires": { - "through": "2" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "requires": { - "has-flag": "^1.0.0" - } - }, - "tar": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", - "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" - } - }, - "tar-fs": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", - "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", - "requires": { - "chownr": "^1.0.1", - "mkdirp": "^0.5.1", - "pump": "^1.0.0", - "tar-stream": "^1.1.2" - } - }, - "tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "requires": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - } - }, - "temp": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.1.tgz", - "integrity": "sha512-WMuOgiua1xb5R56lE0eH6ivpVmg/lq2OHm4+LtT/xtEtPQ+sz6N3bBM6WZ5FvO1lO4IKIOb43qnhoc4qxP5OeA==", - "requires": { - "rimraf": "~2.6.2" - }, - "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "requires": { - "any-promise": "^1.0.0" - } - }, - "thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", - "requires": { - "thenify": ">= 3.1.0 < 4" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" - }, - "to-function": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/to-function/-/to-function-2.0.6.tgz", - "integrity": "sha1-fVbNnDuS+o29eyLoPVGSTedA68U=", - "requires": { - "component-props": "*" - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" - }, - "uglify-js": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.0.tgz", - "integrity": "sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA==", - "optional": true - }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "requires": { - "punycode": "^2.1.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "walk": { - "version": "2.3.14", - "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.14.tgz", - "integrity": "sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg==", - "requires": { - "foreachasync": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yargs": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", - "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "yargs-unparser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", - "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", - "dev": true, - "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.11", - "yargs": "^12.0.5" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - } - } -} diff --git a/node/yarn.lock b/node/yarn.lock new file mode 100644 index 000000000..63cbd87d1 --- /dev/null +++ b/node/yarn.lock @@ -0,0 +1,2183 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +abbrev@1.0.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" + integrity sha1-kbR5JYinc4wl813W9jdSovh3YTU= + +ajv@^6.5.5: + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= + +ansi-colors@3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" + integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +any-promise@^1.0.0, any-promise@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@, argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +async@1.x: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +binary-search@: + version "1.3.6" + resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c" + integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA== + +bl@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" + integrity sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA== + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= + dependencies: + inherits "~2.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chai@: + version "4.2.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" + integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.2" + deep-eql "^3.0.1" + get-func-name "^2.0.0" + pathval "^1.1.0" + type-detect "^4.0.5" + +chalk@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +check-error@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= + +child-process-promise@: + version "2.2.1" + resolved "https://registry.yarnpkg.com/child-process-promise/-/child-process-promise-2.2.1.tgz#4730a11ef610fad450b8f223c79d31d7bdad8074" + integrity sha1-RzChHvYQ+tRQuPIjx50x172tgHQ= + dependencies: + cross-spawn "^4.0.2" + node-version "^1.0.0" + promise-polyfill "^6.0.1" + +chownr@^1.0.1, chownr@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" + integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== + +circular-json@^0.5.9: + version "0.5.9" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.9.tgz#932763ae88f4f7dead7a0d09c8a51a4743a53b1d" + integrity sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ== + +cli@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cli/-/cli-1.0.1.tgz#22817534f24bfa4950c34d532d48ecbc621b8c14" + integrity sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ= + dependencies: + exit "0.1.2" + glob "^7.1.1" + +cliui@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +co@: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +colors@: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +component-props@*: + version "1.1.1" + resolved "https://registry.yarnpkg.com/component-props/-/component-props-1.1.1.tgz#f9b7df9b9927b6e6d97c9bd272aa867670f34944" + integrity sha1-+bffm5kntubZfJvScqqGdnDzSUQ= + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +console-browserify@1.1.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA= + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cross-spawn@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" + integrity sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE= + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= + +debug@3.2.6, debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +deep-eql@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== + dependencies: + type-detect "^4.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +deepcopy@: + version "2.0.0" + resolved "https://registry.yarnpkg.com/deepcopy/-/deepcopy-2.0.0.tgz#2acb9b7645f9f54d815eee991455e790e72e2252" + integrity sha512-d5ZK7pJw7F3k6M5vqDjGiiUS9xliIyWkdzBjnPhnSeRGjkYOGZMCFkdKVwV/WiHOe0NwzB8q+iDo7afvSf0arA== + dependencies: + type-detect "^4.0.8" + +deeper@: + version "2.1.0" + resolved "https://registry.yarnpkg.com/deeper/-/deeper-2.1.0.tgz#bc564e5f73174fdf201e08b00030e8a14da74368" + integrity sha1-vFZOX3MXT98gHgiwADDooU2nQ2g= + +define-properties@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + +diff@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +domelementtype@1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" + integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== + +domhandler@2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738" + integrity sha1-LeWaCCLVAn+r/28DLCsloqir5zg= + dependencies: + domelementtype "1" + +domutils@1.5: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= + dependencies: + dom-serializer "0" + domelementtype "1" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + dependencies: + once "^1.4.0" + +entities@1.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26" + integrity sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY= + +entities@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" + integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== + +es-abstract@^1.5.1: + version "1.13.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" + integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== + dependencies: + es-to-primitive "^1.2.0" + function-bind "^1.1.1" + has "^1.0.3" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-keys "^1.0.12" + +es-to-primitive@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" + integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escodegen@1.8.x: + version "1.8.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" + integrity sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg= + dependencies: + esprima "^2.7.1" + estraverse "^1.9.1" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.2.0" + +esprima@2.7.x, esprima@^2.7.1: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE= + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +estraverse@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" + integrity sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q= + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exit@0.1.2, exit@0.1.x: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@1.3.0, extsprintf@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +find-up@3.0.0, find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +flat@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" + integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== + dependencies: + is-buffer "~2.0.3" + +foreachasync@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" + integrity sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY= + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-extra@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-2.1.2.tgz#046c70163cef9aad46b0e4a7fa467fb22d71de35" + integrity sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU= + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + +fs-extra@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-minipass@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" + integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ== + dependencies: + minipass "^2.2.1" + +fs-promise@: + version "2.0.3" + resolved "https://registry.yarnpkg.com/fs-promise/-/fs-promise-2.0.3.tgz#f64e4f854bcf689aa8bddcba268916db3db46854" + integrity sha1-9k5PhUvPaJqovdy6JokW2z20aFQ= + dependencies: + any-promise "^1.3.0" + fs-extra "^2.0.0" + mz "^2.6.0" + thenify-all "^1.6.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fstream@^1.0.0, fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +get-caller-file@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^5.0.15: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E= + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.3, glob@^7.1.1, glob@^7.1.3: + version "7.1.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" + integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.1.15" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== + +group-by@: + version "0.0.1" + resolved "https://registry.yarnpkg.com/group-by/-/group-by-0.0.1.tgz#857620575f6714786f8d86bb19fd13e188dd68a4" + integrity sha1-hXYgV19nFHhvjYa7Gf0T4YjdaKQ= + dependencies: + to-function "*" + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +handlebars@^4.0.1: + version "4.7.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" + integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" + integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has@^1.0.1, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +htmlparser2@3.8.x: + version "3.8.3" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068" + integrity sha1-mWwosZFRaovoZQGn15dX5ccMEGg= + dependencies: + domelementtype "1" + domhandler "2.3" + domutils "1.5" + entities "1.0" + readable-stream "1.1" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@^0.4.4: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== + dependencies: + minimatch "^3.0.4" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +invert-kv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" + integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== + +is-buffer@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" + integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== + +is-callable@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= + dependencies: + has "^1.0.1" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-symbol@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" + integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== + dependencies: + has-symbols "^1.0.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +istanbul@: + version "0.4.5" + resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" + integrity sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs= + dependencies: + abbrev "1.0.x" + async "1.x" + escodegen "1.8.x" + esprima "2.7.x" + glob "^5.0.15" + handlebars "^4.0.1" + js-yaml "3.x" + mkdirp "0.5.x" + nopt "3.x" + once "1.x" + resolve "1.1.x" + supports-color "^3.1.0" + which "^1.1.1" + wordwrap "^1.0.0" + +js-yaml@3.13.1, js-yaml@3.x: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jshint@^2.8.0: + version "2.11.1" + resolved "https://registry.yarnpkg.com/jshint/-/jshint-2.11.1.tgz#28ec7d1cf7baaae5ce7bd37b9a70ed6cfd938570" + integrity sha512-WXWePB8ssAH3DlD05IoqolsY6arhbll/1+i2JkRPpihQAuiNaR/gSt8VKIcxpV5m6XChP0hCwESQUqpuQMA8Tg== + dependencies: + cli "~1.0.0" + console-browserify "1.1.x" + exit "0.1.x" + htmlparser2 "3.8.x" + lodash "~4.17.11" + minimatch "~3.0.2" + shelljs "0.3.x" + strip-json-comments "1.0.x" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json5@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" + integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== + dependencies: + minimist "^1.2.0" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug= + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +lcid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== + dependencies: + invert-kv "^2.0.0" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +lodash@^4.17.11, lodash@~4.17.11: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + +log-symbols@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" + integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== + dependencies: + chalk "^2.0.1" + +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +map-age-cleaner@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + +mem@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" + integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== + dependencies: + map-age-cleaner "^0.1.1" + mimic-fn "^2.0.0" + p-is-promise "^2.0.0" + +mime-db@1.40.0: + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== + dependencies: + mime-db "1.40.0" + +mimic-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.4, minimatch@~3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minipass@^2.2.1, minipass@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" + integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" + integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== + dependencies: + minipass "^2.2.1" + +mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +mocha-jshint@: + version "2.3.1" + resolved "https://registry.yarnpkg.com/mocha-jshint/-/mocha-jshint-2.3.1.tgz#303f27e533938559d20f26a4123d5bcb5645a546" + integrity sha1-MD8n5TOThVnSDyakEj1by1ZFpUY= + dependencies: + jshint "^2.8.0" + minimatch "^3.0.0" + shelljs "^0.4.0" + uniq "^1.0.1" + +mocha-parallel-tests@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/mocha-parallel-tests/-/mocha-parallel-tests-2.1.2.tgz#2f5c24022257e5fc6c63a6bd22d49c1487496b26" + integrity sha512-FatFg3MHLio9ir1oP6J0HNEo6R5344JC1y+We90iALdiT9F9xNPN0KbGXxRNlGlSl0GodfSESKbRzBvT9ctgIw== + dependencies: + circular-json "^0.5.9" + debug "^4.1.1" + uuid "^3.3.2" + yargs "^13.2.2" + +mocha@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.1.4.tgz#e35fada242d5434a7e163d555c705f6875951640" + integrity sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg== + dependencies: + ansi-colors "3.2.3" + browser-stdout "1.3.1" + debug "3.2.6" + diff "3.5.0" + escape-string-regexp "1.0.5" + find-up "3.0.0" + glob "7.1.3" + growl "1.10.5" + he "1.2.0" + js-yaml "3.13.1" + log-symbols "2.2.0" + minimatch "3.0.4" + mkdirp "0.5.1" + ms "2.1.1" + node-environment-flags "1.0.5" + object.assign "4.1.0" + strip-json-comments "2.0.1" + supports-color "6.0.0" + which "1.3.1" + wide-align "1.1.3" + yargs "13.2.2" + yargs-parser "13.0.0" + yargs-unparser "1.5.0" + +ms@2.1.1, ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +mz@^2.6.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nan@^2.11.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +needle@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" + integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== + dependencies: + debug "^3.2.6" + iconv-lite "^0.4.4" + sax "^1.2.4" + +neo-async@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-environment-flags@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.5.tgz#fa930275f5bf5dae188d6192b24b4c8bbac3d76a" + integrity sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ== + dependencies: + object.getownpropertydescriptors "^2.0.3" + semver "^5.7.0" + +node-gyp@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" + integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== + dependencies: + fstream "^1.0.0" + glob "^7.0.3" + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + nopt "2 || 3" + npmlog "0 || 1 || 2 || 3 || 4" + osenv "0" + request "^2.87.0" + rimraf "2" + semver "~5.3.0" + tar "^2.0.0" + which "1" + +node-pre-gyp@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054" + integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q== + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + +node-version@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/node-version/-/node-version-1.2.0.tgz#34fde3ffa8e1149bd323983479dda620e1b5060d" + integrity sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ== + +nodegit-promise@~4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/nodegit-promise/-/nodegit-promise-4.0.0.tgz#5722b184f2df7327161064a791d2e842c9167b34" + integrity sha1-VyKxhPLfcycWEGSnkdLoQskWezQ= + dependencies: + asap "~2.0.3" + +nodegit@^0.23.0: + version "0.23.1" + resolved "https://registry.yarnpkg.com/nodegit/-/nodegit-0.23.1.tgz#937c2cc998bbed3cef0b138131fee3a2672458af" + integrity sha512-k1bCg260yZlmfLGYPrinqAQNbQ6PnBJhKq6wWfe94yIO3WnOOyoSuweAkAGPR6FhrGq2LkCKHdJaitwhKCEGyA== + dependencies: + fs-extra "^7.0.0" + json5 "^2.1.0" + lodash "^4.17.11" + nan "^2.11.1" + node-gyp "^3.8.0" + node-pre-gyp "^0.11.0" + promisify-node "~0.3.0" + ramda "^0.25.0" + request-promise-native "^1.0.5" + tar-fs "^1.16.3" + +"nopt@2 || 3", nopt@3.x: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= + dependencies: + abbrev "1" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= + dependencies: + abbrev "1" + osenv "^0.1.4" + +npm-bundled@^1.0.1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" + integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== + +npm-packlist@^1.1.6: + version "1.4.1" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc" + integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw== + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-keys@^1.0.11, object-keys@^1.0.12: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.getownpropertydescriptors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.1" + +once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-locale@^3.0.0, os-locale@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" + integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== + dependencies: + execa "^1.0.0" + lcid "^2.0.0" + mem "^4.0.0" + +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@0, osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-is-promise@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" + integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== + +p-limit@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" + integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +pathval@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" + integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + +promise-polyfill@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057" + integrity sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc= + +promisify-node@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/promisify-node/-/promisify-node-0.3.0.tgz#b4b55acf90faa7d2b8b90ca396899086c03060cf" + integrity sha1-tLVaz5D6p9K4uQyjlomQhsAwYM8= + dependencies: + nodegit-promise "~4.0.0" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + +psl@^1.1.24: + version "1.1.32" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.32.tgz#3f132717cf2f9c169724b2b6caf373cf694198db" + integrity sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g== + +pump@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" + integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +ramda@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9" + integrity sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ== + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@1.1: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.0.6, readable-stream@^2.3.0, readable-stream@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +request-promise-core@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" + integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== + dependencies: + lodash "^4.17.11" + +request-promise-native@^1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" + integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== + dependencies: + request-promise-core "1.1.2" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@^2.87.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve@1.1.x: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= + +rimraf@: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rimraf@2, rimraf@^2.6.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@~2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +semver@^5.3.0, semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= + +semver@^5.5.0, semver@^5.7.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" + integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shelljs@0.3.x: + version "0.3.0" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1" + integrity sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E= + +shelljs@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.4.0.tgz#199fe9e2de379efd03d345ff14062525e4b31ec2" + integrity sha1-GZ/p4t43nv0D00X/FAYlJeSzHsI= + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" + integrity sha1-2rc/vPwrqBm03gO9b26qSBZLP50= + dependencies: + amdefine ">=0.0.4" + +split@: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +string-width@^1.0.1, "string-width@^1.0.2 || 2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-json-comments@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" + integrity sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E= + +strip-json-comments@2.0.1, strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +supports-color@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a" + integrity sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg== + dependencies: + has-flag "^3.0.0" + +supports-color@^3.1.0: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= + dependencies: + has-flag "^1.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +tar-fs@^1.16.3: + version "1.16.3" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509" + integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw== + dependencies: + chownr "^1.0.1" + mkdirp "^0.5.1" + pump "^1.0.0" + tar-stream "^1.1.2" + +tar-stream@^1.1.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" + integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== + dependencies: + bl "^1.0.0" + buffer-alloc "^1.2.0" + end-of-stream "^1.0.0" + fs-constants "^1.0.0" + readable-stream "^2.3.0" + to-buffer "^1.1.1" + xtend "^4.0.0" + +tar@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" + integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== + dependencies: + block-stream "*" + fstream "^1.0.12" + inherits "2" + +tar@^4: + version "4.4.9" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.9.tgz#058fbb152f6fc45733e84585a40c39e59302e1b3" + integrity sha512-xisFa7Q2i3HOgfn+nmnWLGHD6Tm23hxjkx6wwGmgxkJFr6wxwXnJOdJYcZjL453PSdF0+bemO03+flAzkIdLBQ== + dependencies: + chownr "^1.1.1" + fs-minipass "^1.2.5" + minipass "^2.3.5" + minizlib "^1.2.1" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.3" + +temp@: + version "0.9.1" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.9.1.tgz#2d666114fafa26966cd4065996d7ceedd4dd4697" + integrity sha512-WMuOgiua1xb5R56lE0eH6ivpVmg/lq2OHm4+LtT/xtEtPQ+sz6N3bBM6WZ5FvO1lO4IKIOb43qnhoc4qxP5OeA== + dependencies: + rimraf "~2.6.2" + +thenify-all@^1.0.0, thenify-all@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +through@2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +to-buffer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" + integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== + +to-function@*: + version "2.0.6" + resolved "https://registry.yarnpkg.com/to-function/-/to-function-2.0.6.tgz#7d56cd9c3b92fa8dbd7b22e83d51924de740ebc5" + integrity sha1-fVbNnDuS+o29eyLoPVGSTedA68U= + dependencies: + component-props "*" + +tough-cookie@^2.3.3, tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +uglify-js@^3.1.4: + version "3.10.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.10.0.tgz#397a7e6e31ce820bfd1cb55b804ee140c587a9e7" + integrity sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA== + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +walk@: + version "2.3.14" + resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.14.tgz#60ec8631cfd23276ae1e7363ce11d626452e1ef3" + integrity sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg== + dependencies: + foreachasync "^3.0.0" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@1, which@1.3.1, which@^1.1.1, which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wide-align@1.1.3, wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +xtend@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= + +"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + +yallist@^3.0.0, yallist@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" + integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== + +yargs-parser@13.0.0, yargs-parser@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.0.0.tgz#3fc44f3e76a8bdb1cc3602e860108602e5ccde8b" + integrity sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" + integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-unparser@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.5.0.tgz#f2bb2a7e83cbc87bb95c8e572828a06c9add6e0d" + integrity sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw== + dependencies: + flat "^4.1.0" + lodash "^4.17.11" + yargs "^12.0.5" + +yargs@13.2.2, yargs@^13.2.2: + version "13.2.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.2.tgz#0c101f580ae95cea7f39d927e7770e3fdc97f993" + integrity sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA== + dependencies: + cliui "^4.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + os-locale "^3.1.0" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.0.0" + +yargs@^12.0.5: + version "12.0.5" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" + integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== + dependencies: + cliui "^4.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^1.0.1" + os-locale "^3.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^11.1.1" From 66f97e93f17d9fbf04757787d9fa12a106c1f20b Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Thu, 20 Aug 2020 13:26:34 -0400 Subject: [PATCH 302/402] Bump nodegit to ^0.25.0 Bump to pick up a few changes: 1. https://github.com/nodegit/nodegit/commit/6f665bc5439c2e853fb48dbaadaae577949b854a, aka get rid of the dependency on system libcurl4-gnutls-dev library. 2. https://github.com/nodegit/nodegit/commit/e3c95e14e169c94d139ff27110b06a407354f42d and https://github.com/nodegit/nodegit/commit/ab3f379970dc4c070c3b10c5098be17f7dc6ad9c, yarn compartibility. --- node/lib/util/git_util.js | 2 +- node/lib/util/read_repo_ast_util.js | 2 +- node/lib/util/test_util.js | 4 +- node/lib/util/write_repo_ast_util.js | 2 +- node/package.json | 4 +- node/test/util/bulk_notes_util.js | 2 +- node/test/util/config_util.js | 4 +- node/test/util/destitch_util.js | 4 +- node/test/util/git_util.js | 2 +- node/test/util/include.js | 4 +- node/test/util/read_repo_ast_util.js | 14 ++-- node/test/util/repo_ast_test_util.js | 8 +- node/test/util/stash_util.js | 4 +- node/test/util/submodule_config_util.js | 4 +- node/yarn.lock | 101 ++++++++++++------------ 15 files changed, 80 insertions(+), 81 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index ea75abfed..d82c4c388 100755 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -208,7 +208,7 @@ exports.isValidRemoteName = co.wrap(function *(repo, name) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(name); - const remotes = yield repo.getRemotes(); + const remotes = yield repo.getRemoteNames(); return remotes.find(x => x === name) !== undefined; }); diff --git a/node/lib/util/read_repo_ast_util.js b/node/lib/util/read_repo_ast_util.js index 61095a5aa..7cdc1fdbd 100644 --- a/node/lib/util/read_repo_ast_util.js +++ b/node/lib/util/read_repo_ast_util.js @@ -258,7 +258,7 @@ exports.readRAST = co.wrap(function *(repo, includeRefsCommits) { includeRefsCommits = false; } assert.instanceOf(repo, NodeGit.Repository); - const branches = yield repo.getReferences(NodeGit.Reference.TYPE.LISTALL); + const branches = yield repo.getReferences(); let commits = {}; let branchTargets = {}; let refTargets = {}; diff --git a/node/lib/util/test_util.js b/node/lib/util/test_util.js index 4d47c5ba5..f2d1fee54 100644 --- a/node/lib/util/test_util.js +++ b/node/lib/util/test_util.js @@ -257,12 +257,12 @@ exports.makeBareCopy = co.wrap(function *(repo, path) { // Record the branches that exist in the bare repo. let existingBranches = {}; - const bareRefs = yield bare.getReferences(NodeGit.Reference.TYPE.LISTALL); + const bareRefs = yield bare.getReferences(); bareRefs.forEach(r => existingBranches[r.shorthand()] = true); // Then create all the branches that weren't copied initially. - const refs = yield repo.getReferences(NodeGit.Reference.TYPE.LISTALL); + const refs = yield repo.getReferences(); const sig = yield ConfigUtil.defaultSignature(bare); for (let i = 0; i < refs.length; ++i) { const ref = refs[i]; diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index d74b71e0c..6184a2880 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -754,7 +754,7 @@ exports.writeMultiRAST = co.wrap(function *(repos, rootDirectory) { repo.detachHead(); - const refs = yield repo.getReferences(NodeGit.Reference.TYPE.LISTALL); + const refs = yield repo.getReferences(); for (let i = 0; i < refs.length; ++i) { NodeGit.Branch.delete(refs[i]); } diff --git a/node/package.json b/node/package.json index f7899530a..8aa66d726 100644 --- a/node/package.json +++ b/node/package.json @@ -27,7 +27,7 @@ "preferGlobal": true, "homepage": "https://github.com/twosigma/git-meta#readme", "dependencies": { - "argparse": "", + "argparse": "^1.0.0", "binary-search": "", "chai": "", "child-process-promise": "", @@ -36,7 +36,7 @@ "deeper": "", "fs-promise": "", "group-by": "", - "nodegit": "^0.23.0", + "nodegit": "^0.25.0", "rimraf": "", "split": "", "walk": "" diff --git a/node/test/util/bulk_notes_util.js b/node/test/util/bulk_notes_util.js index 3362b06e1..d3fa6e4f2 100644 --- a/node/test/util/bulk_notes_util.js +++ b/node/test/util/bulk_notes_util.js @@ -112,7 +112,7 @@ describe("readNotes", function () { const foo = yield repo.getBranchCommit("foo"); const fooSha = foo.id().tostrS(); const refName = "refs/notes/foo/bar"; - const sig = repo.defaultSignature(); + const sig = yield repo.defaultSignature(); yield NodeGit.Note.create(repo, refName, sig, sig, fooSha, "foo", 1); yield NodeGit.Note.create(repo, refName, sig, sig, headSha, "bar", 1); const result = yield BulkNotesUtil.readNotes(repo, refName); diff --git a/node/test/util/config_util.js b/node/test/util/config_util.js index ed41be18d..f61909f11 100644 --- a/node/test/util/config_util.js +++ b/node/test/util/config_util.js @@ -98,8 +98,8 @@ describe("defaultSignature", function () { it("works", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); const actual = yield ConfigUtil.defaultSignature(repo); - assert.equal(actual.toString(), - repo.defaultSignature().toString()); + const sig = yield repo.defaultSignature(); + assert.equal(actual.toString(), sig.toString()); })); }); }); diff --git a/node/test/util/destitch_util.js b/node/test/util/destitch_util.js index 2e777e1aa..48220855d 100644 --- a/node/test/util/destitch_util.js +++ b/node/test/util/destitch_util.js @@ -539,7 +539,7 @@ describe("getDestitched", function () { metaCommit: "1", subCommits: {}, }; - const sig = repo.defaultSignature(); + const sig = yield repo.defaultSignature(); const refName = DestitchUtil.localReferenceNoteRef; const data = JSON.stringify(destitched, null, 4); yield NodeGit.Note.create(repo, refName, sig, sig, headSha, data, 1); @@ -555,7 +555,7 @@ describe("getDestitched", function () { metaCommit: "1", subCommits: {}, }; - const sig = repo.defaultSignature(); + const sig = yield repo.defaultSignature(); const refName = StitchUtil.referenceNoteRef; const data = JSON.stringify(destitched, null, 4); yield NodeGit.Note.create(repo, refName, sig, sig, headSha, data, 1); diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index 1949b5c50..2e207cf5b 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -1120,7 +1120,7 @@ describe("GitUtil", function () { const repo = yield TestUtil.createSimpleRepository(); const head = yield repo.getHeadCommit(); yield fs.appendFile(path.join(repo.workdir(), "README.md"), "foo"); - const sig = repo.defaultSignature(); + const sig = yield repo.defaultSignature(); const newCommitId = yield repo.createCommitOnHead(["README.md"], sig, sig, diff --git a/node/test/util/include.js b/node/test/util/include.js index d9ba2362b..1ff1b286e 100644 --- a/node/test/util/include.js +++ b/node/test/util/include.js @@ -115,10 +115,10 @@ describe("include", function () { })); it("should have signature of the current repo", co.wrap(function *() { - const repoSignature = repo.defaultSignature(); + const repoSignature = yield repo.defaultSignature(); const submoduleRepo = yield NodeGit.Repository.open(repo.workdir() + path); - const submoduleSignature = submoduleRepo.defaultSignature(); + const submoduleSignature = yield submoduleRepo.defaultSignature(); assert.equal(repoSignature.toString(), submoduleSignature.toString()); diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index 254d25e92..0d5b9e79d 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -408,7 +408,7 @@ describe("readRAST", function () { it("headless with a commit", co.wrap(function *() { const path = yield TestUtil.makeTempDir(); const r = yield NodeGit.Repository.init(path, 1); - const sig = r.defaultSignature(); + const sig = yield r.defaultSignature(); const builder = yield NodeGit.Treebuilder.create(r, null); const treeObj = yield builder.write(); const tree = yield r.getTree(treeObj.tostrS()); @@ -467,7 +467,7 @@ describe("readRAST", function () { it("remote with path in tracking branch", co.wrap(function *() { const base = yield TestUtil.createSimpleRepository(); const headId = (yield base.getHeadCommit()).id(); - const sig = base.defaultSignature(); + const sig = yield base.defaultSignature(); yield base.createBranch("foo/bar", headId, 1, sig, "branch"); const clonePath = yield TestUtil.makeTempDir(); const clone = yield NodeGit.Clone.clone(base.workdir(), clonePath); @@ -1007,7 +1007,7 @@ describe("readRAST", function () { const workdir = repo.workdir(); const firstCommit = yield repo.getHeadCommit(); const firstSha = firstCommit.id().tostrS(); - const sig = repo.defaultSignature(); + const sig = yield repo.defaultSignature(); yield repo.createBranch("b", firstCommit, 0, sig); yield repo.checkoutBranch("b"); yield fs.writeFile(path.join(workdir, "foo"), "foo"); @@ -1060,7 +1060,7 @@ describe("readRAST", function () { it("merge commit with submodule change", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); - const sig = repo.defaultSignature(); + const sig = yield repo.defaultSignature(); // Create the base repo for the submodule and add a couple of // commits. @@ -1217,7 +1217,7 @@ describe("readRAST", function () { it("merge commit with ignored submodule change", co.wrap(function *() { const repo = yield TestUtil.createSimpleRepository(); - const sig = repo.defaultSignature(); + const sig = yield repo.defaultSignature(); // Create the base repo for the submodule and add a couple of // commits. @@ -1372,7 +1372,7 @@ describe("readRAST", function () { const head = yield r.getHeadCommit(); const headId = head.id(); - const sig = r.defaultSignature(); + const sig = yield r.defaultSignature(); yield NodeGit.Note.create(r, "refs/notes/test", sig, sig, headId, "note", 0); @@ -1688,7 +1688,7 @@ describe("readRAST", function () { yield index.addByPath("foobar"); yield index.write(); yield NodeGit.Stash.save(repo, - repo.defaultSignature(), + yield repo.defaultSignature(), "stash", NodeGit.Stash.FLAGS.INCLUDE_UNTRACKED); yield ReadRepoASTUtil.readRAST(repo); diff --git a/node/test/util/repo_ast_test_util.js b/node/test/util/repo_ast_test_util.js index 1723e0360..92ad62dde 100644 --- a/node/test/util/repo_ast_test_util.js +++ b/node/test/util/repo_ast_test_util.js @@ -85,7 +85,7 @@ const makeClone = co.wrap(function *(repos, maps) { // Test framework expects a trailing '/' to support relative paths. const b = yield NodeGit.Clone.clone(aPath, bPath); - const sig = b.defaultSignature(); + const sig = yield b.defaultSignature(); const head = yield b.getHeadCommit(); yield b.createBranch("foo", head.id(), 1, sig, "branch commit"); yield b.checkoutBranch("foo"); @@ -265,7 +265,7 @@ Bmaster=1 origin/master`, m: co.wrap(function *(repos) { const x = repos.x; const head = yield x.getHeadCommit(); - const sig = x.defaultSignature(); + const sig = yield x.defaultSignature(); yield repos.x.createBranch("foo", head, 0, sig); throw new UserError("bad bad"); }), @@ -293,7 +293,7 @@ Bmaster=1 origin/master`, const x = repos.x; const head = yield x.getHeadCommit(); const headStr = head.id().tostrS(); - const sig = x.defaultSignature(); + const sig = yield x.defaultSignature(); yield repos.x.createBranch(`foo-${headStr}`, head, 0, sig); }), e: "x=S", @@ -316,7 +316,7 @@ Bmaster=1 origin/master`, const x = repos.x; const head = yield x.getHeadCommit(); const headStr = head.id().tostrS(); - const sig = x.defaultSignature(); + const sig = yield x.defaultSignature(); yield repos.x.createBranch(`foo-${headStr}`, head, 0, sig); }), e: "x=E:Bfoo-1=1", diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index d0978e921..106ff896b 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -42,7 +42,7 @@ const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); const writeLog = co.wrap(function *(repo, reverseMap, logs) { const log = yield NodeGit.Reflog.read(repo, "refs/meta-stash"); - const sig = repo.defaultSignature(); + const sig = yield repo.defaultSignature(); for(let i = 0; i < logs.length; ++i) { const logSha = logs[logs.length - (i + 1)]; const sha = reverseMap[logSha]; @@ -160,7 +160,7 @@ x=E:Ci#i foo=bar,1=1;Cw#w foo=bar,1=1;Bi=i;Bw=w`, const result = yield StashUtil.stashRepo(repo, status, includeUntracked); - const sig = repo.defaultSignature(); + const sig = yield repo.defaultSignature(); const commitMap = {}; const commitAndBranch = co.wrap(function *(treeId, type) { const tree = yield NodeGit.Tree.lookup(repo, treeId); diff --git a/node/test/util/submodule_config_util.js b/node/test/util/submodule_config_util.js index c6a6883aa..5b4c99d26 100644 --- a/node/test/util/submodule_config_util.js +++ b/node/test/util/submodule_config_util.js @@ -652,7 +652,7 @@ foo newHead, NodeGit.Reset.TYPE.HARD); yield submodule.addFinalize(); - const sig = repo.defaultSignature(); + const sig = yield repo.defaultSignature(); yield repo.createCommitOnHead([".gitmodules", subName], sig, sig, @@ -803,7 +803,7 @@ foo newHead, NodeGit.Reset.TYPE.HARD); yield submodule.addFinalize(); - const sig = repo.defaultSignature(); + const sig = yield repo.defaultSignature(); yield repo.createCommitOnHead([".gitmodules", "foo"], sig, sig, diff --git a/node/yarn.lock b/node/yarn.lock index 63cbd87d1..0c3550e74 100644 --- a/node/yarn.lock +++ b/node/yarn.lock @@ -72,7 +72,7 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" -argparse@, argparse@^1.0.7: +argparse@^1.0.0, argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== @@ -146,13 +146,6 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= - dependencies: - inherits "~2.0.0" - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -664,16 +657,6 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fstream@^1.0.0, fstream@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" - integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -873,7 +856,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@~2.0.1, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= @@ -1080,6 +1063,11 @@ lodash@^4.17.11, lodash@~4.17.11: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== +lodash@^4.17.14: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + log-symbols@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" @@ -1158,6 +1146,14 @@ minipass@^2.2.1, minipass@^2.3.5: safe-buffer "^5.1.2" yallist "^3.0.0" +minipass@^2.8.6: + version "2.9.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + minizlib@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" @@ -1165,7 +1161,7 @@ minizlib@^1.2.1: dependencies: minipass "^2.2.1" -mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= @@ -1235,10 +1231,10 @@ mz@^2.6.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nan@^2.11.1: - version "2.14.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" - integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== +nan@^2.14.0: + version "2.14.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" + integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== needle@^2.2.1: version "2.4.0" @@ -1267,12 +1263,11 @@ node-environment-flags@1.0.5: object.getownpropertydescriptors "^2.0.3" semver "^5.7.0" -node-gyp@^3.8.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" - integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== +node-gyp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-4.0.0.tgz#972654af4e5dd0cd2a19081b4b46fe0442ba6f45" + integrity sha512-2XiryJ8sICNo6ej8d0idXDEMKfVfFK7kekGCtJAuelGsYHQxhj13KTf95swTCN2dZ/4lTfZ84Fu31jqJEEgjWA== dependencies: - fstream "^1.0.0" glob "^7.0.3" graceful-fs "^4.1.2" mkdirp "^0.5.0" @@ -1282,13 +1277,13 @@ node-gyp@^3.8.0: request "^2.87.0" rimraf "2" semver "~5.3.0" - tar "^2.0.0" + tar "^4.4.8" which "1" -node-pre-gyp@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054" - integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q== +node-pre-gyp@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz#df9ab7b68dd6498137717838e4f92a33fc9daa42" + integrity sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ== dependencies: detect-libc "^1.0.2" mkdirp "^0.5.1" @@ -1313,17 +1308,17 @@ nodegit-promise@~4.0.0: dependencies: asap "~2.0.3" -nodegit@^0.23.0: - version "0.23.1" - resolved "https://registry.yarnpkg.com/nodegit/-/nodegit-0.23.1.tgz#937c2cc998bbed3cef0b138131fee3a2672458af" - integrity sha512-k1bCg260yZlmfLGYPrinqAQNbQ6PnBJhKq6wWfe94yIO3WnOOyoSuweAkAGPR6FhrGq2LkCKHdJaitwhKCEGyA== +nodegit@^0.25.0: + version "0.25.1" + resolved "https://registry.yarnpkg.com/nodegit/-/nodegit-0.25.1.tgz#70b9d89e62cbc1153237a10a196e3228a669895b" + integrity sha512-j2kEd4jTraimRPKDX31DsLcfY9fWpcYG8zT0tiLVtN5jMm9fDFgn3WOQ+Nk+3NcBqxr4p5N5pZqNrCS0nGdTbg== dependencies: fs-extra "^7.0.0" json5 "^2.1.0" - lodash "^4.17.11" - nan "^2.11.1" - node-gyp "^3.8.0" - node-pre-gyp "^0.11.0" + lodash "^4.17.14" + nan "^2.14.0" + node-gyp "^4.0.0" + node-pre-gyp "^0.13.0" promisify-node "~0.3.0" ramda "^0.25.0" request-promise-native "^1.0.5" @@ -1920,15 +1915,6 @@ tar-stream@^1.1.2: to-buffer "^1.1.1" xtend "^4.0.0" -tar@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" - integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== - dependencies: - block-stream "*" - fstream "^1.0.12" - inherits "2" - tar@^4: version "4.4.9" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.9.tgz#058fbb152f6fc45733e84585a40c39e59302e1b3" @@ -1942,6 +1928,19 @@ tar@^4: safe-buffer "^5.1.2" yallist "^3.0.3" +tar@^4.4.8: + version "4.4.13" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" + integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== + dependencies: + chownr "^1.1.1" + fs-minipass "^1.2.5" + minipass "^2.8.6" + minizlib "^1.2.1" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.3" + temp@: version "0.9.1" resolved "https://registry.yarnpkg.com/temp/-/temp-0.9.1.tgz#2d666114fafa26966cd4065996d7ceedd4dd4697" From d5ed4bb715e5e6a4f46344db5fa0270bceb5066a Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Fri, 21 Aug 2020 10:22:15 -0400 Subject: [PATCH 303/402] fix travis-ci not triggered --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 14c9498d9..ab951ece4 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@

+[![Build Status](https://travis-ci.org/twosigma/git-meta.svg?branch=master)](https://travis-ci.org/twosigma/git-meta) + # What is git-meta? Git-meta allows developers to work with extremely large codebases -- From b9ce32d058b9365b170b50368d284857ecb91164 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 19 Aug 2020 14:53:35 -0400 Subject: [PATCH 304/402] Support multiple commits in cherry-pick --- node/lib/cmd/cherry_pick.js | 34 ++++++----- node/lib/util/cherry_pick_util.js | 94 +++++++++++++++++++++--------- node/test/util/cherry_pick_util.js | 72 ++++++++++++++++++++++- 3 files changed, 155 insertions(+), 45 deletions(-) diff --git a/node/lib/cmd/cherry_pick.js b/node/lib/cmd/cherry_pick.js index 6ba5d4547..33aee4d48 100644 --- a/node/lib/cmd/cherry_pick.js +++ b/node/lib/cmd/cherry_pick.js @@ -55,7 +55,7 @@ fast-forwarded.`; exports.configureParser = function (parser) { parser.addArgument(["commit"], { - nargs: "?", + nargs: "*", type: "string", help: "the commit to cherry-pick", }); @@ -88,7 +88,6 @@ exports.executeableSubcommand = co.wrap(function *(args) { const CherryPickUtil = require("../util/cherry_pick_util"); const GitUtil = require("../util/git_util"); - const Hook = require("../util/hook"); const UserError = require("../util/user_error"); const repo = yield GitUtil.getCurrentRepo(); @@ -120,22 +119,25 @@ exports.executeableSubcommand = co.wrap(function *(args) { throw new UserError("No commit to cherry-pick."); } - const commitish = args.commit; - const annotated = yield GitUtil.resolveCommitish(repo, commitish); - if (null === annotated) { - throw new UserError(`\ + // TOOD: check if we are mid-rebase already + + const commits = args.commit; + + const resolvedCommits = []; + for (let commitish of commits) { + const annotated = yield GitUtil.resolveCommitish(repo, commitish); + if (null === annotated) { + throw new UserError(`\ Could not resolve ${colors.red(commitish)} to a commit.`); - } - else { - const id = annotated.id(); - console.log(`Cherry-picking commit ${colors.green(id.tostrS())}.`); - const commit = yield repo.getCommit(id); - const result = yield CherryPickUtil.cherryPick(repo, commit); - if (null !== result.errorMessage) { - throw new UserError(result.errorMessage); } + const commit = yield repo.getCommit(annotated.id()); + resolvedCommits.push(commit); + } - // Run post-commit hook as regular git. - yield Hook.execHook(repo, "post-commit"); + const result = yield CherryPickUtil.cherryPick(repo, resolvedCommits); + + if (null !== result.errorMessage) { + throw new UserError(result.errorMessage); + } }); diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 50c63d6a5..de40a8b4e 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -41,6 +41,7 @@ const rimraf = require("rimraf"); const ConflictUtil = require("./conflict_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); +const Hook = require("../util/hook"); const Open = require("./open"); const RepoStatus = require("./repo_status"); const Reset = require("./reset"); @@ -742,6 +743,7 @@ exports.rewriteCommit = co.wrap(function *(repo, commit) { }); const result = { + pickingCommit: commit, submoduleCommits: picks.commits, errorMessage: errorMessage === "" ? null : errorMessage, newMetaCommit: null, @@ -755,33 +757,69 @@ exports.rewriteCommit = co.wrap(function *(repo, commit) { (0 !== Object.keys(changes.simpleChanges).length || 0 !== nChanges)) { result.newMetaCommit = yield SubmoduleRebaseUtil.makeCommit(repo, commit); + } + + if (result.errorMessage === null) { + // Run post-commit hook as regular git. + yield Hook.execHook(repo, "post-commit"); } return result; }); +const pickRemainingCommits = co.wrap(function*(metaRepo, seq) { + const commits = seq.commits; + let result; + for (let i = seq.currentCommit; i < commits.length; ++i) { + const id = commits[i]; + const commit = yield metaRepo.getCommit(id); + console.log(`Cherry-picking commit ${colors.green(id)}.`); + + seq = seq.copy({currentCommit : i}); + yield SequencerStateUtil.writeSequencerState(metaRepo.path(), seq); + result = yield exports.rewriteCommit(metaRepo, commit); + if (null !== result.errorMessage) { + return result; + } + if (null === result.newMetaCommit) { + // TODO: stop and offer the user the option of git commit + // --allow-empty vs cherry-pick --skip. For now, tho, + // empty meta commits are pretty useless so we will just + // skip. + console.log("Nothing to commit."); + } + } + + yield SequencerStateUtil.cleanSequencerState(metaRepo.path()); + return result; +}); + + /** - * Cherry-pick the specified `commit` in the specified `metaRepo`. Return an - * object with the cherry-picked commits ids. This object contains the id of - * the newly-generated meta-repo commit and for each sub-repo, a map from - * new (cherry-pick) sha to the original commit sha. Throw a `UserError` if - * the repository is not in a state that can allow a cherry-pick (e.g., it's - * rebasing), if `commit` contains changes that we cannot cherry-pick (e.g., - * URL-only changes), or if the cherry-pick would result in no changes (TODO: - * provide support for '--allow-empty' if needed). If the cherry-pick is - * initiated but results in a conflicts, the `errorMessage` of the returned - * object will be non-null and will contain a description of the conflicts. + * Cherry-pick the specified `commits` in the specified `metaRepo`. + * Return an object with the cherry-picked commits ids for the last + * cherry-picked commit (whether or not that was successful). This + * object contains the id of the newly-generated meta-repo commit and + * for each sub-repo, a map from new (cherry-pick) sha to the original + * commit sha. Throw a `UserError` if the repository is not in a + * state that can allow a cherry-pick (e.g., it's rebasing), if + * `commit` contains changes that we cannot cherry-pick (e.g., + * URL-only changes), or if the cherry-pick would result in no changes + * (TODO: provide support for '--allow-empty' if needed). If the + * cherry-pick is initiated but results in a conflicts, the + * `errorMessage` of the returned object will be non-null and will + * contain a description of the conflicts. * * @async * @param {NodeGit.Repository} metaRepo - * @param {NodeGit.Commit} commit + * @param [{NodeGit.Commit}] commits * @return {Object} return * @return {String} return.newMetaCommit * @return {Object} returm.submoduleCommits * @return {String|null} return.errorMessage */ -exports.cherryPick = co.wrap(function *(metaRepo, commit) { +exports.cherryPick = co.wrap(function *(metaRepo, commits) { assert.instanceOf(metaRepo, NodeGit.Repository); - assert.instanceOf(commit, NodeGit.Commit); + assert.instanceOf(commits[0], NodeGit.Commit); const status = yield StatusUtil.getRepoStatus(metaRepo); StatusUtil.ensureReady(status); @@ -805,22 +843,19 @@ running cherry-pick.`); // cherry-pick file. const head = yield metaRepo.getHeadCommit(); - const seq = new SequencerState({ + const commitIdStrs = commits.map(x => x.id().tostrS()); + let lastCommit = commitIdStrs[commitIdStrs.length - 1]; + let seq = new SequencerState({ type: CHERRY_PICK, originalHead: new CommitAndRef(head.id().tostrS(), null), - target: new CommitAndRef(commit.id().tostrS(), null), + // target is bogus for cherry-picks but must be filled in anyway + target: new CommitAndRef(lastCommit, null), currentCommit: 0, - commits: [commit.id().tostrS()], + commits: commitIdStrs, }); yield SequencerStateUtil.writeSequencerState(metaRepo.path(), seq); - const result = yield exports.rewriteCommit(metaRepo, commit); - if (null === result.errorMessage) { - yield SequencerStateUtil.cleanSequencerState(metaRepo.path()); - } - if (null === result.newMetaCommit) { - console.log("Nothing to commit."); - } - return result; + + return yield pickRemainingCommits(metaRepo, seq); }); /** @@ -847,21 +882,26 @@ exports.continue = co.wrap(function *(repo) { throw new UserError("Resolve conflicts then continue cherry-pick."); } const index = yield repo.index(); - const commit = yield repo.getCommit(seq.target.sha); + const commit = yield repo.getCommit(seq.commits[seq.currentCommit]); const subResult = yield SubmoduleRebaseUtil.continueSubmodules(repo, index, status, commit); + const result = { + pickingCommit: commit, newMetaCommit: subResult.metaCommit, submoduleCommits: subResult.commits, newSubmoduleCommits: subResult.newCommits, errorMessage: subResult.errorMessage, }; - if (null === subResult.errorMessage) { + if (subResult.errorMessage !== null || + seq.currentCommit + 1 === seq.commits.length) { yield SequencerStateUtil.cleanSequencerState(repo.path()); + return result; } - return result; + const newSeq = seq.copy({currentCommit : seq.currentCommit + 1}); + return yield pickRemainingCommits(repo, newSeq); }); /** diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index 069cd7b2c..43d07c5cb 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -33,16 +33,22 @@ const assert = require("chai").assert; const co = require("co"); const colors = require("colors"); +const fs = require("fs-promise"); const NodeGit = require("nodegit"); +const path = require("path"); + +const Add = require("../../lib/util/add"); const ConflictUtil = require("../../lib/util/conflict_util"); const CherryPickUtil = require("../../lib/util/cherry_pick_util"); const Open = require("../../lib/util/open"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); +const ReadRepoASTUtil = require("../../lib/util/read_repo_ast_util"); const Submodule = require("../../lib/util/submodule"); const SubmoduleChange = require("../../lib/util/submodule_change"); const UserError = require("../../lib/util/user_error"); + /** * Return a commit map as expected from a manipulator for `RepoASTTestUtil` * from a result having the `newMetaCommit` and `submoduleCommits` properties @@ -842,10 +848,10 @@ Submodule ${colors.red("s")} is conflicted. assert.property(reverseCommitMap, "8"); const eightCommitSha = reverseCommitMap["8"]; const eightCommit = yield x.getCommit(eightCommitSha); - const result = yield CherryPickUtil.cherryPick(x, eightCommit); + const result = yield CherryPickUtil.cherryPick(x, [eightCommit]); if (c.duplicate) { - const res = yield CherryPickUtil.cherryPick(x, eightCommit); + const res = yield CherryPickUtil.cherryPick(x, [eightCommit]); assert.isNull(res.newMetaCommit); } assert.equal(result.errorMessage, c.errorMessage || null); @@ -919,6 +925,68 @@ x=E:Cfoo#CP-2 s=Sa:Ns;Bmaster=CP;Os Cfoo#Ns-1 foo=bar;Q`, c.fails); })); }); + + it("handles multiple commits", co.wrap(function*() { + const start = `a=B:C2-1 a=a1; + C3-2 a=a3; + C4-3 a=a4; + C5-3 a=a5; + C6-3 a=a6; + Bb3=3;Bb4=4;Bb5=5;Bb6=6| + x=S:Cm2-1 s=Sa:2; + Cm3-m2 s=Sa:3; + Cm4-m2 s=Sa:4; + Cm5-m2 s=Sa:5; + Cm6-m2 s=Sa:6; + Bmaster=m3;Bm4=m4;Bm5=m5;Bm6=m6;Os`; + const repoMap = yield RepoASTTestUtil.createMultiRepos(start); + const repo = repoMap.repos.x; + const commits = [ + (yield repo.getCommit(repoMap.reverseCommitMap.m4)), + (yield repo.getCommit(repoMap.reverseCommitMap.m5)), + (yield repo.getCommit(repoMap.reverseCommitMap.m6)), + ]; + const result = yield CherryPickUtil.cherryPick(repo, commits); + // I expect, here, that commit m4 has successfully applied, and + // then m5 has hit a conflict... + assert.equal("m5", repoMap.commitMap[result.pickingCommit.id()]); + assert.isNull(result.newMetaCommit); + let rast = yield ReadRepoASTUtil.readRAST(repo, false); + const remainingCommits = [ + commits[0].id().tostrS(), + commits[1].id().tostrS(), + commits[2].id().tostrS() + ]; + + assert.deepEqual(remainingCommits, rast.sequencerState.commits); + assert.equal(1, rast.sequencerState.currentCommit); + + //now, let's resolve & continue + yield fs.writeFile(path.join(repo.workdir(), "s", "a"), "resolv"); + yield Add.stagePaths(repo, ["s/a"], true, false); + const contResult = yield CherryPickUtil.continue(repo); + + assert.equal("m6", repoMap.commitMap[contResult.pickingCommit.id()]); + assert.isNull(contResult.newMetaCommit); + + rast = yield ReadRepoASTUtil.readRAST(repo, false); + assert.deepEqual(remainingCommits, rast.sequencerState.commits); + assert.equal(2, rast.sequencerState.currentCommit); + + //finally, we'll do it again, which should finish this up + yield fs.writeFile(path.join(repo.workdir(), "s", "a"), "resolv2"); + try { + yield CherryPickUtil.continue(repo); + assert.equal(1, 2); //fail + } catch (e) { + //can't continue until we resolve + } + yield Add.stagePaths(repo, ["s/a"], true, false); + const contResult2 = yield CherryPickUtil.continue(repo); + assert.isNull(contResult2.errorMessage); + rast = yield ReadRepoASTUtil.readRAST(repo, false); + assert.null(rast.sequencerState); + })); }); describe("abort", function () { From 139f7c92e7a662dcfdf12ea808d56ee61b3f9f8c Mon Sep 17 00:00:00 2001 From: David Turner Date: Sat, 22 Aug 2020 00:35:11 -0400 Subject: [PATCH 305/402] Handle ranges in cherry-pick --- node/lib/cmd/cherry_pick.js | 76 ++++++++++++++++++++++-------- node/package.json | 1 + node/test/util/cherry_pick_util.js | 2 +- 3 files changed, 59 insertions(+), 20 deletions(-) diff --git a/node/lib/cmd/cherry_pick.js b/node/lib/cmd/cherry_pick.js index 33aee4d48..3c34dc1cf 100644 --- a/node/lib/cmd/cherry_pick.js +++ b/node/lib/cmd/cherry_pick.js @@ -31,6 +31,10 @@ "use strict"; const co = require("co"); +const range = require("git-range"); + +const GitUtil = require("../util/git_util"); +const UserError = require("../util/user_error"); /** * This module contains methods for implementing the `cherry-pick` command. @@ -76,6 +80,57 @@ abort the cherry-pick and return to previous state, throwing away all changes`, }); }; + +exports.isRange = function(spec) { + // note that this includes ^x, which is not precisely a range + return spec.search("\\.\\.|\\^[@!-]|^\\^") !== -1; +}; + +exports.resolveRange = co.wrap(function *(repo, commitArg) { + const colors = require("colors"); + + // range.parse doesn't offer "--no-walk", so if you pass in an arg + // that doesn't specify a range (["morx", "fleem"], say, as opposed to + // "morx..fleem"), it'll give all of the commits which are parents + // of HEAD. But ^morx fleem is treated as morx..fleem. + + // So we need to do some pre-parsing. + + // I did some basic testing to ensure that git-range matches the + // selection and ordering that git cherry-pick uses, and it seems + // to be correct (including in the surprising case where git + // cherry-pick will use the topological ordering of commits rather + // than the order given on the command-line). + + let commits = []; + if (commitArg.some(exports.isRange)) { + for (const arg of commitArg) { + if (arg.search("^@") !== -1) { + // TODO: patch git-range + throw new UserError(`\ +Could not handle ${arg}, because git-range does not support --no-walk. +Please pre-parse these args using regular git.`); + } + } + const r = yield range.parse(repo, commitArg); + commits = yield r.commits(); + } else { + for (let commitish of commitArg) { + let annotated = yield GitUtil.resolveCommitish(repo, commitish); + if (null === annotated) { + throw new UserError(`\ +Could not resolve ${colors.red(commitish)} to a commit.`); + } + const commit = yield repo.getCommit(annotated.id()); + commits.push(commit); + } + } + if (commits.length === 0) { + throw new UserError(`empty commit set passed`); + } + return commits; +}); + /** * Execute the `cherry-pick` command according to the specified `args`. * @@ -84,11 +139,7 @@ abort the cherry-pick and return to previous state, throwing away all changes`, * @param {String[]} args.commit */ exports.executeableSubcommand = co.wrap(function *(args) { - const colors = require("colors"); - const CherryPickUtil = require("../util/cherry_pick_util"); - const GitUtil = require("../util/git_util"); - const UserError = require("../util/user_error"); const repo = yield GitUtil.getCurrentRepo(); @@ -121,21 +172,8 @@ exports.executeableSubcommand = co.wrap(function *(args) { // TOOD: check if we are mid-rebase already - const commits = args.commit; - - const resolvedCommits = []; - for (let commitish of commits) { - const annotated = yield GitUtil.resolveCommitish(repo, commitish); - if (null === annotated) { - throw new UserError(`\ -Could not resolve ${colors.red(commitish)} to a commit.`); - } - const commit = yield repo.getCommit(annotated.id()); - resolvedCommits.push(commit); - - } - - const result = yield CherryPickUtil.cherryPick(repo, resolvedCommits); + const commits = yield exports.resolveRange(repo, args.commit); + const result = yield CherryPickUtil.cherryPick(repo, commits); if (null !== result.errorMessage) { throw new UserError(result.errorMessage); diff --git a/node/package.json b/node/package.json index f7899530a..90d462467 100644 --- a/node/package.json +++ b/node/package.json @@ -35,6 +35,7 @@ "colors": "", "deeper": "", "fs-promise": "", + "git-range": "", "group-by": "", "nodegit": "^0.23.0", "rimraf": "", diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index 43d07c5cb..2fca63dc5 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -985,7 +985,7 @@ x=E:Cfoo#CP-2 s=Sa:Ns;Bmaster=CP;Os Cfoo#Ns-1 foo=bar;Q`, const contResult2 = yield CherryPickUtil.continue(repo); assert.isNull(contResult2.errorMessage); rast = yield ReadRepoASTUtil.readRAST(repo, false); - assert.null(rast.sequencerState); + assert.isNull(rast.sequencerState); })); }); From 81450327c1323a8a9b8b23f277691eb172a754fd Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Mon, 24 Aug 2020 22:45:16 -0400 Subject: [PATCH 306/402] Let git-meta notice changes to .gitmodules With the change: - git meta status will show changes to .gitmodules - git meta add . will stage .gitmodules - git meta commit -a -m will include .gitmodules changes --- node/lib/util/diff_util.js | 8 +- node/lib/util/status_util.js | 239 ++++++++++++++++++---------------- node/test/util/commit.js | 3 + node/test/util/diff_util.js | 6 + node/test/util/status_util.js | 12 ++ 5 files changed, 152 insertions(+), 116 deletions(-) diff --git a/node/lib/util/diff_util.js b/node/lib/util/diff_util.js index 6192e148b..301dc477c 100644 --- a/node/lib/util/diff_util.js +++ b/node/lib/util/diff_util.js @@ -35,7 +35,6 @@ const co = require("co"); const NodeGit = require("nodegit"); const RepoStatus = require("./repo_status"); -const SubmoduleConfigUtil = require("./submodule_config_util"); const DELTA = NodeGit.Diff.DELTA; @@ -81,11 +80,8 @@ function readDiff(diff) { delta.newFile(); const path = file.path(); - // Skip the .gitmodules file and all submodule changes; they're handled - // separately. - - if (SubmoduleConfigUtil.modulesFileName !== path && - NodeGit.TreeEntry.FILEMODE.COMMIT !== file.mode()) { + // Skip the all submodule changes; they're handled separately. + if (NodeGit.TreeEntry.FILEMODE.COMMIT !== file.mode()) { result[path] = fileStatus; } } diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 0487183f8..08b810863 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -355,6 +355,116 @@ exports.readConflicts = function (index, paths) { return result; }; +/** + * Return the status of submodules. + * + * @async + * @param {NodeGit.Repository} repo + * @param {Object} [options] see `getRepoStatus` for option fields + * @param {NodeGit.Commit} headCommit HEAD commit + * @return {Object} status of the submodule + * @returns {Object} return.conflicts list of conflicts in the index + * @returns {Object} return.submodule list of submodule names to status + */ +const getSubRepoStatus = co.wrap(function *(repo, options, headCommit) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(headCommit, NodeGit.Commit); + + const openArray = yield SubmoduleUtil.listOpenSubmodules(repo); + const openSet = new Set(openArray); + const index = yield repo.index(); + const headTree = yield headCommit.getTree(); + const diff = yield NodeGit.Diff.treeToIndex(repo, headTree, index); + const changes = + yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff, true); + const indexUrls = + yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + const headUrls = + yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, headCommit); + + const result = { + conflicts: exports.readConflicts(index, options.paths), + submodules: {}, + }; + + // No paths specified, so we'll do all submodules, restricting to open + // ones based on options. + let filterPaths; // map from sub name to paths to use + const filtering = 0 !== options.paths.length; + + // Will look at submodules that are open or have changes. TODO: we're + // ignoring changes affecting only the `.gitmodules` file for now. + let subsToList = Array.from(new Set( + openArray.concat(Object.keys(changes)))); + + if (filtering) { + filterPaths = SubmoduleUtil.resolvePaths(options.paths, + subsToList, + openArray); + subsToList = Object.keys(filterPaths); + } + + // Make a list of promises to read the status for each submodule, then + // evaluate them in parallel. + const subStatMakers = subsToList.map(co.wrap(function* (name) { + const headUrl = headUrls[name] || null; + const indexUrl = indexUrls[name] || null; + let headSha = null; + let indexSha = null; + let subRepo = null; + + // Load commit information available based on whether the submodule + // was added, removed, changed, or just open. + const change = changes[name]; + if (undefined !== change) { + headSha = change.oldSha; + indexSha = change.newSha; + } + else { + // Just open, we need to load its sha. Unfortunately, the diff + // we did above doesn't catch new submodules with unstaged + // commits; validate that we have commit and index URLs and + // entries before trying to read them. + if (null !== headUrl) { + headSha = (yield headTree.entryByPath(name)).sha(); + } + if (null !== indexUrl) { + const indexEntry = index.getByPath(name); + if (undefined !== indexEntry) { + indexSha = indexEntry.id.tostrS(); + } + } + } + let status = null; + if (openSet.has(name)) { + subRepo = yield SubmoduleUtil.getRepo(repo, name); + status = yield exports.getRepoStatus(subRepo, { + paths: filtering ? filterPaths[name] : [], + showAllUntracked: options.showAllUntracked, + ignoreIndex: options.ignoreIndex, + showMetaChanges: true, + }); + } + return yield exports.getSubmoduleStatus(subRepo, + status, + indexUrl, + headUrl, + indexSha, + headSha); + })); + + const subStats = yield subStatMakers; + + // And copy them into the arguments. + subsToList.forEach((name, i) => { + const subStat = subStats[i]; + if (undefined !== subStat) { + result.submodules[name] = subStats[i]; + } + }); + return result; +}); + /** * Return a description of the status of changes to the specified `repo`. If * the optionally specified `options.showAllUntracked` is true (default false), @@ -453,7 +563,7 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { args.sequencerState = yield SequencerStateUtil.readSequencerState(repo.path()); - if (options.showMetaChanges && !repo.isBare()) { + if (!repo.isBare()) { const head = yield repo.getHeadCommit(); let tree = null; if (null !== head) { @@ -465,118 +575,27 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { options.paths, options.ignoreIndex, options.showAllUntracked); - args.staged = status.staged; - args.workdir = status.workdir; + // if showMetaChanges is off, keep .gitmodules changes only + if (options.showMetaChanges) { + args.staged = status.staged; + args.workdir = status.workdir; + } else { + const gitmodules = SubmoduleConfigUtil.modulesFileName; + if (gitmodules in status.staged) { + args.staged[gitmodules] = status.staged[gitmodules]; + } + if (gitmodules in status.workdir) { + args.workdir[gitmodules] = status.workdir[gitmodules]; + } + } } - // Now do the submodules. First, list the submodules visible in the head - // commit and index. - // - // TODO: For now, we're just not going to return the status of submodules - // in a headless repository (which is better than our previous behavior of - // crashing); we should fix it so that we can accurately reflect staged - // submodules in the index. - + // Now do the submodules. if (null !== headCommit) { - // Now we need to figure out which subs to list, and what paths to - // inspect in them. - - const openArray = yield SubmoduleUtil.listOpenSubmodules(repo); - const openSet = new Set(openArray); - const index = yield repo.index(); - - Object.assign(args.staged, exports.readConflicts(index, options.paths)); - - const headTree = yield headCommit.getTree(); - const diff = yield NodeGit.Diff.treeToIndex(repo, headTree, index); - const changes = yield SubmoduleUtil.getSubmoduleChangesFromDiff(diff, - true); - const indexUrls = - yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); - const headUrls = - yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, headCommit); - - // No paths specified, so we'll do all submodules, restricting to open - // ones based on options. - - let filterPaths; // map from sub name to paths to use - const filtering = 0 !== options.paths.length; - - // Will look at submodules that are open or have changes. TODO: we're - // ignoring changes affecting only the `.gitmodules` file for now. - - let subsToList = Array.from(new Set( - openArray.concat(Object.keys(changes)))); - - if (filtering) { - filterPaths = SubmoduleUtil.resolvePaths(options.paths, - subsToList, - openArray); - subsToList = Object.keys(filterPaths); - } - - // Make a list of promises to read the status for each submodule, then - // evaluate them in parallel. - - const subStatMakers = subsToList.map(co.wrap(function *(name) { - const headUrl = headUrls[name] || null; - const indexUrl = indexUrls[name] || null; - let headSha = null; - let indexSha = null; - let subRepo = null; - - // Load commit information available based on whether the submodule - // was added, removed, changed, or just open. - - const change = changes[name]; - if (undefined !== change) { - headSha = change.oldSha; - indexSha = change.newSha; - } - else { - // Just open, we need to load its sha. Unfortunately, the diff - // we did above doesn't catch new submodules with unstaged - // commits; validate that we have commit and index URLs and - // entries before trying to read them. - - if (null !== headUrl) { - headSha = (yield headTree.entryByPath(name)).sha(); - } - if (null !== indexUrl) { - const indexEntry = index.getByPath(name); - if (undefined !== indexEntry) { - indexSha = indexEntry.id.tostrS(); - } - } - } - let status = null; - if (openSet.has(name)) { - subRepo = yield SubmoduleUtil.getRepo(repo, name); - status = yield exports.getRepoStatus(subRepo, { - paths: filtering ? filterPaths[name] : [], - showAllUntracked: options.showAllUntracked, - ignoreIndex: options.ignoreIndex, - showMetaChanges: true, - }); - } - return yield exports.getSubmoduleStatus(subRepo, - status, - indexUrl, - headUrl, - indexSha, - headSha); - })); - - const subStats = yield subStatMakers; - - // And copy them into the arguments. - - subsToList.forEach((name, i) => { - const subStat = subStats[i]; - if (undefined !== subStat) { - args.submodules[name] = subStats[i]; - } - }); + const {conflicts, submodules} = + yield getSubRepoStatus(repo, options, headCommit); + Object.assign(args.staged, conflicts); + args.submodules = submodules; } return new RepoStatus(args); diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 992ce594c..fc9da47d8 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -1183,6 +1183,9 @@ a=B:Chi#a-1;Ba=a|x=U:C3-2 s=Sa:a;Bmaster=3;Os W README.md=888`, status: new RepoStatus({ currentBranchName: "master", headCommit: "2", + staged: { + ".gitmodules": FILESTATUS.REMOVED + } }), }, }, diff --git a/node/test/util/diff_util.js b/node/test/util/diff_util.js index 8b808ac7e..7465eb86f 100644 --- a/node/test/util/diff_util.js +++ b/node/test/util/diff_util.js @@ -353,6 +353,9 @@ x=S:C2 a/b=c,a/c=d,t=u;H=2;I a/b,a/q=r,f=x;W a/b=q,a/c=f,a/y=g,f`, input: ` a=B:Ca-1;Bmaster=a| x=S:C2-1 s=Sa:1;Bmaster=2`, + staged: { + ".gitmodules": FILESTATUS.ADDED, + }, from: "HEAD^", }, "HEAD^ ignore submodule change": { @@ -365,6 +368,9 @@ x=S:C2-1 s=Sa:1;C3-2 s=Sa:a;Bmaster=3`, input: ` a=B:Ca-1;Bmaster=a| x=S:C2-1 s=Sa:1;C3-2 s;Bmaster=3`, + staged: { + ".gitmodules": FILESTATUS.REMOVED, + }, from: "HEAD^", }, "HEAD^ unmodified": { diff --git a/node/test/util/status_util.js b/node/test/util/status_util.js index f12fec2ba..f2cceaf17 100644 --- a/node/test/util/status_util.js +++ b/node/test/util/status_util.js @@ -711,6 +711,9 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, index: new Index("1", "a", null), }), }, + staged: { + ".gitmodules": FILESTATUS.ADDED, + } }), }, "sub removed from index": { @@ -723,6 +726,9 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, commit: new Commit("1", "a"), }), }, + staged: { + ".gitmodules": FILESTATUS.REMOVED, + } }), }, "sub changed in workdir": { @@ -741,6 +747,9 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, }), RELATION.SAME), }), }, + staged: { + ".gitmodules": FILESTATUS.ADDED, + } }), }, "show root untracked": { @@ -840,6 +849,9 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, expected: new RepoStatus({ headCommit: "1", currentBranchName: "master", + staged: { + ".gitmodules": FILESTATUS.ADDED, + }, submodules: { s: new Submodule({ commit: null, From 48d58fe4bf40821502872ea940e917b096fa9c3b Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Tue, 1 Sep 2020 08:44:46 -0400 Subject: [PATCH 307/402] Cherry-pick command should test if "args.commit" is an empty array --- node/lib/cmd/cherry_pick.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node/lib/cmd/cherry_pick.js b/node/lib/cmd/cherry_pick.js index 3c34dc1cf..7612d33d9 100644 --- a/node/lib/cmd/cherry_pick.js +++ b/node/lib/cmd/cherry_pick.js @@ -148,7 +148,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { } if (args.continue) { - if (null !== args.commit) { + if (args.commit.length) { throw new UserError("Cannot specify a commit with '--continue'."); } const result = yield CherryPickUtil.continue(repo); @@ -159,14 +159,14 @@ exports.executeableSubcommand = co.wrap(function *(args) { } if (args.abort) { - if (null !== args.commit) { + if (args.commit.length) { throw new UserError("Cannot specify a commit with '--abort'."); } yield CherryPickUtil.abort(repo); return; // RETURN } - if (null === args.commit) { + if (!args.commit.length) { throw new UserError("No commit to cherry-pick."); } From bed9d01c705e75fc86be6cb1fd9fc5a4c146018f Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 1 Sep 2020 09:19:06 -0400 Subject: [PATCH 308/402] Do not display a backtrace for hook failure --- node/lib/git-meta.js | 4 +++- node/lib/util/commit.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js index 686b628ba..47a59fb9c 100755 --- a/node/lib/git-meta.js +++ b/node/lib/git-meta.js @@ -99,7 +99,9 @@ function configureSubcommand(parser, commandName, module, skipNodeGit) { // diagnostic message because the stack is irrelevant. if (error instanceof UserError) { - console.error(error.message); + if (error.message) { + console.error(error.message); + } process.exit(error.code); } else { diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index bae5df62f..0a25c2dbe 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -212,7 +212,7 @@ const runHooks = co.wrap(function *(repo, index) { if (!isOk) { // hooks are responsible for printing their own message - throw new Error(""); + throw new UserError(""); } } } finally { From cda00b21611ba1f9ed9558fb91c4796a3878f50a Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Thu, 3 Sep 2020 17:31:58 -0400 Subject: [PATCH 309/402] Change Diff.OPTION.EXCLUDE_SUBMODULE to Diff.OPTION.IGNORE_SUBMODULE --- node/lib/util/diff_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/diff_util.js b/node/lib/util/diff_util.js index 301dc477c..a3a67d8a9 100644 --- a/node/lib/util/diff_util.js +++ b/node/lib/util/diff_util.js @@ -130,7 +130,7 @@ exports.getRepoStatus = co.wrap(function *(repo, const options = { ignoreSubmodules: 1, flags: NodeGit.Diff.OPTION.INCLUDE_UNTRACKED | - NodeGit.Diff.OPTION.EXCLUDE_SUBMODULES, + NodeGit.Diff.OPTION.IGNORE_SUBMODULES, }; if (0 !== paths.length) { options.pathspec = paths; From e10e9d030a71d2a5beee438a959c8ccbe308820e Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Thu, 3 Sep 2020 18:18:50 -0400 Subject: [PATCH 310/402] Fix NodeGit.Reference.TYPE.LISTALL warning --- node/lib/util/checkout.js | 2 +- node/lib/util/syncrefs.js | 2 +- node/yarn.lock | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 87a843b9a..4151601db 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -60,7 +60,7 @@ exports.findTrackingBranch = co.wrap(function *(repo, name) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(name); let result = null; - const refs = yield repo.getReferenceNames(NodeGit.Reference.TYPE.LISTALL); + const refs = yield repo.getReferenceNames(NodeGit.Reference.TYPE.ALL); const matcher = new RegExp(`^refs/remotes/(.*)/${name}$`); for (let i = 0; i < refs.length; ++i) { const refName = refs[i]; diff --git a/node/lib/util/syncrefs.js b/node/lib/util/syncrefs.js index 249f48b39..20095abfe 100644 --- a/node/lib/util/syncrefs.js +++ b/node/lib/util/syncrefs.js @@ -43,6 +43,6 @@ exports.doSyncRefs = co.wrap(function *() { const repo = yield GitUtil.getCurrentRepo(); let subs = yield SubmoduleUtil.listOpenSubmodules(repo); - const refs = yield repo.getReferenceNames(NodeGit.Reference.TYPE.LISTALL); + const refs = yield repo.getReferenceNames(NodeGit.Reference.TYPE.ALL); yield SubmoduleUtil.syncRefs(repo, refs, subs); }); diff --git a/node/yarn.lock b/node/yarn.lock index 0c3550e74..a9b40ed16 100644 --- a/node/yarn.lock +++ b/node/yarn.lock @@ -705,6 +705,13 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +git-range@: + version "0.2.0" + resolved "https://registry.yarnpkg.com/git-range/-/git-range-0.2.0.tgz#dc79f44a261f31aa8bc9e3c06eaedcc10fbf8349" + integrity sha1-3Hn0SiYfMaqLyePAbq7cwQ+/g0k= + dependencies: + lodash.flatten "^4.4.0" + glob@7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" @@ -1058,6 +1065,11 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + lodash@^4.17.11, lodash@~4.17.11: version "4.17.19" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" From 3cea11c57da0907338ad1258e8f65b07fa6b0896 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 9 Sep 2020 22:08:24 -0400 Subject: [PATCH 311/402] optimize isOpen for the common case, to not read all submodule dirs --- node/lib/util/cherry_pick_util.js | 2 +- node/lib/util/open.js | 24 +++++++++++++++++------- node/test/util/open.js | 6 +++--- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index de40a8b4e..15206d5f4 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -120,7 +120,7 @@ exports.changeSubmodules = co.wrap(function *(repo, delete urls[name]; yield rmrf(name); } - else if (yield opener.isOpen(name)) { + else if (opener.isOpen(name)) { const subRepo = yield opener.getSubrepo(name, Open.SUB_OPEN_OPTION.FORCE_OPEN); diff --git a/node/lib/util/open.js b/node/lib/util/open.js index 51e472275..6e2d3663d 100644 --- a/node/lib/util/open.js +++ b/node/lib/util/open.js @@ -35,6 +35,8 @@ const assert = require("chai").assert; const co = require("co"); const colors = require("colors"); +const fs = require("fs-promise"); +const path = require("path"); const NodeGit = require("nodegit"); const GitUtil = require("./git_util"); @@ -123,7 +125,7 @@ exports.openOnCommit = co.wrap(function *(fetcher, const submoduleUrl = yield fetcher.getSubmoduleUrl(submoduleName); - const wasOpen = yield new Opener(metaRepo, null).isOpen(submoduleName); + const wasOpen = new Opener(metaRepo, null).isOpen(submoduleName); // Set up the submodule. @@ -254,12 +256,20 @@ Opener.prototype.clearAbsorbedCache = function (subName) { * @param {String} subName * @return {Boolean} */ -Opener.prototype.isOpen = co.wrap(function *(subName) { - if (!this.d_initialized) { - yield this._initialize(); +Opener.prototype.isOpen = function (subName) { + if (this.d_initialized) { + return this.d_openSubs.has(subName) || (subName in this.d_cachedSubs); + } else { + const modulesDir = path.join(this.d_repo.path(), "modules"); + const submodulePath = path.join(modulesDir, subName); + const headPath = path.join(submodulePath, "HEAD"); + if (!fs.existsSync(headPath)) { + return false; + } + const gitlinkPath = path.join(this.d_repo.workdir(), subName, ".git"); + return fs.existsSync(gitlinkPath); } - return this.d_openSubs.has(subName) || (subName in this.d_cachedSubs); -}); +}; /** * Return true if the submodule is opened nor half opened. @@ -347,7 +357,7 @@ Opener.prototype.getSubrepo = co.wrap(function *(subName, openOption) { return subRepo; } } - const openable = yield this.isOpen(subName); + const openable = this.isOpen(subName); const halfOpenable = yield this.isAtLeastHalfOpen(subName); switch (openOption) { diff --git a/node/test/util/open.js b/node/test/util/open.js index 21109640f..c40e78849 100644 --- a/node/test/util/open.js +++ b/node/test/util/open.js @@ -202,7 +202,7 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos(state); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - const result = yield opener.isOpen("s"); + const result = opener.isOpen("s"); assert.equal(false, result); })); it("isOpen, true after open", co.wrap(function *() { @@ -211,7 +211,7 @@ describe("openOnCommit", function () { const repo = w.repos.x; const opener = new Open.Opener(repo, null); const s = yield opener.getSubrepo("s", FORCE_OPEN); - const result = yield opener.isOpen("s"); + const result = opener.isOpen("s"); assert.equal(true, result); const config = yield s.config(); const gcConfig = yield ConfigUtil.getConfigString(config, @@ -223,7 +223,7 @@ describe("openOnCommit", function () { const w = yield RepoASTTestUtil.createMultiRepos(state); const repo = w.repos.x; const opener = new Open.Opener(repo, null); - const result = yield opener.isOpen("s"); + const result = opener.isOpen("s"); assert.equal(true, result); })); }); From 21658fe83e85551c7d635783f04442cedb3676e4 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 14 Sep 2020 14:46:23 -0400 Subject: [PATCH 312/402] Reject amend commits mid merge/rebase/cherry-pick --- node/lib/cmd/commit.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/node/lib/cmd/commit.js b/node/lib/cmd/commit.js index 30a5cd217..1d1f61f47 100644 --- a/node/lib/cmd/commit.js +++ b/node/lib/cmd/commit.js @@ -133,10 +133,11 @@ const doCommit = co.wrap(function *(args) { }); const doAmend = co.wrap(function *(args) { - const Commit = require("../util/commit"); - const GitUtil = require("../util/git_util"); - const Hook = require("../util/hook"); - const UserError = require("../util/user_error"); + const Commit = require("../util/commit"); + const GitUtil = require("../util/git_util"); + const Hook = require("../util/hook"); + const SequencerStateUtil = require("../util/sequencer_state_util"); + const UserError = require("../util/user_error"); const usingPaths = 0 !== args.file.length; const message = args.message ? args.message.join("\n\n") : null; @@ -148,6 +149,13 @@ const doAmend = co.wrap(function *(args) { const repo = yield GitUtil.getCurrentRepo(); const cwd = process.cwd(); + const seq = yield SequencerStateUtil.readSequencerState(repo.path()); + if (seq) { + const ty = seq.type.toLowerCase(); + const msg = "You are in the middle of a " + ty + " -- cannot amend."; + throw new UserError(msg); + } + yield Commit.doAmendCommand(repo, cwd, message, From 5787c553c4d256b77cb680239a6ec2c386847be3 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 14 Sep 2020 15:54:26 -0400 Subject: [PATCH 313/402] Do not allow partial commits mid-merge/rebase/etc --- node/lib/util/commit.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 0a25c2dbe..763f42feb 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -53,6 +53,7 @@ const Hook = require("../util/hook"); const Open = require("./open"); const RepoStatus = require("./repo_status"); const PrintStatusUtil = require("./print_status_util"); +const SequencerStateUtil = require("../util/sequencer_state_util"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const StatusUtil = require("./status_util"); const Submodule = require("./submodule"); @@ -2170,6 +2171,14 @@ exports.doCommitCommand = co.wrap(function *(repo, if (usingPaths) { checkForPathIncompatibleSubmodules(repoStatus, relCwd); } + if (usingPaths || !all) { + const seq = yield SequencerStateUtil.readSequencerState(repo.path()); + if (seq) { + const ty = seq.type.toLowerCase(); + const msg = "Cannot do a partial commit during a " + ty; + throw new UserError(msg); + } + } // If there is nothing possible to commit, exit early. From 746d683064d6560d171768105f3ed82d81e3d46f Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 16 Sep 2020 12:52:49 -0400 Subject: [PATCH 314/402] make commit --all mid-merge continue the merge --- node/lib/util/commit.js | 45 ++++++++++++++++++++++--- node/test/util/commit.js | 73 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 5 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 763f42feb..44cd28b0d 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -53,6 +53,7 @@ const Hook = require("../util/hook"); const Open = require("./open"); const RepoStatus = require("./repo_status"); const PrintStatusUtil = require("./print_status_util"); +const SequencerState = require("../util/sequencer_state"); const SequencerStateUtil = require("../util/sequencer_state_util"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const StatusUtil = require("./status_util"); @@ -488,6 +489,10 @@ const updateHead = co.wrap(function *(repo, sha) { * @param {RepoStatus} metaStatus * @param {String|null} message * @param {Object} [subMessages] map from submodule to message + * @param {Commit} mergeParent if mid-merge, the commit being + * merged, which will become the right parent + * of this commit (and the equivalent in the + * submodules). * @return {Object} * @return {String|null} return.metaCommit * @return {Object} return.submoduleCommits map submodule name to new commit @@ -497,7 +502,8 @@ exports.commit = co.wrap(function *(metaRepo, metaStatus, message, subMessages, - noVerify) { + noVerify, + mergeParent) { assert.instanceOf(metaRepo, NodeGit.Repository); assert.isBoolean(all); assert.instanceOf(metaStatus, RepoStatus); @@ -513,6 +519,12 @@ exports.commit = co.wrap(function *(metaRepo, "if no meta message, sub messages must be specified"); assert.isBoolean(noVerify); + let mergeTree = null; + if (mergeParent) { + assert.instanceOf(mergeParent, NodeGit.Commit); + mergeTree = yield mergeParent.getTree(); + } + const signature = yield ConfigUtil.defaultSignature(metaRepo); const submodules = metaStatus.submodules; @@ -550,6 +562,13 @@ exports.commit = co.wrap(function *(metaRepo, if (headCommit !== null) { parents.push(headCommit); } + + if (mergeTree !== null) { + const mergeSubParent = yield mergeTree.entryByPath(name); + if (mergeSubParent.id() !== headCommit.id()) { + parents.push(mergeSubParent.id()); + } + } const index = yield subRepo.index(); const tree = yield index.writeTree(); @@ -597,6 +616,11 @@ exports.commit = co.wrap(function *(metaRepo, const tree = yield index.writeTree(); const headCommit = yield metaRepo.getHeadCommit(); + const parents = [headCommit]; + if (mergeParent) { + assert(mergeParent !== headCommit); + parents.push(mergeParent); + } result.metaCommit = yield metaRepo.createCommit( "HEAD", @@ -604,7 +628,11 @@ exports.commit = co.wrap(function *(metaRepo, signature, exports.ensureEolOnLastLine(message), tree, - [headCommit]); + parents); + + if (mergeParent) { + yield SequencerStateUtil.cleanSequencerState(metaRepo.path()); + } return result; }); @@ -2171,8 +2199,9 @@ exports.doCommitCommand = co.wrap(function *(repo, if (usingPaths) { checkForPathIncompatibleSubmodules(repoStatus, relCwd); } - if (usingPaths || !all) { - const seq = yield SequencerStateUtil.readSequencerState(repo.path()); + const seq = yield SequencerStateUtil.readSequencerState(repo.path()); + + if (usingPaths || !all || interactive) { if (seq) { const ty = seq.type.toLowerCase(); const msg = "Cannot do a partial commit during a " + ty; @@ -2180,6 +2209,11 @@ exports.doCommitCommand = co.wrap(function *(repo, } } + let mergeParent = null; + if (seq && seq.type === SequencerState.TYPE.MERGE) { + mergeParent = NodeGit.Commit.lookup(repo, seq.target); + } + // If there is nothing possible to commit, exit early. if (!exports.shouldCommit(repoStatus, false, undefined)) { @@ -2235,7 +2269,8 @@ exports.doCommitCommand = co.wrap(function *(repo, repoStatus, message, subMessages, - noVerify); + noVerify, + mergeParent); } }); diff --git a/node/test/util/commit.js b/node/test/util/commit.js index fc9da47d8..af671318e 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -3385,3 +3385,76 @@ x=U:Cmsg\n#x-2 s=Sa:s;Bmaster=x;Os Cmsg\n#s-1 README.md=foo,addedbyhook=bar`; }); }); + +describe("commit mid-merge", function() { + it("handles a commit mid-merge", co.wrap(function*() { + const MergeUtil = require("../../lib/util/merge_util"); + const MergeCommon = require("../../lib/util/merge_common"); + const Open = require("../../lib/util/open"); + const SeqStateUtil = require("../../lib/util/sequencer_state_util"); + + const start = `a=B:C2-1 a=a1; + C3-2 a=a3; + C4-2 a=a4; + Bb3=3;Bb4=4;| + x=S:Cm2-1 s=Sa:2; + Cm3-m2 s=Sa:3; + Cm4-m2 s=Sa:4; + Bmaster=m3;Bm4=m4;`; + const repoMap = yield RepoASTTestUtil.createMultiRepos(start); + const repo = repoMap.repos.x; + + const m4 = yield NodeGit.Commit.lookup(repo, + repoMap.reverseCommitMap.m4); + try { + yield MergeUtil.merge(repo, + null, + m4, + MergeCommon.MODE.NORMAL, + Open.SUB_OPEN_OPTION.FORCE_OPEN, + [], + "msg", + function() {}); + } catch (e) { + // we expect the merge to fail, since we have a conflict + // and we want to test that git meta commit in the middle + // of merge conflict resolution continues the merge. + } + + const sRepo = yield NodeGit.Repository.open(repo.workdir() + "s"); + const index = yield sRepo.index(); + const sEntry = index.getByIndex(1); + const newEntry = new NodeGit.IndexEntry(); + newEntry.flags = sEntry.flags; + newEntry.flagsExtended = sEntry.flagsExtended; + newEntry.mode = sEntry.mode; + newEntry.id = sEntry.id; + newEntry.path = sEntry.path; + + yield index.conflictCleanup(); + yield index.add(newEntry); + yield index.write(); + + const repoStatus = yield Commit.getCommitStatus(repo, repo.path(), { + all: true, + paths: [], + }); + + yield Commit.commit(repo, true, repoStatus, "commit message", + undefined, true, m4); + + const head = yield repo.getHeadCommit(); + const left = yield head.parent(0); + const right = yield head.parent(1); + assert.equal(left.id().tostrS(), repoMap.reverseCommitMap.m3); + assert.equal(right.id().tostrS(), repoMap.reverseCommitMap.m4); + const tree = yield head.getTree(); + const subEntry = yield tree.entryByPath("s"); + const subCommit = yield NodeGit.Commit.lookup(sRepo, + subEntry.oid()); + assert.equal(2, subCommit.parentcount()); + const seq = yield SeqStateUtil.readSequencerState(repo.path()); + assert.isNull(seq); + + })); +}); From 2f7a0964e94181c72f9b1dc7811424b46dc7e7c9 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 22 Sep 2020 14:12:38 -0400 Subject: [PATCH 315/402] Use git instead of node-git to determine what commit's submodules need pushing --- node/lib/util/push.js | 83 ++++++++++++++++++++++-------------------- node/test/util/push.js | 59 ++++++++++-------------------- 2 files changed, 63 insertions(+), 79 deletions(-) diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 199bacf89..3a04fa0fa 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -34,10 +34,11 @@ * This module contains methods for pushing. */ -const assert = require("chai").assert; -const co = require("co"); -const colors = require("colors"); -const NodeGit = require("nodegit"); +const assert = require("chai").assert; +const ChildProcess = require("child-process-promise"); +const co = require("co"); +const colors = require("colors"); +const NodeGit = require("nodegit"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); @@ -51,10 +52,9 @@ const UserError = require("./user_error"); const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; /** - * For a given commit and remote, determine a reasonably close oid that has - * already been pushed. This does *not* do a fetch, but rather just compares - * against all available remote refs. To compare against all remotes, simply - * pass a "*" wildcard. + * For a given commit, determine a reasonably close oid that has + * already been pushed. This does *not* do a fetch, but rather just + * compares against all available remote refs. * * This works by building a revwalk and removing all ancestors of remote refs * for the given remote. It then reverses the result, and returns the first @@ -66,38 +66,46 @@ const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; * * @async * @param {NodeGit.Repository} repo - * @param {String} remoteName * @param {NodeGit.Commit} commit */ -exports.getClosePushedCommit = co.wrap(function*(repo, remoteName, commit) { +exports.getClosePushedCommit = co.wrap(function*(repo, commit) { assert.instanceOf(repo, NodeGit.Repository); - assert.isString(remoteName); assert.instanceOf(commit, NodeGit.Commit); // Search for the first commit that is not an ancestor of a remote ref, ie. - // the first commit that is unpushed. First by topological, then resolving - // by timestamp. - - const walk = NodeGit.Revwalk.create(repo); - walk.sorting(NodeGit.Revwalk.SORT.TOPOLOGICAL, - NodeGit.Revwalk.SORT.TIME, - NodeGit.Revwalk.SORT.REVERSE); - walk.hideGlob(`refs/remotes/${remoteName}`); - walk.push(commit); - - // This occurs when walk.next() has no commits left -- in this case, - // when there are no unpushed commits. Return the passed commit as the - // last pushed commit. - - let firstNewOid; - try { - firstNewOid = yield walk.next(); - } catch (err) { - if (NodeGit.Error.CODE.ITEROVER === err.errno) { - return commit; + // the first commit that is unpushed. NodeGit Revwalk is too slow, so + // we shell out. + + // First, get the list of remote refs to exclude + const excludeRefs = []; + for (const ref of (yield NodeGit.Reference.list(repo))) { + if (ref.startsWith("refs/remotes/")) { + excludeRefs.push("^" + ref); } - throw err; } + if (excludeRefs.length === 0) { + return null; + } + + const execString = `\ +git -C '${repo.workdir()}' rev-list --reverse --topo-order ${commit} \ +${excludeRefs.join(" ")}`; + let result = yield ChildProcess.exec(execString); + if (result.error) { + throw new Error( + `Unexpected error figuring out what to push: +stderr: +{result.stderr} +stdout: +{result.stdout}`); + } + const out = result.stdout; + if (!out) { + // Nothing new to push + return commit; + } + + const firstNewOid = out.substring(0, out.indexOf("\n")); const firstNewCommit = yield repo.getCommit(firstNewOid); @@ -120,19 +128,15 @@ exports.getClosePushedCommit = co.wrap(function*(repo, remoteName, commit) { * * @async * @param {NodeGit.Repository} repo - * @param {String} remoteName * @param {String} source * @param {NodeGit.Commit} commit */ -exports.getPushMap = co.wrap(function*(repo, remoteName, source, - commit) { +exports.getPushMap = co.wrap(function*(repo, source, commit) { assert.instanceOf(repo, NodeGit.Repository); - assert.isString(remoteName); assert.isString(source); assert.instanceOf(commit, NodeGit.Commit); - const baseCommit = yield exports.getClosePushedCommit(repo, remoteName, - commit); + const baseCommit = yield exports.getClosePushedCommit(repo, commit); let baseTree; if (null !== baseCommit) { baseTree = yield baseCommit.getTree(); @@ -222,8 +226,7 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { const commit = yield repo.getCommit(sha); // First, push the submodules. - const pushMap = yield exports.getPushMap(repo, `*`, source, - commit); + const pushMap = yield exports.getPushMap(repo, source, commit); let errorMessage = ""; diff --git a/node/test/util/push.js b/node/test/util/push.js index da14db4b7..113ca9e7e 100644 --- a/node/test/util/push.js +++ b/node/test/util/push.js @@ -299,8 +299,7 @@ x=S:B3=3;C2-1 s=Sa:1;Rorigin=a master=3;Rtarget=b;Bmaster=2 origin/master;Os`, yield SubmoduleConfigUtil.deinit(repo, subs); } - const pushMap = yield Push.getPushMap(repo, "origin", source, - commit); + const pushMap = yield Push.getPushMap(repo, source, commit); const mappedPushMap = {}; for (const sub of Object.keys(pushMap)) { mappedPushMap[sub] = commitMap.commitMap[pushMap[sub]]; @@ -353,8 +352,7 @@ x=S:B3=3;C2-1 s=Sa:1;Rorigin=a master=3;Rtarget=b;Bmaster=2 origin/master;Os`, const sha = theirCommitMap["2"]; const commit = yield ourRepo.getCommit(sha); - const pushMap = yield Push.getPushMap(ourRepo, "upstream", "2", - commit); + const pushMap = yield Push.getPushMap(ourRepo, "2", commit); assert.deepEqual({}, pushMap); })); @@ -374,8 +372,7 @@ x=S:B3=3;C2-1 s=Sa:1;Rorigin=a master=3;Rtarget=b;Bmaster=2 origin/master;Os`, const sha = ourCommitMap["3"]; const commit = yield ourRepo.getCommit(sha); - const pushMap = yield Push.getPushMap(ourRepo, "upstream", "2", - commit); + const pushMap = yield Push.getPushMap(ourRepo, "2", commit); assert.deepEqual({"d": ourCommitMap["7"]}, pushMap); // "Delete" the codebase, but remain absorbed. @@ -383,8 +380,7 @@ x=S:B3=3;C2-1 s=Sa:1;Rorigin=a master=3;Rtarget=b;Bmaster=2 origin/master;Os`, return rimraf(ourWritten.repos.x.workdir() + "d", {}, callback); })); - const pushMapDel = yield Push.getPushMap(ourRepo, "upstream", "2", - commit); + const pushMapDel = yield Push.getPushMap(ourRepo, "2", commit); assert.deepEqual({"d": ourCommitMap["7"]}, pushMapDel); // Remove the absorbed codebase. @@ -392,8 +388,7 @@ x=S:B3=3;C2-1 s=Sa:1;Rorigin=a master=3;Rtarget=b;Bmaster=2 origin/master;Os`, return rimraf(ourWritten.repos.x.path() + "modules/d", {}, callback); })); - const pushMapRm = yield Push.getPushMap(ourRepo, "upstream", "2", - commit); + const pushMapRm = yield Push.getPushMap(ourRepo, "2", commit); assert.deepEqual({}, pushMapRm); })); }); @@ -515,39 +510,28 @@ describe("getClosePushedCommit", function () { const baseRepo = "x=S:C2-1;C3-1;C4-3,2;C5-4;Bmaster=5"; const cases = { - "no refs, no close pushed commit": { + "no ref, no close pushed commit": { source: "5", - remote: "origin", refs: { }, expectedCommit: null, }, - "no refs, with wildcard": { + "one ref, commit is pushed": { source: "5", - remote: "*", - refs: { - }, - expectedCommit: null, - }, - "one refs, commit is pushed": { - source: "5", - remote: "*", refs: { "refs/remotes/origin/1": "5", }, expectedCommit: "5", }, - "one refs, far commit": { + "one ref, far commit": { source: "5", - remote: "*", refs: { "refs/remotes/origin/1": "1", }, expectedCommit: "1", }, - "one refs, close commit": { + "one ref, close commit": { source: "5", - remote: "*", refs: { "refs/remotes/origin/1": "2", }, @@ -557,38 +541,35 @@ describe("getClosePushedCommit", function () { source: "5", remote: "origin", refs: { - "refs/remotes/foo/1": "1", - "refs/remotes/foo/2": "2", - "refs/remotes/bar/3": "3", - "refs/remotes/bar/4": "4", + "refs/nonmatch/foo/1": "1", + "refs/nonmatch/foo/2": "2", + "refs/nonmatch/bar/3": "3", + "refs/nonmatch/bar/4": "4", }, expectedCommit: null, }, "lots of refs, something matching": { source: "5", - remote: "origin", refs: { "refs/remotes/origin/1": "1", - "refs/remotes/foo/2": "2", - "refs/remotes/bar/3": "3", - "refs/remotes/bar/4": "4", + "refs/nonmatch/foo/2": "2", + "refs/nonmatch/bar/3": "3", + "refs/nonmatch/bar/4": "4", }, expectedCommit: "1", }, "lots of refs, something matching 2": { source: "5", - remote: "origin", refs: { - "refs/remotes/foo/1": "1", - "refs/remotes/foo/2": "2", - "refs/remotes/bar/3": "3", + "refs/nonmatch/foo/1": "1", + "refs/nonmatch/foo/2": "2", + "refs/nonmatch/bar/3": "3", "refs/remotes/origin/4": "4", }, expectedCommit: "4", }, "lots of refs, all matching": { source: "5", - remote: "*", refs: { "refs/remotes/origin/1": "1", "refs/remotes/origin/2": "2", @@ -624,7 +605,7 @@ describe("getClosePushedCommit", function () { const commit = yield repo.getCommit( repos.reverseCommitMap[c.source]); const actualCommit = yield Push.getClosePushedCommit( - repo, c.remote, commit); + repo, commit); if (null !== c.expectedCommit) { assert.deepEqual(repos.reverseCommitMap[c.expectedCommit], actualCommit.id().tostrS()); From 00eef5f3c5fae32b2e341f58b4362e796e5ea5bd Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 24 Sep 2020 14:39:03 -0400 Subject: [PATCH 316/402] do not shell out unnecessarily (which should eliminate E2BIG) --- node/lib/util/push.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 3a04fa0fa..160c92503 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -87,10 +87,9 @@ exports.getClosePushedCommit = co.wrap(function*(repo, commit) { return null; } - const execString = `\ -git -C '${repo.workdir()}' rev-list --reverse --topo-order ${commit} \ -${excludeRefs.join(" ")}`; - let result = yield ChildProcess.exec(execString); + const args = ["-C", repo.workdir(), "rev-list", "--reverse", + "--topo-order", commit, ...excludeRefs]; + let result = yield ChildProcess.execFile("git", args); if (result.error) { throw new Error( `Unexpected error figuring out what to push: From 0269a4f8fce21a5057ed3048c0796c9f0e613924 Mon Sep 17 00:00:00 2001 From: Alex Sturm Date: Fri, 25 Sep 2020 11:45:38 +0100 Subject: [PATCH 317/402] Fix generate-repo.js. --- node/lib/generate-repo.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/node/lib/generate-repo.js b/node/lib/generate-repo.js index 814e7c744..505a5e51f 100755 --- a/node/lib/generate-repo.js +++ b/node/lib/generate-repo.js @@ -153,8 +153,8 @@ function makeSubCommits(state, name, madeShas) { const numChanges = randomInt(2) + 1; for (let j = 0; j < numChanges; ++j) { const pathToUpdate = paths[randomInt(paths.length)]; - changes[pathToUpdate] = - state.nextCommitId + generateCharacter(); + changes[pathToUpdate] = new RepoAST.File( + state.nextCommitId + generateCharacter(), false); } } @@ -162,7 +162,8 @@ function makeSubCommits(state, name, madeShas) { if (undefined === lastHead || 0 === randomInt(6)) { const path = generatePath(randomInt(3) + 1); - changes[path] = state.nextCommitId + generateCharacter(); + changes[path] = new RepoAST.File( + state.nextCommitId + generateCharacter(), false); } const parents = undefined === lastHead ? [] : [lastHead]; lastHead = newHead; From d068db58834d616d9c0cb7bc1957881fe2892fe2 Mon Sep 17 00:00:00 2001 From: tsuikevi-twosigma Date: Fri, 25 Sep 2020 16:24:17 +0100 Subject: [PATCH 318/402] avoid exceptions from calling git-meta checkout on unopened submodules --- node/lib/util/checkout.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 4151601db..451894bc8 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -601,7 +601,11 @@ function noSuchFileMessage(subName, path) { exports.checkoutFiles = co.wrap(function*(repo, options) { assert.instanceOf(repo, NodeGit.Repository); const resolvedPaths = options.resolvedPaths; - const subNames = Object.keys(resolvedPaths); + + // Exception is thrown if we try to get repo info from unopened submodules + const openSubmodules = yield SubmoduleUtil.listOpenSubmodules(repo); + const subNames = Object.keys(resolvedPaths).filter( + subName => openSubmodules.includes(subName)); let subCommits; let stage = 0; From e0213ff8d70b7169bcdc786b84a972f7c4c25c2b Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 25 Sep 2020 13:33:52 -0400 Subject: [PATCH 319/402] adjust maxbuffer for rev-list --- node/lib/util/push.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 160c92503..0393584b8 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -89,7 +89,9 @@ exports.getClosePushedCommit = co.wrap(function*(repo, commit) { const args = ["-C", repo.workdir(), "rev-list", "--reverse", "--topo-order", commit, ...excludeRefs]; - let result = yield ChildProcess.execFile("git", args); + let result = yield ChildProcess.execFile("git", args, { + maxBuffer: 1024*1024*100 + }); if (result.error) { throw new Error( `Unexpected error figuring out what to push: From 1f283798c6e4369d74a849ef54e6a67e522fb63d Mon Sep 17 00:00:00 2001 From: kaaniboy Date: Fri, 25 Sep 2020 10:41:50 -0700 Subject: [PATCH 320/402] Show error when committing with -i and -a flags --- node/lib/cmd/commit.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/node/lib/cmd/commit.js b/node/lib/cmd/commit.js index 1d1f61f47..dbbd5631d 100644 --- a/node/lib/cmd/commit.js +++ b/node/lib/cmd/commit.js @@ -192,6 +192,10 @@ exports.executeableSubcommand = function (args) { console.error("The use of '-i' and '-m' does not make sense."); process.exit(1); } + if (args.all && args.interactive) { + console.error("The use of '-i' and '-a' does not make sense."); + process.exit(1); + } if (args.amend) { return doAmend(args); // RETURN } From f245ec89b3c5f6ca2d07da6d468cce6fdd2007ec Mon Sep 17 00:00:00 2001 From: kaaniboy Date: Fri, 25 Sep 2020 12:02:46 -0700 Subject: [PATCH 321/402] Fix formatting --- node/lib/cmd/commit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/lib/cmd/commit.js b/node/lib/cmd/commit.js index dbbd5631d..5f1be76a3 100644 --- a/node/lib/cmd/commit.js +++ b/node/lib/cmd/commit.js @@ -193,8 +193,8 @@ exports.executeableSubcommand = function (args) { process.exit(1); } if (args.all && args.interactive) { - console.error("The use of '-i' and '-a' does not make sense."); - process.exit(1); + console.error("The use of '-i' and '-a' does not make sense."); + process.exit(1); } if (args.amend) { return doAmend(args); // RETURN From 9fa9d5d70a63821ffa07f6e6be342c90529e45dc Mon Sep 17 00:00:00 2001 From: kaaniboy Date: Fri, 25 Sep 2020 15:11:44 -0700 Subject: [PATCH 322/402] Add submodule foreach by forwarding to vanilla git --- node/lib/cmd/submodule.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/node/lib/cmd/submodule.js b/node/lib/cmd/submodule.js index dc583d45f..e3554b9b4 100644 --- a/node/lib/cmd/submodule.js +++ b/node/lib/cmd/submodule.js @@ -31,6 +31,7 @@ "use strict"; const co = require("co"); +const Forward = require("./forward"); /** * This module contains the command entry point for direct interactions with @@ -120,6 +121,36 @@ ancestor of M (or M itself) that references S (or a descendant of S)`, required: false, defaultValue: "HEAD", }); + + const foreachParser = subParsers.addParser("foreach", { + help: "evaluate a shell command in each open sub-repo", + description: `\ +Evaluate a shell command in each open sub-repo. Any sub-repo in the \ +meta-repo that is not opened is ignored by this command.`, + }); + + // The foreach command is forwarded to vanilla git. + // These arguments exist solely to populate the --help section. + foreachParser.addArgument(["foreach-command"], { + help: "shell command to execute for each open sub-repo", + type: "string", + }); + + foreachParser.addArgument(["--recursive"], { + help: "sub-repos are traversed recursively", + defaultValue: false, + required: false, + action: "storeConst", + constant: true, + }); + + foreachParser.addArgument(["--quiet"], { + help: "only print error messages", + defaultValue: false, + required: false, + action: "storeConst", + constant: true, + }); }; const doStatusCommand = co.wrap(function *(paths, verbose) { @@ -278,6 +309,11 @@ meta-repo.`); } }); +const doForeachCommand = co.wrap(function *() { + const args = process.argv.slice(3); + yield Forward.execute("submodule", args); +}); + /** * Execute the `submodule` command according to the specified `args`. * @@ -304,6 +340,9 @@ Also, the paths argument is deprecated. Exiting with no change.`); } return SyncRefs.doSyncRefs(); } + else if ("foreach" === args.command) { + return doForeachCommand(); + } return doFindCommand(args.path, args.meta_committish, args["submodule committish"]); From 49852dce63c9aa3aa44f08137b8718d1a3cc472f Mon Sep 17 00:00:00 2001 From: tsuikevi-twosigma Date: Mon, 28 Sep 2020 15:41:47 +0100 Subject: [PATCH 323/402] only filter out submodules when checking out files if no commit is specified --- node/lib/util/checkout.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/node/lib/util/checkout.js b/node/lib/util/checkout.js index 451894bc8..66854adfc 100644 --- a/node/lib/util/checkout.js +++ b/node/lib/util/checkout.js @@ -602,10 +602,13 @@ exports.checkoutFiles = co.wrap(function*(repo, options) { assert.instanceOf(repo, NodeGit.Repository); const resolvedPaths = options.resolvedPaths; - // Exception is thrown if we try to get repo info from unopened submodules + // Exception is thrown if we try to get repo info from unopened submodules. + // TODO: handle other use cases besides when a commit is not specified. const openSubmodules = yield SubmoduleUtil.listOpenSubmodules(repo); - const subNames = Object.keys(resolvedPaths).filter( - subName => openSubmodules.includes(subName)); + const submodules = Object.keys(resolvedPaths); + const subNames = null === options.commit ? + submodules.filter(submodule => openSubmodules.includes(submodule)) : + submodules; let subCommits; let stage = 0; From d17a6e2d75cbba9c42233844abc031da1a5d4474 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 28 Sep 2020 12:58:38 -0400 Subject: [PATCH 324/402] Add mutex for commit hooks to serialize execution (since hooks might write to stdout) --- node/lib/util/commit.js | 8 ++++++++ node/package.json | 1 + 2 files changed, 9 insertions(+) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 44cd28b0d..b2ecc3caa 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -50,6 +50,7 @@ const DoWorkQueue = require("../util/do_work_queue"); const DiffUtil = require("./diff_util"); const GitUtil = require("./git_util"); const Hook = require("../util/hook"); +const Mutex = require("async-mutex").Mutex; const Open = require("./open"); const RepoStatus = require("./repo_status"); const PrintStatusUtil = require("./print_status_util"); @@ -198,6 +199,8 @@ exports.stageChange = co.wrap(function *(index, path, change) { } }); +const mutex = new Mutex(); + const runHooks = co.wrap(function *(repo, index) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(index, NodeGit.Index); @@ -206,8 +209,10 @@ const runHooks = co.wrap(function *(repo, index) { yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index, tempIndexPath); + let release = null; try { if (Hook.hasHook(repo, "pre-commit")) { + release = yield mutex.acquire(); const isOk = yield Hook.execHook(repo, "pre-commit", [], { GIT_INDEX_FILE: tempIndexPath}); yield GitUtil.overwriteIndexFromFile(index, tempIndexPath); @@ -218,6 +223,9 @@ const runHooks = co.wrap(function *(repo, index) { } } } finally { + if (release !== null) { + release(); + } yield fs.unlink(tempIndexPath); } }); diff --git a/node/package.json b/node/package.json index 4b0b0293d..9b8ce9d7c 100644 --- a/node/package.json +++ b/node/package.json @@ -28,6 +28,7 @@ "homepage": "https://github.com/twosigma/git-meta#readme", "dependencies": { "argparse": "^1.0.0", + "async-mutex": "^0.2.4", "binary-search": "", "chai": "", "child-process-promise": "", From ae11e99039eeee9e2921c040cd13cee56a76c99a Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 28 Sep 2020 14:14:51 -0400 Subject: [PATCH 325/402] rename function --- node/lib/util/commit.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index b2ecc3caa..0b2546a18 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -201,7 +201,7 @@ exports.stageChange = co.wrap(function *(index, path, change) { const mutex = new Mutex(); -const runHooks = co.wrap(function *(repo, index) { +const runPreCommitHook = co.wrap(function *(repo, index) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(index, NodeGit.Index); @@ -264,7 +264,7 @@ const prepareIndexAndRunHooks = co.wrap(function *(repo, } if (!noVerify) { - yield runHooks(repo, index); + yield runPreCommitHook(repo, index); } }); @@ -619,7 +619,7 @@ exports.commit = co.wrap(function *(metaRepo, yield stageOpenSubmodules(metaRepo, index, submodules); if (!noVerify) { - yield runHooks(metaRepo, index); + yield runPreCommitHook(metaRepo, index); } const tree = yield index.writeTree(); @@ -739,7 +739,7 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message, noVerify) { } if (!noVerify) { - yield runHooks(repo, index); + yield runPreCommitHook(repo, index); } // Use 'TreeUtil' to create a new tree having the required paths. @@ -1370,7 +1370,7 @@ exports.amendMetaRepo = co.wrap(function *(repo, } } if (!noVerify) { - yield runHooks(subRepo, subIndex); + yield runPreCommitHook(subRepo, subIndex); } } subCommits[subName] = yield exports.createAmendCommit(subRepo, @@ -1393,7 +1393,7 @@ exports.amendMetaRepo = co.wrap(function *(repo, } if (!noVerify) { - yield runHooks(subRepo, subIndex); + yield runPreCommitHook(subRepo, subIndex); } const tree = yield subIndex.writeTree(); const commit = yield subRepo.createCommit( From b02d26993429555f43996c079e51bb75999766a0 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 28 Sep 2020 14:15:01 -0400 Subject: [PATCH 326/402] fix indent --- node/lib/util/commit.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 0b2546a18..5119348fa 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -1379,18 +1379,18 @@ exports.amendMetaRepo = co.wrap(function *(repo, } if (all) { - const actualStatus = yield StatusUtil.getRepoStatus(subRepo, { - showMetaChanges: true, - }); + const actualStatus = yield StatusUtil.getRepoStatus(subRepo, { + showMetaChanges: true, + }); - const workdir = actualStatus.workdir; - for (let path in actualStatus.workdir) { - const change = workdir[path]; - if (RepoStatus.FILESTATUS.ADDED !== change) { - yield exports.stageChange(subIndex, path, change); + const workdir = actualStatus.workdir; + for (let path in actualStatus.workdir) { + const change = workdir[path]; + if (RepoStatus.FILESTATUS.ADDED !== change) { + yield exports.stageChange(subIndex, path, change); + } } } - } if (!noVerify) { yield runPreCommitHook(subRepo, subIndex); From 1ea06e802bf5cc426333c996d1fc2190639639be Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 29 Sep 2020 16:12:03 -0400 Subject: [PATCH 327/402] Support commit-msg hook --- node/lib/cmd/commit.js | 6 +- node/lib/util/commit.js | 44 +++++++-------- node/lib/util/git_util.js | 32 ++++++++--- node/test/util/commit.js | 112 +++++++++++++++++++++---------------- node/test/util/git_util.js | 3 +- 5 files changed, 113 insertions(+), 84 deletions(-) diff --git a/node/lib/cmd/commit.js b/node/lib/cmd/commit.js index 1d1f61f47..d3908ce4e 100644 --- a/node/lib/cmd/commit.js +++ b/node/lib/cmd/commit.js @@ -128,7 +128,7 @@ const doCommit = co.wrap(function *(args) { args.file, args.interactive, args.no_verify, - GitUtil.editMessage); + true); yield Hook.execHook(repo, "post-commit"); }); @@ -161,8 +161,8 @@ const doAmend = co.wrap(function *(args) { message, args.all, args.interactive, - args.no_edit ? null : GitUtil.editMessage, - args.no_verify); + args.no_verify, + !args.no_edit); yield Hook.execHook(repo, "post-commit"); }); diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 5119348fa..3b2f28c62 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -2135,8 +2135,7 @@ function abortForNoMessage() { * modified but unstaged changes. If `paths` is non-empty, include only the * files indicated in those `paths` in the commit. If the specified * `interactive` is true, prompt the user to create an "interactive" message, - * allowing for different commit messages for each changed submodules. Use the - * specified `editMessage` function to invoke an editor when needed. The + * allowing for different commit messages for each changed submodules. The * behavior is undefined if `null !== message && true === interactive` or if `0 * !== paths.length && all`. * @@ -2147,7 +2146,8 @@ function abortForNoMessage() { * @param {Boolean} all * @param {String[]} paths * @param {Boolean} interactive - * @param {(repo, txt) -> Promise(String)} editMessage + * @param {Boolean} noVerify + * @param {Boolean} editMessage * @return {Object} * @return {String} return.metaCommit * @return {Object} return.submoduleCommits map from sub name to commit id @@ -2169,7 +2169,7 @@ exports.doCommitCommand = co.wrap(function *(repo, assert.isArray(paths); assert.isBoolean(interactive); assert.isBoolean(noVerify); - assert.isFunction(editMessage); + assert.isBoolean(editMessage); const workdir = repo.workdir(); const relCwd = path.relative(workdir, cwd); @@ -2241,7 +2241,8 @@ exports.doCommitCommand = co.wrap(function *(repo, sig, null, {}); - const userText = yield editMessage(repo, prompt); + const userText = yield GitUtil.editMessage(repo, prompt, editMessage, + !noVerify); const userData = exports.parseSplitCommitMessages(userText); message = userData.metaMessage; subMessages = userData.subMessages; @@ -2257,7 +2258,8 @@ exports.doCommitCommand = co.wrap(function *(repo, // If no message on the command line, prompt for one. const initialMessage = exports.formatEditorPrompt(repoStatus, cwd); - message = yield editMessage(repo, initialMessage); + message = yield GitUtil.editMessage(repo, initialMessage, editMessage, + !noVerify); } message = GitUtil.stripMessage(message); @@ -2291,7 +2293,7 @@ exports.doCommitCommand = co.wrap(function *(repo, * prompt the user to create an "interactive" message, allowing for different * commit messages for each changed submodules. Use the specified * `editMessage` function to invoke an editor when needed. If - * `null === editMessage`, use the message of the previous commit. The + * `!editMessage`, use the message of the previous commit. The * behavior is undefined if `null !== message && true === interactive`. Do not * generate a commit if it would be empty. * @@ -2300,7 +2302,8 @@ exports.doCommitCommand = co.wrap(function *(repo, * @param {String|null} message * @param {Boolean} all * @param {Boolean} interactive - * @param {(repo, txt) -> Promise(String) | null} editMessage + * @param {Boolean} noVerify + * @param {Boolean} editMessage * @return {Object} * @return {String|null} return.metaCommit * @return {Object} return.submoduleCommits map from sub name to commit id @@ -2310,8 +2313,8 @@ exports.doAmendCommand = co.wrap(function *(repo, message, all, interactive, - editMessage, - noVerify) { + noVerify, + editMessage) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(cwd); if (null !== message) { @@ -2319,9 +2322,7 @@ exports.doAmendCommand = co.wrap(function *(repo, } assert.isBoolean(all); assert.isBoolean(interactive); - if (null !== editMessage) { - assert.isFunction(editMessage); - } + assert.isBoolean(editMessage); const workdir = repo.workdir(); const relCwd = path.relative(workdir, cwd); @@ -2345,7 +2346,8 @@ exports.doAmendCommand = co.wrap(function *(repo, defaultSig, headMeta, subsToAmend); - const userText = yield editMessage(repo, prompt); + const userText = yield GitUtil.editMessage(repo, prompt, editMessage, + !noVerify); const userData = exports.parseSplitCommitMessages(userText); message = userData.metaMessage; subMessages = userData.subMessages; @@ -2370,21 +2372,15 @@ repository independently.`; throw new UserError(error); } - - if (null === editMessage) { - // If no `editMessage` function, use the message of the previous - // commit. - - message = head.message(); - } if (null === message) { - // If no message, use editor. - + // If no message, use editor / hooks. const prompt = exports.formatAmendEditorPrompt(defaultSig, headMeta, status, relCwd); - const rawMessage = yield editMessage(repo, prompt); + const rawMessage = yield GitUtil.editMessage(repo, prompt, + editMessage, + !noVerify); message = GitUtil.stripMessage(rawMessage); } } diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index d82c4c388..333561ed6 100755 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -761,19 +761,33 @@ exports.getEditorCommand = co.wrap(function *(repo) { * @param {String} initialContents * @return {String} */ -exports.editMessage = co.wrap(function *(repo, initialContents) { +exports.editMessage = co.wrap(function *(repo, initialContents, + runEditor, runHooks) { const messagePath = path.join(repo.path(), "COMMIT_EDITMSG"); yield fs.writeFile(messagePath, initialContents); - const editorCommand = yield exports.getEditorCommand(repo); + if (runEditor) { + const editorCommand = yield exports.getEditorCommand(repo); - // TODO: if we ever need this to work on Windows, we'll need to do - // something else. The `ChildProcess.exec` method doesn't provide for a - // way to auto-redirect stdio or I'd use it. + // TODO: if we ever need this to work on Windows, we'll need to do + // something else. The `ChildProcess.exec` method doesn't provide for a + // way to auto-redirect stdio or I'd use it. + + yield ChildProcess.spawn("/bin/sh", + ["-c", `${editorCommand} '${messagePath}'`], { + stdio: "inherit", + }); + + } + + if (runHooks && Hook.hasHook(repo, "commit-msg")) { + const isOk = yield Hook.execHook(repo, "commit-msg", [messagePath]); + + if (!isOk) { + // hooks are responsible for printing their own message + throw new UserError(""); + } + } - yield ChildProcess.spawn("/bin/sh", - ["-c", `${editorCommand} '${messagePath}'`], { - stdio: "inherit", - }); return yield fs.readFile(messagePath, "utf8"); }); diff --git a/node/test/util/commit.js b/node/test/util/commit.js index af671318e..f8fb725c9 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -3124,31 +3124,41 @@ x=U:Cfoo\n#x-2 s=Sa:s;Os Cbar\n#s-1 a=b!H=s;Bmaster=x`, Object.keys(cases).forEach(caseName => { const c = cases[caseName]; const doCommit = co.wrap(function *(repos) { - const repo = repos.x; - let cwd = ""; - if (undefined !== c.cwd) { - cwd = path.join(repo.workdir(), c.cwd); - } - else { - cwd = repo.workdir(); - } - const editor = c.editor || (() => { - assert(false, "no editor"); - }); - const result = yield Commit.doCommitCommand( - repo, - cwd, - c.message || null, - c.all || false, - c.paths || [], - c.interactive || false, - false, - editor); - if (undefined !== result) { - return { - commitMap: mapCommitResult(result), + const old = GitUtil.editMessage; + try { + const repo = repos.x; + let cwd = ""; + if (undefined !== c.cwd) { + cwd = path.join(repo.workdir(), c.cwd); + } + else { + cwd = repo.workdir(); + } + if (c.editor) { + GitUtil.editMessage = c.editor; + } else { + GitUtil.editMessage = () => { + assert(false, "no editor"); }; } + + const result = yield Commit.doCommitCommand( + repo, + cwd, + c.message || null, + c.all || false, + c.paths || [], + c.interactive || false, + false, + true); + if (undefined !== result) { + return { + commitMap: mapCommitResult(result), + }; + } + } finally { + GitUtil.editMessage = old; + } }); it(caseName, co.wrap(function *() { yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, @@ -3263,34 +3273,42 @@ a=B:Cbar\n#a-1 README.md=foo;Bmaster=a| x=U:Cbar\n#3-2 s=Sa:a;Bmaster=3;Os I a=b`, expected: ` x=U:Cbar\n#x-2 s=Sa:s;Bmaster=x;Os Cbar\n#s-1 README.md=foo, a=b`, - editor: null, + editor: null }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; const doAmend = co.wrap(function *(repos) { - const repo = repos.x; - let cwd = ""; - if (undefined !== c.cwd) { - cwd = path.join(repo.workdir(), c.cwd); - } - else { - cwd = repo.workdir(); - } - let editor = c.editor; - if (undefined === editor) { - editor = () => assert(false, "no editor"); + const old = GitUtil.editMessage; + try { + const repo = repos.x; + let cwd = ""; + if (undefined !== c.cwd) { + cwd = path.join(repo.workdir(), c.cwd); + } + else { + cwd = repo.workdir(); + } + let editor = c.editor; + if (editor) { + GitUtil.editMessage = editor; + } else { + GitUtil.editMessage = old; + } + const result = yield Commit.doAmendCommand( + repo, + cwd, + c.message || null, + c.all || false, + c.interactive || false, + false, + editor === undefined); + return { + commitMap: mapCommitResult(result), + }; + } finally { + GitUtil.editMessage = old; } - const result = yield Commit.doAmendCommand( - repo, - cwd, - c.message || null, - c.all || false, - c.interactive || false, - editor); - return { - commitMap: mapCommitResult(result), - }; }); it(caseName, co.wrap(function *() { yield RepoASTTestUtil.testMultiRepoManipulator(c.initial, @@ -3322,7 +3340,6 @@ x=U:Cmsg\n#x-2 s=Sa:s;Bmaster=x;Os Cmsg\n#s-1 README.md=foo,addedbyhook=bar`; const hookPath = repo.path() + "modules/s/hooks/pre-commit"; fs.writeFileSync(hookPath, addNewFileHook); yield fs.chmod(hookPath, 493); //0755 - const editor = () => assert(false, "no editor"); const result = yield Commit.doAmendCommand( repo, @@ -3330,7 +3347,8 @@ x=U:Cmsg\n#x-2 s=Sa:s;Bmaster=x;Os Cmsg\n#s-1 README.md=foo,addedbyhook=bar`; message, true, // all false, // interactive - editor); + false, + false); return { commitMap: mapCommitResult(result), diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index 2e207cf5b..3dbbd602f 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -946,7 +946,8 @@ describe("GitUtil", function () { const cmd = "echo bar >> "; process.env.GIT_EDITOR = cmd; const repo = yield TestUtil.createSimpleRepository(); - const result = yield GitUtil.editMessage(repo, "foo\n"); + const result = yield GitUtil.editMessage(repo, "foo\n", true, + true); assert.equal(result, "foo\nbar\n"); })); }); From 0f13e1a6b9100098e73dd4c07566a6d7d1b3fcb9 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 30 Sep 2020 13:45:20 -0400 Subject: [PATCH 328/402] Better error message (no stack trace) if editor exits non-zero --- node/lib/util/git_util.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 333561ed6..5229def59 100755 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -772,10 +772,16 @@ exports.editMessage = co.wrap(function *(repo, initialContents, // something else. The `ChildProcess.exec` method doesn't provide for a // way to auto-redirect stdio or I'd use it. - yield ChildProcess.spawn("/bin/sh", - ["-c", `${editorCommand} '${messagePath}'`], { - stdio: "inherit", - }); + try { + const args = ["-c", `${editorCommand} '${messagePath}'`]; + yield ChildProcess.spawn("/bin/sh", args, { + stdio: "inherit", + }); + } catch(e) { + throw new UserError( + `There was a problem with the editor '${editorCommand}'. +Please supply the message using the -m option.`); + } } From fa0d4a2cd9965464e25ddabddddd3320506d4107 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 21 Oct 2020 10:11:00 -0400 Subject: [PATCH 329/402] Avoid searching the whole repo when we only care about changes to .gitmodules --- node/lib/util/status_util.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 08b810863..11f803e06 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -570,9 +570,13 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { const treeId = head.treeId(); tree = yield NodeGit.Tree.lookup(repo, treeId); } + let paths = options.paths; + if (!options.showMetaChanges && !paths) { + paths = [SubmoduleConfigUtil.modulesFileName]; + } const status = yield DiffUtil.getRepoStatus(repo, tree, - options.paths, + paths, options.ignoreIndex, options.showAllUntracked); // if showMetaChanges is off, keep .gitmodules changes only From 17f1ee6f7a3906e68db618b1cfc245d00ae79479 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 22 Oct 2020 14:45:11 -0400 Subject: [PATCH 330/402] Oops, empty lists apparently aren't false in JS --- node/lib/util/status_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 11f803e06..fb42247fd 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -571,7 +571,7 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { tree = yield NodeGit.Tree.lookup(repo, treeId); } let paths = options.paths; - if (!options.showMetaChanges && !paths) { + if (!options.showMetaChanges && (!paths || paths.length === 0)) { paths = [SubmoduleConfigUtil.modulesFileName]; } const status = yield DiffUtil.getRepoStatus(repo, From 56354c7613f5d085f92abdd500b2ed31459627e5 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 6 Nov 2020 14:40:01 -0500 Subject: [PATCH 331/402] Stop calling NodeGit.Branch.upstream on non-branches (e.g. detached HEAD). This apparently occasionally segfaults --- node/lib/util/git_util.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 5229def59..f760a1249 100755 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -107,6 +107,9 @@ exports.findBranch = co.wrap(function *(repo, branchName) { exports.getTrackingInfo = co.wrap(function *(repo, branch) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(branch, NodeGit.Reference); + if (!branch.isBranch()) { + return null; + } let upstream; try { upstream = yield NodeGit.Branch.upstream(branch); From e99f6b0c96b96c80fc98ecfe3c2b26cc095d9740 Mon Sep 17 00:00:00 2001 From: Alex Sturm Date: Fri, 4 Dec 2020 13:18:51 +0000 Subject: [PATCH 332/402] Correctly handle tz offset. Fixes #700. --- node/lib/util/config_util.js | 3 ++- node/test/util/config_util.js | 25 +++++++++++++++++++++++++ node/yarn.lock | 12 ++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/node/lib/util/config_util.js b/node/lib/util/config_util.js index 41e8b860d..8e1cf9ed2 100644 --- a/node/lib/util/config_util.js +++ b/node/lib/util/config_util.js @@ -102,7 +102,8 @@ exports.defaultSignature = co.wrap(function*(repo) { if (name && email) { const now = new Date(); const tz = now.getTimezoneOffset(); - return NodeGit.Signature.create(name, email, now.getTime() / 1000, tz); + // libgit's timezone offset convention is inverted from JS. + return NodeGit.Signature.create(name, email, now.getTime() / 1000, -tz); } throw new UserError("Git config vars user.email and user.name are unset"); }); diff --git a/node/test/util/config_util.js b/node/test/util/config_util.js index f61909f11..27fdbc872 100644 --- a/node/test/util/config_util.js +++ b/node/test/util/config_util.js @@ -101,5 +101,30 @@ describe("defaultSignature", function () { const sig = yield repo.defaultSignature(); assert.equal(actual.toString(), sig.toString()); })); + +}); + +["America/New_York", "UTC", "Asia/Tokyo"].forEach(function (tz) { + describe("defaultSignature tz handling " + tz, function() { + let env; + before(function() { + env = process.env; + process.env.TZ = tz; + }); + + it("correctly sets the TZ offset", co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + const sig = yield ConfigUtil.defaultSignature(repo); + const time = sig.when(); + + const dtOffset = new Date().getTimezoneOffset(); + assert.equal(time.offset(), -dtOffset); + assert.equal(time.sign(), dtOffset > 0 ? "-" : "+"); + })); + + after(function (){ + process.env = env; + }); + }); }); }); diff --git a/node/yarn.lock b/node/yarn.lock index a9b40ed16..320c5ee1d 100644 --- a/node/yarn.lock +++ b/node/yarn.lock @@ -101,6 +101,13 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +async-mutex@^0.2.4: + version "0.2.6" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.2.6.tgz#0d7a3deb978bc2b984d5908a2038e1ae2e54ff40" + integrity sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw== + dependencies: + tslib "^2.0.0" + async@1.x: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -1999,6 +2006,11 @@ tough-cookie@^2.3.3, tough-cookie@~2.4.3: psl "^1.1.24" punycode "^1.4.1" +tslib@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" + integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" From 86f1138baf2f19505d442b612db81dafa18eb764 Mon Sep 17 00:00:00 2001 From: Alex Sturm Date: Fri, 4 Dec 2020 17:05:07 +0000 Subject: [PATCH 333/402] Add --force-with-lease to push. Fixes #732. --- node/lib/cmd/push.js | 18 +++++++++-- node/lib/util/destitch_util.js | 9 +++++- node/lib/util/force_push_spec.js | 46 ++++++++++++++++++++++++++++ node/lib/util/git_util.js | 10 +++--- node/lib/util/push.js | 5 +-- node/test/util/close_util.js | 2 +- node/test/util/git_util.js | 28 +++++++++++++++-- node/test/util/push.js | 52 ++++++++++++++++++++++++++++++-- 8 files changed, 153 insertions(+), 17 deletions(-) create mode 100644 node/lib/util/force_push_spec.js diff --git a/node/lib/cmd/push.js b/node/lib/cmd/push.js index af45c7022..89f4e2b59 100644 --- a/node/lib/cmd/push.js +++ b/node/lib/cmd/push.js @@ -32,6 +32,8 @@ const co = require("co"); +const ForcePushSpec = require("../util/force_push_spec"); + /** * This module contains methods for pushing. */ @@ -79,9 +81,19 @@ if not specified.`, parser.addArgument(["-f", "--force"], { required: false, action: "storeConst", - constant: true, + constant: ForcePushSpec.Force, + defaultValue: ForcePushSpec.NoForce, help: `Attempt to push even if not a fast-forward change.`, }); + + parser.addArgument(["--force-with-lease"], { + required: false, + action: "storeConst", + constant: ForcePushSpec.ForceWithLease, + dest: "force", + help: `Force-push only if the remote ref is in the expected state +(i.e. matches the local version of that ref)`, + }); }; /** @@ -131,7 +143,9 @@ exports.executeableSubcommand = co.wrap(function *(args) { yield strRefspecs.map(co.wrap(function *(strRefspec) { const refspec = GitUtil.parseRefspec(strRefspec); - const force = args.force || refspec.force || false; + // Force-push if the refspec explicitly tells us to do so (i.e. is + // prefixed with a '+'). + const force = refspec.force ? ForcePushSpec.Force : args.force; // If 'src' is empty, this is a deletion. Do not use the normal meta // push; there is no need to, e.g., push submodules, in this case. diff --git a/node/lib/util/destitch_util.js b/node/lib/util/destitch_util.js index a137e1f52..c6b3596b8 100644 --- a/node/lib/util/destitch_util.js +++ b/node/lib/util/destitch_util.js @@ -37,6 +37,7 @@ const path = require("path"); const BulkNotesUtil = require("./bulk_notes_util"); const DoWorkQueue = require("./do_work_queue"); +const ForcePushSpec = require("./force_push_spec"); const GitUtil = require("./git_util"); const StitchUtil = require("./stitch_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -404,7 +405,13 @@ exports.pushSyntheticRefs = co.wrap(function *(repo, const pushOne = co.wrap(function *(push) { const sha = push.sha; const refName = SyntheticBranchUtil.getSyntheticBranchForCommit(sha); - yield GitUtil.push(repo, push.url, sha, refName, true, true); + yield GitUtil.push( + repo, + push.url, + sha, + refName, + ForcePushSpec.Force, + true); }); yield DoWorkQueue.doInParallel(toPush, pushOne); }); diff --git a/node/lib/util/force_push_spec.js b/node/lib/util/force_push_spec.js new file mode 100644 index 000000000..2217abfdb --- /dev/null +++ b/node/lib/util/force_push_spec.js @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +class ForcePushSpec { + constructor(flag) { + this.d_flag = flag; + } + + toString() { + return this.d_flag; + } +} +ForcePushSpec.NoForce = new ForcePushSpec(""); +ForcePushSpec.Force = new ForcePushSpec("--force"); +ForcePushSpec.ForceWithLease = new ForcePushSpec("--force-with-lease"); + +module.exports = ForcePushSpec; diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index f760a1249..1a33c64d4 100755 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -45,6 +45,7 @@ const path = require("path"); const ConfigUtil = require("./config_util"); const Hook = require("./hook"); +const ForcePushSpec = require("./force_push_spec"); const GitUtilFast = require("./git_util_fast"); const SyntheticBranchUtil = require("./synthetic_branch_util"); const UserError = require("./user_error"); @@ -330,7 +331,7 @@ exports.getCurrentRepo = function () { * @param {String} remote * @param {String} source * @param {String} target - * @param {String} force + * @param {ForcePushSpec} force * @param {Boolean} [quiet] * @return {String} [return] */ @@ -343,7 +344,7 @@ exports.push = co.wrap(function *(repo, remote, source, target, force, quiet) { assert.isString(remote); assert.isString(source); assert.isString(target); - assert.isBoolean(force); + assert.instanceOf(force, ForcePushSpec); if (undefined === quiet) { quiet = false; @@ -352,10 +353,7 @@ exports.push = co.wrap(function *(repo, remote, source, target, force, quiet) { assert.isBoolean(quiet); } - let forceStr = ""; - if (force) { - forceStr = "-f"; - } + let forceStr = force ? force.toString() : ""; const { execString, environ } = (() => { if (repo.workdir()) { diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 0393584b8..5bf4451b4 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -41,6 +41,7 @@ const colors = require("colors"); const NodeGit = require("nodegit"); const DoWorkQueue = require("./do_work_queue"); +const ForcePushSpec = require("./force_push_spec"); const GitUtil = require("./git_util"); const SubmoduleUtil = require("./submodule_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -215,7 +216,7 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { assert.isString(remoteName); assert.isString(source); assert.isString(target); - assert.isBoolean(force); + assert.instanceOf(force, ForcePushSpec); let remoteUrl = yield GitUtil.getUrlFromRemoteName(repo, remoteName); @@ -256,7 +257,7 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { subUrl, sha, syntheticName, - true, + ForcePushSpec.Force, true); if (null !== pushResult) { errorMessage += diff --git a/node/test/util/close_util.js b/node/test/util/close_util.js index a8341a5f4..a79f5d2d5 100644 --- a/node/test/util/close_util.js +++ b/node/test/util/close_util.js @@ -130,7 +130,7 @@ a/b/c/d/e/s4=Sa:1;Bmaster=2`, } yield CloseUtil.close(x, cwd, c.paths, c.force || false); }); - it(caseName, co.wrap(function *() { + it(caseName + (sparse ? " sparse" : ""), co.wrap(function *() { let state = c.state; let expected = c.expected; if (sparse) { diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js index 3dbbd602f..e015e53e9 100644 --- a/node/test/util/git_util.js +++ b/node/test/util/git_util.js @@ -37,6 +37,7 @@ const mkdirp = require("mkdirp"); const NodeGit = require("nodegit"); const path = require("path"); +const ForcePushSpec = require("../../lib/util/force_push_spec"); const GitUtil = require("../../lib/util/git_util"); const RepoAST = require("../../lib/util/repo_ast"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); @@ -431,7 +432,7 @@ describe("GitUtil", function () { function pusher(repoName, origin, local, remote, force, quiet) { return co.wrap(function *(repos) { - force = force || false; + force = force || ForcePushSpec.NoForce; const result = yield GitUtil.push(repos[repoName], origin, @@ -459,9 +460,29 @@ describe("GitUtil", function () { }, "force success": { input: "a=B:C2-1;Bmaster=2|b=Ca:C3-1;Bmaster=3", - manipulator: pusher("b", "origin", "master", "master", true), + manipulator: pusher( + "b", "origin", "master", "master", ForcePushSpec.Force), expected: "a=B:C3-1;Bmaster=3|b=Ca:Bmaster=3", }, + "force with lease success": { + input: "a=B:C2-1;Bmaster=2|b=Ca:C3-1;Bmaster=3", + manipulator: pusher( + "b", + "origin", + "master", + "master", + ForcePushSpec.ForceWithLease), + expected: "a=B:C3-1;Bmaster=3|b=Ca:Bmaster=3", + }, + "force with lease failure": { + input: ` + a=B:C2-1;Bmaster=2| + b=Ca:Rorigin=a master=1;C3-1; + Bmaster=3 origin/master;Bold=2`, + manipulator: pusher( + "b", "origin", "master", "master", ForcePushSpec.Force), + fail: true, + }, "push new branch": { input: "a=S|b=Ca:Bfoo=1", expected: "a=S:Bfoo=1|b=Ca:Bfoo=1", @@ -470,7 +491,8 @@ describe("GitUtil", function () { "quiet push new branch": { input: "a=S|b=Ca:Bfoo=1", expected: "a=S:Bfoo=1|b=Ca:Bfoo=1", - manipulator: pusher("b", "origin", "foo", "foo", true), + manipulator: pusher( + "b", "origin", "foo", "foo", ForcePushSpec.Force), }, "update a branch": { input: "a=B|b=Ca:C2-1;Bmaster=2 origin/master", diff --git a/node/test/util/push.js b/node/test/util/push.js index 113ca9e7e..62baab433 100644 --- a/node/test/util/push.js +++ b/node/test/util/push.js @@ -35,6 +35,7 @@ const co = require("co"); const NodeGit = require("nodegit"); const rimraf = require("rimraf"); +const ForcePushSpec = require("../../lib/util/force_push_spec"); const GitUtil = require("../../lib/util/git_util"); const Push = require("../../lib/util/push"); const RepoAST = require("../../lib/util/repo_ast"); @@ -397,7 +398,7 @@ describe("push", function () { function pusher(repoName, remoteName, source, target, force) { return co.wrap(function *(repos) { - force = force || false; + force = force || ForcePushSpec.NoForce; const x = repos[repoName]; yield Push.push(x, remoteName, source, target, force); }); @@ -416,9 +417,56 @@ describe("push", function () { }, "no-ffwd success": { initial: "a=B:C2-1;Bmaster=2|b=Ca:C3-1;Bmaster=3 origin/master", - manipulator: pusher("b", "origin", "master", "master", true), + manipulator: pusher( + "b", "origin", "master", "master", ForcePushSpec.Force), expected: "a=B:C3-1;Bmaster=3|b=Ca", }, + "no-ffwd success with lease": { + initial: "a=B:C2-1;Bmaster=2|b=Ca:C3-1;Bmaster=3 origin/master", + manipulator: pusher( + "b", + "origin", + "master", + "master", + ForcePushSpec.ForceWithLease), + expected: "a=B:C3-1;Bmaster=3|b=Ca", + }, + "no-ffwd old remote failure": { + initial: ` + a=B:C2-1;Bmaster=2| + b=Ca:Rorigin=a master=1;C3-1;Bmaster=3 origin/master;Bold=2`, + manipulator: pusher( + "b", + "origin", + "master", + "master", + ForcePushSpec.NoForce), + fails: true, + }, + "no-ffwd old remote success": { + initial: ` + a=B:C2-1;Bmaster=2| + b=Ca:Rorigin=a master=1;C3-1;Bmaster=3 origin/master;Bold=2`, + manipulator: pusher( + "b", + "origin", + "master", + "master", + ForcePushSpec.Force), + expected: "a=B:C3-1;Bmaster=3|b=Ca:Bold=2", + }, + "no-ffwd old remote failure with lease": { + initial: ` + a=B:C2-1;Bmaster=2| + b=Ca:Rorigin=a master=1;C3-1;Bmaster=3 origin/master;Bold=2`, + manipulator: pusher( + "b", + "origin", + "master", + "master", + ForcePushSpec.ForceWithLease), + fails: true, + }, "simple (noop) success": { initial: "a=S|b=Ca", manipulator: pusher("b", "origin", "master", "master"), From 65695d1bfcac12fafc008a7fd92e366f87422d58 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 9 Dec 2020 12:41:48 -0500 Subject: [PATCH 334/402] Remove bogus params to createBranch See https://github.com/nodegit/nodegit/issues/1549 for bogosity. --- node/lib/util/git_util.js | 4 +--- node/lib/util/test_util.js | 3 +-- node/test/util/read_repo_ast_util.js | 17 ++++++++--------- node/test/util/repo_ast_test_util.js | 12 ++++-------- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 1a33c64d4..040d92f49 100755 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -68,9 +68,7 @@ exports.createBranchFromHead = co.wrap(function *(repo, branchName) { const head = yield repo.getHeadCommit(); return yield repo.createBranch(branchName, head, - 0, - yield ConfigUtil.defaultSignature(repo), - "git-meta branch"); + 0); }); /** diff --git a/node/lib/util/test_util.js b/node/lib/util/test_util.js index f2d1fee54..ca551371e 100644 --- a/node/lib/util/test_util.js +++ b/node/lib/util/test_util.js @@ -132,8 +132,7 @@ exports.createSimpleRepositoryOnBranch = co.wrap(function *(branchName) { const repo = yield exports.createSimpleRepository(); const commit = yield repo.getHeadCommit(); - const sig = yield ConfigUtil.defaultSignature(repo); - const publicBranch = yield repo.createBranch(branchName, commit, 0, sig); + const publicBranch = yield repo.createBranch(branchName, commit, 0); yield repo.setHead(publicBranch.name()); return repo; diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js index 0d5b9e79d..b19c2fd69 100644 --- a/node/test/util/read_repo_ast_util.js +++ b/node/test/util/read_repo_ast_util.js @@ -467,8 +467,7 @@ describe("readRAST", function () { it("remote with path in tracking branch", co.wrap(function *() { const base = yield TestUtil.createSimpleRepository(); const headId = (yield base.getHeadCommit()).id(); - const sig = yield base.defaultSignature(); - yield base.createBranch("foo/bar", headId, 1, sig, "branch"); + yield base.createBranch("foo/bar", headId, 1); const clonePath = yield TestUtil.makeTempDir(); const clone = yield NodeGit.Clone.clone(base.workdir(), clonePath); const master = yield clone.getBranch("refs/heads/master"); @@ -1008,7 +1007,7 @@ describe("readRAST", function () { const firstCommit = yield repo.getHeadCommit(); const firstSha = firstCommit.id().tostrS(); const sig = yield repo.defaultSignature(); - yield repo.createBranch("b", firstCommit, 0, sig); + yield repo.createBranch("b", firstCommit, 0); yield repo.checkoutBranch("b"); yield fs.writeFile(path.join(workdir, "foo"), "foo"); const commitB = yield TestUtil.makeCommit(repo, ["foo"]); @@ -1069,12 +1068,12 @@ describe("readRAST", function () { const basePath = base.workdir(); const baseMaster = yield base.getHeadCommit(); const baseMasterSha = baseMaster.id().tostrS(); - yield base.createBranch("foo", baseMaster, 0, sig); + yield base.createBranch("foo", baseMaster, 0); yield base.checkoutBranch("foo"); yield fs.writeFile(path.join(basePath, "foo"), "foo"); const fooCommit = yield TestUtil.makeCommit(base, ["foo"]); const fooSha = fooCommit.id().tostrS(); - yield base.createBranch("bar", baseMaster, 0, sig); + yield base.createBranch("bar", baseMaster, 0); yield base.checkoutBranch("bar"); yield fs.writeFile(path.join(basePath, "bar"), "bar"); const barCommit = yield TestUtil.makeCommit(base, ["bar"]); @@ -1092,7 +1091,7 @@ describe("readRAST", function () { // Make the `wham` branch and put a change to the submodule on it. - yield repo.createBranch("wham", subCommit, 0, sig); + yield repo.createBranch("wham", subCommit, 0); yield repo.checkoutBranch("wham"); const subRepo = yield submodule.open(); const localBar = yield subRepo.getCommit(barSha); @@ -1226,12 +1225,12 @@ describe("readRAST", function () { const basePath = base.workdir(); const baseMaster = yield base.getHeadCommit(); const baseMasterSha = baseMaster.id().tostrS(); - yield base.createBranch("foo", baseMaster, 0, sig); + yield base.createBranch("foo", baseMaster, 0); yield base.checkoutBranch("foo"); yield fs.writeFile(path.join(basePath, "foo"), "foo"); const fooCommit = yield TestUtil.makeCommit(base, ["foo"]); const fooSha = fooCommit.id().tostrS(); - yield base.createBranch("bar", baseMaster, 0, sig); + yield base.createBranch("bar", baseMaster, 0); yield base.checkoutBranch("bar"); yield fs.writeFile(path.join(basePath, "bar"), "bar"); const barCommit = yield TestUtil.makeCommit(base, ["bar"]); @@ -1249,7 +1248,7 @@ describe("readRAST", function () { // Make the `wham` branch and put a change to the submodule on it. - yield repo.createBranch("wham", subCommit, 0, sig); + yield repo.createBranch("wham", subCommit, 0); yield repo.checkoutBranch("wham"); const subRepo = yield submodule.open(); const localBar = yield subRepo.getCommit(barSha); diff --git a/node/test/util/repo_ast_test_util.js b/node/test/util/repo_ast_test_util.js index 92ad62dde..de3656372 100644 --- a/node/test/util/repo_ast_test_util.js +++ b/node/test/util/repo_ast_test_util.js @@ -85,9 +85,8 @@ const makeClone = co.wrap(function *(repos, maps) { // Test framework expects a trailing '/' to support relative paths. const b = yield NodeGit.Clone.clone(aPath, bPath); - const sig = yield b.defaultSignature(); const head = yield b.getHeadCommit(); - yield b.createBranch("foo", head.id(), 1, sig, "branch commit"); + yield b.createBranch("foo", head.id(), 1); yield b.checkoutBranch("foo"); const commit = yield TestUtil.generateCommit(b); yield b.checkoutBranch("master"); @@ -265,8 +264,7 @@ Bmaster=1 origin/master`, m: co.wrap(function *(repos) { const x = repos.x; const head = yield x.getHeadCommit(); - const sig = yield x.defaultSignature(); - yield repos.x.createBranch("foo", head, 0, sig); + yield repos.x.createBranch("foo", head, 0); throw new UserError("bad bad"); }), e: "x=E:Bfoo=1", @@ -293,8 +291,7 @@ Bmaster=1 origin/master`, const x = repos.x; const head = yield x.getHeadCommit(); const headStr = head.id().tostrS(); - const sig = yield x.defaultSignature(); - yield repos.x.createBranch(`foo-${headStr}`, head, 0, sig); + yield repos.x.createBranch(`foo-${headStr}`, head, 0); }), e: "x=S", options: { @@ -316,8 +313,7 @@ Bmaster=1 origin/master`, const x = repos.x; const head = yield x.getHeadCommit(); const headStr = head.id().tostrS(); - const sig = yield x.defaultSignature(); - yield repos.x.createBranch(`foo-${headStr}`, head, 0, sig); + yield repos.x.createBranch(`foo-${headStr}`, head, 0); }), e: "x=E:Bfoo-1=1", options: { From d0b62659488fecbce8dbb0c9904a04871232d863 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 13 Dec 2020 04:56:36 +0000 Subject: [PATCH 335/402] Bump ini from 1.3.5 to 1.3.8 in /node Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8) Signed-off-by: dependabot[bot] --- node/yarn.lock | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/node/yarn.lock b/node/yarn.lock index a9b40ed16..4ff78b0a0 100644 --- a/node/yarn.lock +++ b/node/yarn.lock @@ -101,6 +101,13 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +async-mutex@^0.2.4: + version "0.2.6" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.2.6.tgz#0d7a3deb978bc2b984d5908a2038e1ae2e54ff40" + integrity sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw== + dependencies: + tslib "^2.0.0" + async@1.x: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -869,9 +876,9 @@ inherits@2, inherits@~2.0.1, inherits@~2.0.3: integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== invert-kv@^2.0.0: version "2.0.0" @@ -1999,6 +2006,11 @@ tough-cookie@^2.3.3, tough-cookie@~2.4.3: psl "^1.1.24" punycode "^1.4.1" +tslib@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" + integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" From 32a9535626d1bc95e84fcfe678cdfb309cc1af61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Dec 2020 14:52:10 +0000 Subject: [PATCH 336/402] Bump bl from 1.2.2 to 1.2.3 in /node Bumps [bl](https://github.com/rvagg/bl) from 1.2.2 to 1.2.3. - [Release notes](https://github.com/rvagg/bl/releases) - [Commits](https://github.com/rvagg/bl/compare/v1.2.2...v1.2.3) Signed-off-by: dependabot[bot] --- node/yarn.lock | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/node/yarn.lock b/node/yarn.lock index 4ff78b0a0..9626abc8c 100644 --- a/node/yarn.lock +++ b/node/yarn.lock @@ -146,9 +146,9 @@ binary-search@: integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA== bl@^1.0.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" - integrity sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA== + version "1.2.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" + integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww== dependencies: readable-stream "^2.3.5" safe-buffer "^5.1.1" @@ -870,11 +870,16 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@~2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -1537,9 +1542,9 @@ prelude-ls@~1.1.2: integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= process-nextick-args@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" - integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== promise-polyfill@^6.0.1: version "6.1.0" @@ -1620,9 +1625,9 @@ readable-stream@1.1: string_decoder "~0.10.x" readable-stream@^2.0.6, readable-stream@^2.3.0, readable-stream@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -1715,7 +1720,12 @@ rimraf@~2.6.2: dependencies: glob "^7.1.3" -safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== From fbfec71443549973634235ff61536e624eef6edb Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 15 Dec 2020 11:20:10 -0500 Subject: [PATCH 337/402] remove unused parameter --- node/lib/util/stitch_util.js | 9 +++------ node/test/util/stitch_util.js | 11 +---------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/node/lib/util/stitch_util.js b/node/lib/util/stitch_util.js index ebe9abb1f..121831462 100644 --- a/node/lib/util/stitch_util.js +++ b/node/lib/util/stitch_util.js @@ -399,7 +399,7 @@ exports.listSubmoduleChanges = co.wrap(function *(repo, commits) { }); /** - * Return a map from submodule name to shas to list of objects containing the + * Return a map from submodule name to list of objects containing the * fields: * - `metaSha` -- the meta-repo sha from which this subodule sha came * - `url` -- url configured for the submodule @@ -423,14 +423,12 @@ exports.listFetches = co.wrap(function *(repo, toFetch, commitChanges, keepAsSubmodule, - adjustPath, - numParallel) { + adjustPath) { assert.instanceOf(repo, NodeGit.Repository); assert.isArray(toFetch); assert.isObject(commitChanges); assert.isFunction(keepAsSubmodule); assert.isFunction(adjustPath); - assert.isNumber(numParallel); let urls = {}; @@ -964,8 +962,7 @@ exports.stitch = co.wrap(function *(repoPath, commitsToStitch, changes, options.keepAsSubmodule, - adjustPath, - options.numParallel); + adjustPath); console.log("Found", Object.keys(fetches).length, "subs to fetch."); const subNames = Object.keys(fetches); const doFetch = co.wrap(function *(name, i) { diff --git a/node/test/util/stitch_util.js b/node/test/util/stitch_util.js index 24aee64c8..de931fc0a 100644 --- a/node/test/util/stitch_util.js +++ b/node/test/util/stitch_util.js @@ -1105,21 +1105,18 @@ describe("listFetches", function () { state: "S", toFetch: [], keepAsSubmodule: () => false, - numParallel: 2, expected: {}, }, "a sub, not picked": { state: "S:C2-1 s=S/a:1;Bmaster=2", toFetch: ["1"], keepAsSubmodule: () => false, - numParallel: 2, expected: {}, }, "added sub": { state: "S:C2-1 s=S/a:1;Bmaster=2", toFetch: ["2"], keepAsSubmodule: () => false, - numParallel: 2, expected: { "s": [ { metaSha: "2", url: "/a", sha: "1" }, @@ -1130,7 +1127,6 @@ describe("listFetches", function () { state: "S:C2-1 s=S/a:1;Bmaster=2", toFetch: ["2"], keepAsSubmodule: (name) => "s" === name, - numParallel: 2, expected: {}, }, "adjusted to null": { @@ -1138,14 +1134,12 @@ describe("listFetches", function () { toFetch: ["2"], keepAsSubmodule: () => false, adjustPath: () => null, - numParallel: 2, expected: {}, }, "changed sub": { state: "S:Cx-1;Bx=x;C2-1 s=S/a:1;C3-2 s=S/a:x;Bmaster=3", toFetch: ["3"], keepAsSubmodule: () => false, - numParallel: 2, expected: { "s": [ { metaSha: "3", url: "/a", sha: "x" }, @@ -1156,14 +1150,12 @@ describe("listFetches", function () { state: "S:Cx-1;Bx=x;C2-1 s=S/a:1;C3-2 s=S/a:x;Bmaster=3", toFetch: ["3"], keepAsSubmodule: (name) => "s" === name, - numParallel: 2, expected: {}, }, "two changes in a sub": { state: "S:Cx-1;Bx=x;C2-1 s=S/a:1;C3-2 s=S/a:x;Bmaster=3", toFetch: ["2", "3"], keepAsSubmodule: () => false, - numParallel: 2, expected: { "s": [ { metaSha: "3", url: "/a", sha: "x" }, @@ -1190,8 +1182,7 @@ describe("listFetches", function () { toFetch, changes, c.keepAsSubmodule, - adjustPath, - c.numParallel); + adjustPath); function mapFetch(f) { return { url: f.url, From bc39376bbed07583e230dc0fde162789ecb5273f Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 16 Dec 2020 10:09:10 -0500 Subject: [PATCH 338/402] Remove redundant synthetic ref update. Fetch already fetches into the synthetic ref. --- node/lib/util/stitch_util.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/node/lib/util/stitch_util.js b/node/lib/util/stitch_util.js index 121831462..8645c42b2 100644 --- a/node/lib/util/stitch_util.js +++ b/node/lib/util/stitch_util.js @@ -40,7 +40,6 @@ const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); const SubmoduleUtil = require("./submodule_util"); -const SyntheticBranchUtil = require("./synthetic_branch_util"); const TreeUtil = require("./tree_util"); const UserError = require("./user_error"); @@ -706,9 +705,6 @@ exports.fetchSubCommits = co.wrap(function *(repo, url, subFetches) { if (fetched) { console.log("Fetched:", sha, "from", subUrl); - const refName = - SyntheticBranchUtil.getSyntheticBranchForCommit(sha); - yield NodeGit.Reference.create(repo, refName, sha, 1, "fetched"); } } }); From 7a0d85380edacbb6fc2b3dedc71457d61cd91349 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 16 Dec 2020 10:10:24 -0500 Subject: [PATCH 339/402] stitch: prefix ref names for submodule refs When fetching from a submodule, we would fetch into a global synthetic ref namespace. This works, but it makes ref negotiation take a long time. Instead, we figure that there is little to no overlap between submodules, and prefix submodule refs with the name of their submodule. Note that this could, in theory, lead to d/f conflicts if someone names a submodule after a SHA, but this would require true malice. --- node/lib/util/git_util.js | 28 +++++++++++++++++++++----- node/lib/util/stitch_util.js | 6 +++--- node/lib/util/synthetic_branch_util.js | 1 + node/test/util/stitch_util.js | 2 +- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 040d92f49..f0e14fddf 100755 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -555,10 +555,13 @@ git -C '${repo.path()}' fetch -q '${remoteName}' '${branch}'`; * @param {String} sha * @return {Bool} */ -exports.fetchSha = co.wrap(function *(repo, url, sha) { +exports.fetchSha = co.wrap(function *(repo, url, sha, prefix) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(url); assert.isString(sha); + if (prefix === undefined) { + prefix = ""; + } // First, try to get the commit. If we succeed, no need to fetch. @@ -569,10 +572,25 @@ exports.fetchSha = co.wrap(function *(repo, url, sha) { catch (e) { } - const syntheticName = - SyntheticBranchUtil.getSyntheticBranchForCommit(sha); - const execString = `git -C '${repo.path()}' fetch -q '${url}' \ -${sha}:${syntheticName}`; + const refPrefix = `${SyntheticBranchUtil.SYNTHETIC_BRANCH_BASE}${prefix}`; + let negotiationTip = ""; + let negotiationAlgorithm = ""; + if (prefix) { + const refCountCommand = `git -C '${repo.path()}' for-each-ref \ +--count 1 ${refPrefix}`; + let result = yield ChildProcess.exec(refCountCommand); + const anyRefs = !!result.stdout.trim(); + if (anyRefs) { + negotiationTip = `--negotiation-tip ${refPrefix}*`; + } else { + // If there are no existing refs, negotiation-tip will be ignored. + // In this case, we would still prefer not to negotiate + negotiationAlgorithm = "-c fetch.negotiationAlgorithm=noop"; + } + } + + const execString = `git -C '${repo.path()}' ${negotiationAlgorithm} fetch \ + -q '${url}' ${negotiationTip} ${sha}:${refPrefix}${sha}`; try { yield ChildProcess.exec(execString, { maxBuffer: EXEC_BUFFER diff --git a/node/lib/util/stitch_util.js b/node/lib/util/stitch_util.js index 8645c42b2..f22715093 100644 --- a/node/lib/util/stitch_util.js +++ b/node/lib/util/stitch_util.js @@ -683,7 +683,7 @@ git notes --ref ${exports.allowedToFailNoteRef} add -m skip ${metaSha}`); * @param {String} url * @param {[Object]} subFetches */ -exports.fetchSubCommits = co.wrap(function *(repo, url, subFetches) { +exports.fetchSubCommits = co.wrap(function *(repo, name, url, subFetches) { assert.instanceOf(repo, NodeGit.Repository); assert.isString(url); assert.isArray(subFetches); @@ -694,7 +694,7 @@ exports.fetchSubCommits = co.wrap(function *(repo, url, subFetches) { const sha = fetch.sha; let fetched; try { - fetched = yield GitUtil.fetchSha(repo, subUrl, sha); + fetched = yield GitUtil.fetchSha(repo, subUrl, sha, name + "/"); } catch (e) { console.log("Fetch of", subUrl, "failed:", e.message); @@ -967,7 +967,7 @@ exports.stitch = co.wrap(function *(repoPath, (${i + 1}/${subNames.length}) -- fetched ${subFetches.length} SHAs for \ ${name}`; console.time(fetchTimeMessage); - yield exports.fetchSubCommits(repo, url, subFetches); + yield exports.fetchSubCommits(repo, name, url, subFetches); console.timeEnd(fetchTimeMessage); }); yield DoWorkQueue.doInParallel(subNames, diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js index 4166b3c88..455da8e4e 100644 --- a/node/lib/util/synthetic_branch_util.js +++ b/node/lib/util/synthetic_branch_util.js @@ -46,6 +46,7 @@ const assert = require("chai").assert; const NOTES_REF = "refs/notes/git-meta/subrepo-check"; const SYNTHETIC_BRANCH_BASE = "refs/commits/"; +exports.SYNTHETIC_BRANCH_BASE = SYNTHETIC_BRANCH_BASE; /** * The identity function diff --git a/node/test/util/stitch_util.js b/node/test/util/stitch_util.js index de931fc0a..4793fe575 100644 --- a/node/test/util/stitch_util.js +++ b/node/test/util/stitch_util.js @@ -1242,7 +1242,7 @@ describe("fetchSubCommits", function () { }; }); const url = maps.reverseUrlMap[c.url]; - yield StitchUtil.fetchSubCommits(x, url, fetches); + yield StitchUtil.fetchSubCommits(x, "x", url, fetches); }); yield RepoASTTestUtil.testMultiRepoManipulator(c.input, c.expected, From 589c4689a5244dcf5da078ee6784af581d621250 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 16 Dec 2020 14:36:40 -0500 Subject: [PATCH 340/402] fix quoting --- node/lib/util/git_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index f0e14fddf..06b137926 100755 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -581,7 +581,7 @@ exports.fetchSha = co.wrap(function *(repo, url, sha, prefix) { let result = yield ChildProcess.exec(refCountCommand); const anyRefs = !!result.stdout.trim(); if (anyRefs) { - negotiationTip = `--negotiation-tip ${refPrefix}*`; + negotiationTip = `--negotiation-tip '${refPrefix}*'`; } else { // If there are no existing refs, negotiation-tip will be ignored. // In this case, we would still prefer not to negotiate From c2e33692c784ecd09ff3c4323c32d4eeee04bd5d Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 17 Dec 2020 10:06:37 -0500 Subject: [PATCH 341/402] fix test --- node/test/util/stitch_util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/test/util/stitch_util.js b/node/test/util/stitch_util.js index 4793fe575..c42b7eb61 100644 --- a/node/test/util/stitch_util.js +++ b/node/test/util/stitch_util.js @@ -50,7 +50,7 @@ const FILEMODE = NodeGit.TreeEntry.FILEMODE; * Replace refs and notes with their equivalent logical mapping. */ function refMapper(actual, mapping) { - const fetchedSubRe = /(commits\/)(.*)/; + const fetchedSubRe = /(commits\/(?:[a-z/]*\/)?)(.*)/; const commitMap = mapping.commitMap; let result = {}; @@ -1214,7 +1214,7 @@ describe("fetchSubCommits", function () { } ], url: "a", - expected: "x=E:Fcommits/z=z", + expected: "x=E:Fcommits/x/z=z", }, "one, w sub no need to fetch": { input: "a=B|x=U", From fc2eb8c2b7357d59dd8129f33b8e0eb69e96105b Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 16 Dec 2020 21:17:06 -0500 Subject: [PATCH 342/402] run precommit hook on non-all amend --- node/lib/util/commit.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 3b2f28c62..87d2dafa6 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -1369,9 +1369,9 @@ exports.amendMetaRepo = co.wrap(function *(repo, yield exports.stageChange(subIndex, path, change); } } - if (!noVerify) { - yield runPreCommitHook(subRepo, subIndex); - } + } + if (!noVerify) { + yield runPreCommitHook(subRepo, subIndex); } subCommits[subName] = yield exports.createAmendCommit(subRepo, subMessage); From bc377d41d0f3bdb6b9d57dec2e719b3a4e1a3e9e Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 28 Dec 2020 14:48:02 -0500 Subject: [PATCH 343/402] Improve message when attempting to push with .gitmodules in a bad state --- node/lib/util/push.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/node/lib/util/push.js b/node/lib/util/push.js index 5bf4451b4..c7669b74f 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -246,6 +246,10 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { // Resolve the submodule's URL against the URL of the meta-repo, // ignoring the remote that is configured in the open submodule. + if (!(subName in urls)) { + throw new UserError( + `The submodule {subName} doesn't have an entry in .gitmodules`); + } const subUrl = SubmoduleConfigUtil.resolveSubmoduleUrl(remoteUrl, urls[subName]); From 86ea3cc400ca9572bb4e61f6a71af25d0d5b415b Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 29 Dec 2020 11:32:35 -0500 Subject: [PATCH 344/402] fix missing $ --- node/lib/util/push.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node/lib/util/push.js b/node/lib/util/push.js index c7669b74f..04ea6fe1f 100644 --- a/node/lib/util/push.js +++ b/node/lib/util/push.js @@ -248,7 +248,8 @@ exports.push = co.wrap(function *(repo, remoteName, source, target, force) { if (!(subName in urls)) { throw new UserError( - `The submodule {subName} doesn't have an entry in .gitmodules`); + `The submodule ${subName} doesn't have an entry in .gitmodules` + ); } const subUrl = SubmoduleConfigUtil.resolveSubmoduleUrl(remoteUrl, urls[subName]); From 36f60459d555707d282e0e9b951c6e5c2bc4a628 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 22 Jan 2021 00:29:40 -0500 Subject: [PATCH 345/402] Make commit faster with many open submodules I manually tested this inside Two Sigma on the repo that our user complained about, and it seems to work. --- node/lib/util/commit.js | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 87d2dafa6..d906c16ba 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -387,12 +387,44 @@ exports.formatEditorPrompt = function (status, cwd) { * @param {Object} submodules name -> RepoStatus.Submodule */ const stageOpenSubmodules = co.wrap(function *(repo, index, submodules) { - yield Object.keys(submodules).map(co.wrap(function *(name) { + const entries = yield Object.keys(submodules).map(co.wrap(function *(name) { const sub = submodules[name]; if (null !== sub.workdir) { - yield index.addByPath(name); + /* + We probably shouldn't run addByPath in parallel because + one of the following two things is probably true: + + 1. Updating the index is not be thread-safe. I don't + think this is true, but the docs are useless here, and I + don't see any locking in the libgit2 code, so if it's + present it must be in NodeGit. + + 2. Updating the index is made thread-safe by a too-coarse + lock which locks before reading the submodule HEAD + instead of before the actual index update (this makes + sense given the libgit2 index code). This too-coarse + lock means slow submodule HEAD lookups are effectively + serialized despite this being a parallel map. + + So instead, we'll do the submodule HEAD lookups in + parallel and then add the entries serially, which should + be relatively fast. + */ + + const subRepo = yield SubmoduleUtil.getRepo(repo, name); + const head = yield subRepo.getHeadCommit(); + const newEntry = new NodeGit.IndexEntry(); + newEntry.flags = 0; + newEntry.flagsExtended = 0; + newEntry.mode = NodeGit.TreeEntry.FILEMODE.COMMIT; + newEntry.id = head; + newEntry.path = name; + return newEntry; } })); + for (const entry of entries.filter(x => !!x)) { + index.add(entry); + } yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); }); From 7ee2492f030bd4aba2990003175ace3c7da2a623 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 22 Jan 2021 14:02:17 -0500 Subject: [PATCH 346/402] make reset (and probably also push) faster by using a modern version of readdir --- .jshintrc | 3 +- node/lib/util/submodule_util.js | 25 +++++--------- node/lib/util/walk.js | 61 +++++++++++++++++++++++++++++++++ node/package.json | 4 +-- 4 files changed, 73 insertions(+), 20 deletions(-) create mode 100644 node/lib/util/walk.js diff --git a/.jshintrc b/.jshintrc index dc4d9bbd1..946454b8d 100644 --- a/.jshintrc +++ b/.jshintrc @@ -12,7 +12,8 @@ "undef" : true, "node" : true, "unused" : true, - "esnext" : true, + "esversion": 8, + "noyield" : true, "varstmt" : true, "predef": [ "Intl" diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index d0b0ce677..7d3b3cdf8 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -40,7 +40,6 @@ const colors = require("colors"); const NodeGit = require("nodegit"); const fs = require("fs-promise"); const path = require("path"); -const walk = require("walk"); const DoWorkQueue = require("../util/do_work_queue"); const GitUtil = require("./git_util"); @@ -49,6 +48,7 @@ const SubmoduleChange = require("./submodule_change"); const SubmoduleFetcher = require("./submodule_fetcher"); const SubmoduleConfigUtil = require("./submodule_config_util"); const UserError = require("./user_error"); +const Walk = require("./walk"); /** * Return the names of the submodules (visible or otherwise) for the index @@ -212,16 +212,14 @@ const gitReservedNames = new Set(["HEAD", "FETCH_HEAD", "ORIG_HEAD", * approximately, those which we have ever opened. */ exports.listAbsorbedSubmodules = co.wrap(function*(repo) { - const options = { - followLinks: false - }; - const modules_dir = path.join(repo.path(), "modules"); - const walker = walk.walk(modules_dir, options); const out = []; - walker.on("names", function (root, nodeNamesArray) { - if (nodeNamesArray.indexOf("HEAD") !== -1) { + if (!fs.existsSync(modules_dir)) { + return out; + } + yield Walk.walk(modules_dir, function*(root, files, dirs) { + if (files.indexOf("HEAD") !== -1) { // We've hit an actual git module -- don't recurse // further. It's possible that our module contains other // modules (e.g. if foo/bar/baz gets moved to @@ -236,23 +234,16 @@ exports.listAbsorbedSubmodules = co.wrap(function*(repo) { // reserved name, and recurse the rest if any. const filtered = []; - for (const name of nodeNamesArray) { + for (const name of dirs) { if (!gitReservedNames.has(name)) { filtered.push(name); } } - nodeNamesArray.splice(0, nodeNamesArray.length, ...filtered); + dirs.splice(0, dirs.length, ...filtered); out.push(root.substring(modules_dir.length + 1)); } }); - yield new Promise(function(resolve, reject) { - try { - walker.on("end", resolve); - } catch (e) { - reject(); - } - }); return out; diff --git a/node/lib/util/walk.js b/node/lib/util/walk.js new file mode 100644 index 000000000..be9cf5d0a --- /dev/null +++ b/node/lib/util/walk.js @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +/** + * This is a fast, simple replacement for walk. Normally, we would + * use something like @root/walk, but (a) that requires Node 10 and + * (b) it doesn't support fine-grained filtering of subdirs. + */ + +const co = require("co"); +const path = require("path"); +const readdirWith = require("readdir-withfiletypes"); + +exports.walk = co.wrap(function*(pathname, callback) { + const dirents = readdirWith.readdirSync(pathname, { withFileTypes: true }); + + const files = []; + const dirs = []; + for (const dirent of dirents) { + if (dirent.isDirectory()) { + dirs.push(dirent.name); + } else { + files.push(dirent.name); + } + } + + // Note that caller can edit dirs in-place to change the traversal + yield callback(pathname, files, dirs); + for (let dir of dirs) { + yield exports.walk(path.join(pathname, dir), callback); + } +}); diff --git a/node/package.json b/node/package.json index 9b8ce9d7c..15d4e2126 100644 --- a/node/package.json +++ b/node/package.json @@ -39,9 +39,9 @@ "git-range": "", "group-by": "", "nodegit": "^0.25.0", + "readdir-withfiletypes": "^1.0.2", "rimraf": "", - "split": "", - "walk": "" + "split": "" }, "devDependencies": { "deepcopy": "", From dd6ebe8819432b296e723cc6069b905b6cf0e27f Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 22 Jan 2021 14:03:13 -0500 Subject: [PATCH 347/402] Test for cherry-pick I missed this as part of 72057ba42a6fa3184a1c6f4b30373a41e9d28cf1 --- node/test/util/cherry_pick.js | 87 +++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 node/test/util/cherry_pick.js diff --git a/node/test/util/cherry_pick.js b/node/test/util/cherry_pick.js new file mode 100644 index 000000000..bc508d33c --- /dev/null +++ b/node/test/util/cherry_pick.js @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2021, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +"use strict"; + +const assert = require("chai").assert; +const co = require("co"); + +const CherryPick = require("../../lib/cmd/cherry_pick"); +const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); + +describe("isRange", function () { + it("handles non-ranges", function () { + assert(!CherryPick.isRange("")); + assert(!CherryPick.isRange("branch")); + assert(!CherryPick.isRange("refs/heads/branch")); + assert(!CherryPick.isRange("HEAD^3")); + }); + it("handles ranges", function () { + assert(CherryPick.isRange("x..y")); + assert(CherryPick.isRange("x...y")); + assert(CherryPick.isRange("x^@")); + assert(CherryPick.isRange("x^-1")); + assert(CherryPick.isRange("x^!")); + assert(CherryPick.isRange("^x")); + }); +}); + +describe("CherryPick", function () { + it("handles some common cases", co.wrap(function *() { + const start = `x=S:Cm1-1;Cm2-m1;Cm3-m2;Cm4-m3;Cm5-m4;Cm6-m5; + Bmaster=m6;Bm3=m3;Bm4=m4;Bm5=m5`; + const repoMap = yield RepoASTTestUtil.createMultiRepos(start); + const repo = repoMap.repos.x; + const byNumber = {}; + for (let i = 1; i <= 6; i++) { + byNumber[i] = yield repo.getCommit( + repoMap.reverseCommitMap["m" + i]); + } + let actual = yield CherryPick.resolveRange(repo, ["m5"]); + assert.deepEqual([byNumber[5]], actual); + + actual = yield CherryPick.resolveRange(repo, ["m4", "m5"]); + assert.deepEqual([byNumber[4], byNumber[5]], actual); + + actual = yield CherryPick.resolveRange(repo, ["m3..m5"]); + assert.deepEqual([byNumber[4], byNumber[5]], actual); + + actual = yield CherryPick.resolveRange(repo, ["^m3", "m5"]); + assert.deepEqual([byNumber[4], byNumber[5]], actual); + + actual = yield CherryPick.resolveRange(repo, ["m5^!"]); + assert.deepEqual([byNumber[5]], actual); + + actual = yield CherryPick.resolveRange(repo, ["m5^-"]); + assert.deepEqual([byNumber[5]], actual); + + assert.throws(() => CherryPick.resolveRange(repo, ["m5^@"]).done()); + })); +}); From 9ba33c8748e6723473e3146a7afc7950f3298044 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 25 Jan 2021 15:47:41 -0500 Subject: [PATCH 348/402] make git meta open open ts/foo if ts/foo/bar is requested --- node/lib/util/submodule_util.js | 15 ++++++++++++--- node/test/util/submodule_util.js | 14 +++++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 7d3b3cdf8..3739e2116 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -538,14 +538,21 @@ exports.getSubmodulesForCommit = co.wrap(function *(repo, commit, names) { * are a descendant of the specified `dir`, including (potentially) `dir` * itself (unless `dir` is suffixed with '/'). * + * if includeParents is true, submodules that would be parent + * directories of `dir` are are also included + * * @param {NodeGit.Repository} repo * @param {String} dir * @param {String []} indexSubNames + * @param {Boolean} includeParents * @return {String[]} */ -exports.getSubmodulesInPath = function (dir, indexSubNames) { +exports.getSubmodulesInPath = function (dir, indexSubNames, includeParents) { assert.isString(dir); assert.isArray(indexSubNames); + if (includeParents === undefined) { + includeParents = false; + } if ("" !== dir) { assert.notEqual("/", dir[0]); assert.notEqual(".", dir); @@ -555,7 +562,7 @@ exports.getSubmodulesInPath = function (dir, indexSubNames) { return indexSubNames; } - // test if the short path a parent dir of the long path + // test if the short path is a parent dir of the long path const isParentDir = (shortPath, longPath) => { return longPath.startsWith(shortPath) && ( shortPath[shortPath.length-1] === "/" || @@ -568,6 +575,8 @@ exports.getSubmodulesInPath = function (dir, indexSubNames) { return [dir]; // RETURN } else if (isParentDir(dir, subPath)) { result.push(subPath); + } else if (includeParents && isParentDir(subPath, dir)) { + result.push(subPath); } } return result; @@ -601,7 +610,7 @@ exports.resolveSubmoduleNames = function (workdir, const relPath = GitUtil.resolveRelativePath(workdir, cwd, filename); - const result = exports.getSubmodulesInPath(relPath, submoduleNames); + const result = exports.getSubmodulesInPath(relPath, submoduleNames, true); if (0 === result.length) { console.warn(`\ No submodules found from ${colors.yellow(filename)}.`); diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index cceed5d60..e9a3a32fb 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -788,13 +788,25 @@ describe("SubmoduleUtil", function () { indexSubNames: ["q/r", "qr", "qr/"], expected: ["q/r"], }, + "from a subdir without includeSubdirs": { + dir: "q/r/s", + indexSubNames: ["q/r", "qr", "qr/"], + expected: [], + }, + "from a subdirq with includeSubdirs": { + dir: "q/r/s", + indexSubNames: ["q/r", "qr", "qr/"], + expected: ["q/r"], + includeSubdirs: true, + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; it(caseName, function() { const result = SubmoduleUtil.getSubmodulesInPath( c.dir, - c.indexSubNames); + c.indexSubNames, + c.includeSubdirs); assert.deepEqual(result.sort(), c.expected.sort()); }); }); From 66a12d188ce33dc8f76766a1695ea4a0c4bf7cf5 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Tue, 2 Feb 2021 12:35:13 -0500 Subject: [PATCH 349/402] workAroundLibgit2MergeBug to skip creating synthetic ancestor if ancestor has the same sha as theirs --- node/lib/util/cherry_pick_util.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index 15206d5f4..b8c24ef01 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -282,10 +282,12 @@ const workAroundLibgit2MergeBug = co.wrap(function *(data, repo, name, for (const base of mergeBases) { const shas = yield SubmoduleUtil.getSubmoduleShasForCommit( repo, [ours.path], base); - if (shas[name] !== undefined) { - + const sha = shas[name]; + // Avoid creating a synthetic ancestor with the same sha as + // theirs. See more in `SubmoduleChange` + if (sha !== undefined && sha !== theirs.id.tostrS()) { ancestor = new NodeGit.IndexEntry(); - ancestor.id = NodeGit.Oid.fromString(shas[name]); + ancestor.id = NodeGit.Oid.fromString(sha); ancestor.mode = NodeGit.TreeEntry.FILEMODE.COMMIT; ancestor.path = ours.path; ancestor.flags = ours.flags; From 3d4364eb217c467ae0e47ad39389ed7e41e3667d Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 5 Feb 2021 14:28:12 -0500 Subject: [PATCH 350/402] Do not tell the user that we "finished" opening a submodule when we actually failed --- node/lib/cmd/open.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 4109e23fb..bd776cee6 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -197,6 +197,7 @@ Opening ${colors.blue(name)} on ${colors.green(shas[idx])}.`); templatePath, false); subsOpenSuccessfully.push(name); + console.log(`Finished opening ${colors.blue(name)}.`); } catch (e) { if (e instanceof UserError) { @@ -208,7 +209,6 @@ Opening ${colors.blue(name)} on ${colors.green(shas[idx])}.`); throw e; } } - console.log(`Finished opening ${colors.blue(name)}.`); }); yield DoWorkQueue.doInParallel(subsToOpen, opener, 10); From 3ff242853542323b43ee15c2a3966ed888d1c973 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 5 Feb 2021 14:31:15 -0500 Subject: [PATCH 351/402] format --- node/lib/util/submodule_util.js | 4 +++- node/test/util/submodule_util.js | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 3739e2116..ca766c724 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -610,7 +610,9 @@ exports.resolveSubmoduleNames = function (workdir, const relPath = GitUtil.resolveRelativePath(workdir, cwd, filename); - const result = exports.getSubmodulesInPath(relPath, submoduleNames, true); + const result = exports.getSubmodulesInPath(relPath, + submoduleNames, + true); if (0 === result.length) { console.warn(`\ No submodules found from ${colors.yellow(filename)}.`); diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index e9a3a32fb..811646e8d 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -804,9 +804,9 @@ describe("SubmoduleUtil", function () { const c = cases[caseName]; it(caseName, function() { const result = SubmoduleUtil.getSubmodulesInPath( - c.dir, - c.indexSubNames, - c.includeSubdirs); + c.dir, + c.indexSubNames, + c.includeSubdirs); assert.deepEqual(result.sort(), c.expected.sort()); }); }); From 24f08d9eea744631983046dd85837f7a84976f7a Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 1 Mar 2021 17:01:08 -0500 Subject: [PATCH 352/402] Make stash support commits inside submodules --- node/lib/cmd/stash.js | 11 +- node/lib/util/stash_util.js | 320 ++++++++++++++++++++++++++++------- node/test/util/stash_util.js | 12 +- 3 files changed, 283 insertions(+), 60 deletions(-) diff --git a/node/lib/cmd/stash.js b/node/lib/cmd/stash.js index b07823eaf..7ddfa2720 100644 --- a/node/lib/cmd/stash.js +++ b/node/lib/cmd/stash.js @@ -117,7 +117,16 @@ function cleanSubs(status, includeUntracked) { for (let subName in subs) { const sub = subs[subName]; const wd = sub.workdir; - if (null !== wd && !wd.status.isClean(includeUntracked)) { + if (sub.commit.sha !== sub.index.sha) { + // The submodule has a commit which is staged in the meta repo's + // index + return false; + } + if (null === wd) { + continue; + } + if ((!wd.status.isClean(includeUntracked)) || + wd.status.headCommit !== sub.commit.sha) { return false; } } diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index bc240e788..3b976f3cb 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -35,16 +35,19 @@ const co = require("co"); const colors = require("colors"); const NodeGit = require("nodegit"); -const ConfigUtil = require("./config_util"); -const GitUtil = require("./git_util"); -const Open = require("./open"); -const PrintStatusUtil = require("./print_status_util"); -const RepoStatus = require("./repo_status"); -const SparseCheckoutUtil = require("./sparse_checkout_util"); -const StatusUtil = require("./status_util"); -const SubmoduleUtil = require("./submodule_util"); -const TreeUtil = require("./tree_util"); -const UserError = require("./user_error"); +const ConfigUtil = require("./config_util"); +const GitUtil = require("./git_util"); +const Open = require("./open"); +const PrintStatusUtil = require("./print_status_util"); +const RepoStatus = require("./repo_status"); +const SparseCheckoutUtil = require("./sparse_checkout_util"); +const StatusUtil = require("./status_util"); +const SubmoduleUtil = require("./submodule_util"); +const SubmoduleRebaseUtil = require("./submodule_rebase_util"); +const TreeUtil = require("./tree_util"); +const UserError = require("./user_error"); + +const Commit = NodeGit.Commit; /** * Return the IDs of tress reflecting the current state of the index and @@ -117,6 +120,15 @@ WIP on ${branchDesc}: ${GitUtil.shortSha(head.id().tostrS())} ${message}`; * Return a map from submodule name to stashed commit for each submodule that * was stashed. * + * Normal stashes have up to two parents: + * 1. HEAD at stash time + * 2. a new commit, with tree = index at stash time + * + * Our stashes can have up to two additional parents: + * + * 3. If the user has a commit inside the submodule, that commit + * 4. If the user has staged a commit in the meta index, that commit + * * @param {NodeGit.Repository} repo * @param {RepoStatus} status * @param {Boolean} includeUntracked @@ -148,30 +160,159 @@ exports.save = co.wrap(function *(repo, status, includeUntracked, message) { yield Object.keys(submodules).map(co.wrap(function *(name) { const sub = submodules[name]; const wd = sub.workdir; - if (null === wd || - (wd.status.isClean() && - (sub.commit === null || - wd.status.headCommit === sub.commit.sha) && - (!includeUntracked || - 0 === Object.keys(wd.status.workdir).length))) { - // Nothing to do for closed or clean subs - - return; // RETURN - } - const subRepo = yield SubmoduleUtil.getRepo(repo, name); - subRepos[name] = subRepo; let stashId; - if (sub.commit !== null && wd.status.headCommit === sub.commit.sha) { - const FLAGS = NodeGit.Stash.FLAGS; - const flags = includeUntracked ? - FLAGS.INCLUDE_UNTRACKED : - FLAGS.DEFAULT; - stashId = yield NodeGit.Stash.save(subRepo, sig, "stash", - flags); + + if (sub.commit === null) { + // I genuinely have no idea when this happens -- it's not: + // (a) a closed submodule with a change staged in the meta repo + // (b) a submodule yet to be born -- that is, a submodule + // added to .gitmodules but without any commits. + // (c) a submodule which does not even appear inside the + // .gitmodules (e.g. one that you meant to add but didn't) + console.error(`BUG: ${name} is in an unexpected state. Please \ +report this. Continuing stash anyway.`); + return; } - else { - stashId = NodeGit.Oid.fromString(wd.status.headCommit); + + if (null === wd) { + // closed submodule + if (sub.commit.sha === sub.index.sha) { + // ... with no staged changes + return; // RETURN + } + // This is a case that regular git stash doesn't really have + // to handle. In a normal stash commit, the tree points + // to the working directory tree, but here, there is no working + // directory. But if there were, we would want to have + // this commit checked out. + + const subRepo = yield SubmoduleUtil.getRepo(repo, name); + + const subCommit = yield Commit.lookup(subRepo, sub.commit.sha); + const indexCommit = yield Commit.lookup(subRepo, sub.index.sha); + const indexTree = yield indexCommit.getTree(); + stashId = yield Commit.create(subRepo, + null, + sig, + sig, + null, + "stash", + indexTree, + 4, + [subCommit, + indexCommit, + indexCommit, + indexCommit]); + + } else { + // open submodule + if (sub.commit.sha !== sub.index.sha && + sub.index.sha !== wd.status.headCommit) { + // Giant mess case: the user has commit staged in the + // meta index, and new commits in the submodule (which + // may or may not be related to those staged in the + // index). + + // In theory, our data structures support writing this case, + // but since we don't yet support reading it, we probably + // shouldn't let the user get into a state that they can't + // easily get out of. + + throw new UserError(`${name} is in a state that is too \ +complicated for git-meta to handle right now. There is a commit inside the \ +submodule, and also a different commit staged in the index. Consider either \ +staging or unstaging ${name} in the meta repository`); + } + + const untrackedFiles = Object.keys(wd.status.workdir).length > 0; + + const uncommittedChanges = (!wd.status.isClean() || + (includeUntracked && untrackedFiles)); + + if (!uncommittedChanges && + wd.status.headCommit === sub.commit.sha && + sub.commit.sha === sub.index.sha) { + // Nothing to do for fully clean subs + return; // RETURN + } + + const subRepo = yield SubmoduleUtil.getRepo(repo, name); + subRepos[name] = subRepo; + + if (uncommittedChanges) { + const FLAGS = NodeGit.Stash.FLAGS; + const flags = includeUntracked ? + FLAGS.INCLUDE_UNTRACKED : + FLAGS.DEFAULT; + stashId = yield NodeGit.Stash.save(subRepo, sig, "stash", + flags); + if (wd.status.headCommit !== sub.commit.sha || + sub.commit.sha !== sub.index.sha) { + // That stashed the local changes in the submodule, if + // any. So now we need to mangle this commit to + // include more parents. + + const stashCommit = yield Commit.lookup(subRepo, stashId); + const stashTree = yield stashCommit.getTree(); + if (stashCommit.parentcount() !== 2) { + throw new Error(`BUG: expected newly-created stash \ +commit to have two parents`); + } + const parent1 = yield stashCommit.parent(0); + const parent2 = yield stashCommit.parent(1); + + const metaHeadSha = sub.commit.sha; + const headCommit = yield Commit.lookup(subRepo, + metaHeadSha); + const indexCommit = yield Commit.lookup(subRepo, + sub.index.sha); + + const parents = [parent1, parent2, headCommit, + indexCommit]; + stashId = yield Commit.create(subRepo, + null, + sig, + sig, + null, + "stash", + stashTree, + 4, + parents); + + } + + } else { + // we need to manually create the commit here. + const metaHead = yield Commit.lookup(subRepo, sub.commit.sha); + const head = yield Commit.lookup(subRepo, wd.status.headCommit); + const indexCommit = yield Commit.lookup(subRepo, + sub.index.sha); + + const parents = [metaHead, + metaHead, + head, + indexCommit]; + const headCommit = yield Commit.lookup(subRepo, + wd.status.headCommit); + const headTree = yield headCommit.getTree(); + + stashId = yield Commit.create(subRepo, + null, + sig, + sig, + null, + "stash", + headTree, + 4, + parents); + } + const subCommit = yield Commit.lookup(subRepo, + sub.commit.sha); + yield NodeGit.Checkout.tree(subRepo, subCommit, { + checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, + }); + subRepo.setHeadDetached(subCommit); } subResults[name] = stashId.tostrS(); // Record the values we've created. @@ -180,18 +321,19 @@ exports.save = co.wrap(function *(repo, status, includeUntracked, message) { stashId, NodeGit.TreeEntry.FILEMODE.COMMIT); })); + const head = yield repo.getHeadCommit(); const headTree = yield head.getTree(); const subsTree = yield TreeUtil.writeTree(repo, headTree, subChanges); - const stashId = yield NodeGit.Commit.create(repo, - null, - sig, - sig, - null, - "stash", - subsTree, - 1, - [head]); + const stashId = yield Commit.create(repo, + null, + sig, + sig, + null, + "stash", + subsTree, + 1, + [head]); const stashSha = stashId.tostrS(); @@ -310,6 +452,7 @@ exports.apply = co.wrap(function *(repo, id, reinstateIndex) { null); const opener = new Open.Opener(repo, null); let result = {}; + const index = {}; yield Object.keys(newSubs).map(co.wrap(function *(name) { const stashSha = newSubs[name].sha; if (baseSubs[name].sha === stashSha) { @@ -317,16 +460,15 @@ exports.apply = co.wrap(function *(repo, id, reinstateIndex) { return; // RETURN } - const subRepo = + let subRepo = yield opener.getSubrepo(name, Open.SUB_OPEN_OPTION.FORCE_OPEN); - // Try to get the comit for the stash; if it's missing, fail. - + // Try to get the commit for the stash; if it's missing, fail. + let stashCommit; try { - yield subRepo.getCommit(stashSha); - } - catch (e) { + stashCommit = yield Commit.lookup(subRepo, stashSha); + } catch (e) { console.error(`\ Stash commit ${colors.red(stashSha)} is missing from submodule \ ${colors.red(name)}`); @@ -334,6 +476,56 @@ ${colors.red(name)}`); return; // RETURN } + const indexCommit = yield stashCommit.parent(1); + + if (stashCommit.parentcount() > 2) { + const oldHead = yield stashCommit.parent(2); + if (stashCommit.parentcount() > 3) { + const stagedCommit = yield stashCommit.parent(3); + index[name] = stagedCommit.id(); + } + + // Before we get started, we might need to rebase the + // commits from oldHead..commitBeforeStash + + const commitBeforeStash = yield stashCommit.parent(0); + + const rebaseError = function(resolution) { + console.error(`The stash for submodule ${name} had one or \ +more commits, ending with ${commitBeforeStash.id()}. We tried to rebase these \ +commits onto the current commit, but this failed. ${resolution} +After you are done with this rebase, you may need to apply working tree \ +and index changes. To restore the index, try (inside ${name}) 'git read-tree \ +${indexCommit.id()}'. To restore the working tree, try (inside ${name}) \ +'git checkout ${stashCommit} -- .'`); + }; + + try { + const res = yield SubmoduleRebaseUtil.rewriteCommits( + subRepo, + commitBeforeStash, + oldHead); + if (res.errorMessage) { + rebaseError(res.errorMessage); + result = null; + return; + } + } catch (e) { + // We expect these errors to be caught, but if + // something goes wrong, wWe are leaving the user in a + // pretty yucky state. the alternative is to try to + // back out the whole stash apply, which seems worse. + + rebaseError(`We are leaving the rebase half-finished, so \ +that you can fix the conflicts and continue by running 'git rebase \ +--continue' inside of ${name} (note: not 'git meta rebase').`); + console.error(`The underlying error, which might be useful \ +for debugging, is:`, e); + result = null; + return; + } + } + // Make sure this sha is the current stash. yield exports.setStashHead(subRepo, stashSha); @@ -356,8 +548,20 @@ ${colors.red(name)}`); result[name] = stashSha; } })); - yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, - yield repo.index()); + + const repoIndex = yield repo.index(); + + if (null !== result) { + for (let name of Object.keys(index)) { + const entry = new NodeGit.IndexEntry(); + entry.flags = 0; + entry.flagsExtended = 0; + entry.id = index[name]; + repoIndex.add(entry); + } + } + + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, repoIndex); return result; }); @@ -524,7 +728,7 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, const tree = yield index.writeTree(); const sig = yield ConfigUtil.defaultSignature(repo); const subCommit = yield repo.createCommit(null, sig, sig, - message, tree, []); + message, tree, []); return subCommit.tostrS(); } @@ -554,15 +758,15 @@ const makeShadowCommitForRepo = co.wrap(function *(repo, head.time() + 1, head.timeOffset()); } - const id = yield NodeGit.Commit.create(repo, - null, - sig, - sig, - null, - message, - newTree, - parents.length, - parents); + const id = yield Commit.create(repo, + null, + sig, + sig, + null, + message, + newTree, + parents.length, + parents); return id.tostrS(); }); diff --git a/node/test/util/stash_util.js b/node/test/util/stash_util.js index 106ff896b..5f07b8d2b 100644 --- a/node/test/util/stash_util.js +++ b/node/test/util/stash_util.js @@ -224,7 +224,7 @@ x=E:Ci#i foo=bar,1=1;Cw#w foo=bar,1=1;Bi=i;Bw=w`, state: `a=B:Css-1;Bss=ss| x=S:C2-1 README.md,s=Sa:1;Bmaster=2;Os H=ss`, expected: `x=E:Cstash#s-2 s=Sa:ss;Fmeta-stash=s; - Os Fsub-stash/ss=ss!H=ss`, + Os Fsub-stash/ss=ss!H=1`, }, "open sub with an added file": { state: "a=B|x=S:C2-1 README.md,s=Sa:1;Bmaster=2;Os W foo=bar", @@ -338,6 +338,16 @@ x=E:Fmeta-stash=s; } } + // In some cases, the first or second parent might be + // an existing commit, but the test framework only + // wants commitMap to contain commits it has never + // seen, so we have to remove the ones that already + // existed. + for (const c of Object.keys(commitMap)) { + if (c in mapping.commitMap) { + delete commitMap[c]; + } + } return { commitMap: commitMap, }; From 9dd049321e1cc2e3cc37c1b2be9eb8a290b61a8c Mon Sep 17 00:00:00 2001 From: tsuikevi-twosigma Date: Fri, 12 Mar 2021 20:29:26 +0000 Subject: [PATCH 353/402] support '-uno' in git-meta-status --- node/lib/cmd/status.js | 17 +++++++++++ node/lib/util/add.js | 3 +- node/lib/util/commit.js | 16 +++++++++-- node/lib/util/diff_util.js | 54 +++++++++++++++++++++++------------ node/lib/util/stash_util.js | 3 +- node/lib/util/status_util.js | 42 ++++++++++++++------------- node/test/util/diff_util.js | 29 ++++++++++--------- node/test/util/status_util.js | 12 ++++++-- 8 files changed, 116 insertions(+), 60 deletions(-) diff --git a/node/lib/cmd/status.js b/node/lib/cmd/status.js index 43e8ac265..abcd371ea 100644 --- a/node/lib/cmd/status.js +++ b/node/lib/cmd/status.js @@ -32,6 +32,8 @@ const co = require("co"); +const DiffUtil = require("../util/diff_util"); + /** * This module contains methods for pulling. */ @@ -63,6 +65,20 @@ exports.configureParser = function (parser) { help: "Give the output in a short format", dest: "shortFormat" //"short" is a reserved word in js }); + parser.addArgument(["-u", "--untracked-files"], { + required: false, + choices: [ + DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, + DiffUtil.UNTRACKED_FILES_OPTIONS.NORMAL, + DiffUtil.UNTRACKED_FILES_OPTIONS.NO, + ], + constant: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, + defaultValue: DiffUtil.UNTRACKED_FILES_OPTIONS.NORMAL, + help: `show untracked files, optional modes: all, normal, no. + (Default:normal)`, + dest: "untrackedFilesOption", + nargs: "?", + }); parser.addArgument(["path"], { type: "string", help: "paths to inspect for changes", @@ -91,6 +107,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { const repoStatus = yield StatusUtil.getRepoStatus(repo, { cwd: cwd, paths: args.path, + untrackedFilesOption: args.untrackedFilesOption }); // Compute the current directory relative to the working directory of the diff --git a/node/lib/util/add.js b/node/lib/util/add.js index 05175ef1a..2298a6ada 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -37,6 +37,7 @@ const fs = require("fs-promise"); const NodeGit = require("nodegit"); const path = require("path"); +const DiffUtil = require("./diff_util"); const RepoStatus = require("./repo_status"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const StatusUtil = require("./status_util"); @@ -72,7 +73,7 @@ exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update) { const repoStatus = yield StatusUtil.getRepoStatus(repo, { showMetaChanges: stageMetaChanges, paths: paths, - showAllUntracked: true, + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, }); // First, stage submodules. diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index d906c16ba..08f3060dc 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -96,7 +96,12 @@ const getHeadParentTree = co.wrap(function *(repo) { const getAmendStatusForRepo = co.wrap(function *(repo, all) { const tree = yield getHeadParentTree(repo); - const normal = yield DiffUtil.getRepoStatus(repo, tree, [], false, true); + const normal = yield DiffUtil.getRepoStatus( + repo, + tree, + [], + false, + DiffUtil.UNTRACKED_FILES_OPTIONS.ALL); if (!all) { return normal; // RETURN @@ -106,7 +111,12 @@ const getAmendStatusForRepo = co.wrap(function *(repo, all) { // We've already got the "normal" comparison now we need to get changes // directly against the workdir. - const toWorkdir = yield DiffUtil.getRepoStatus(repo, tree, [], true, true); + const toWorkdir = yield DiffUtil.getRepoStatus( + repo, + tree, + [], + true, + DiffUtil.UNTRACKED_FILES_OPTIONS.ALL); // And use `calculateAllStatus` to create the final value. @@ -1900,7 +1910,7 @@ exports.getCommitStatus = co.wrap(function *(repo, cwd, options) { const workdirStatus = yield StatusUtil.getRepoStatus(repo, { showMetaChanges: options.showMetaChanges, ignoreIndex: true, - showAllUntracked: true, + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, }); return exports.calculateAllRepoStatus(baseStatus, workdirStatus); } diff --git a/node/lib/util/diff_util.js b/node/lib/util/diff_util.js index a3a67d8a9..9c587976c 100644 --- a/node/lib/util/diff_util.js +++ b/node/lib/util/diff_util.js @@ -38,6 +38,12 @@ const RepoStatus = require("./repo_status"); const DELTA = NodeGit.Diff.DELTA; +exports.UNTRACKED_FILES_OPTIONS = { + ALL: "all", + NORMAL: "normal", + NO: "no", +}; + /** * Return the `RepoStatus.FILESTATUS` value that corresponds to the specified * flag. The behavior is undefined unless `flag` represents one of the types @@ -94,22 +100,22 @@ function readDiff(diff) { * * Return differences for the specified `paths` in the specified `repo` between * the current index and working directory, and the specified `tree`, if - * not null. If the specified `allUntracked` is true, include all untracked - * files rather than accumulating them by directory. If `paths` is empty, - * check the entire `repo`. If the specified `ignoreIndex` is true, - * return, in the `workdir` field, the status difference between the workdir - * and `tree`, ignoring the state of the index. Otherwise, return, in the - * `workdir` field, the difference between the workir and the index; and in the - * `staged` field, the difference between the index and `tree`. Note that when - * `ignoreIndex` is true, the returned `staged` field will always be `{}`. - * Note also that conflicts are ignored; we don't have enough information here - * to handle them properly. + * not null. If the specified `untrackedFilesOption` is ALL, include all + * untracked files. If it is NORMAL, accumulate them by directory. If it is NO, + * don't show untracked files. If `paths` is empty, check the entire `repo`. + * If the specified `ignoreIndex` is true, return, in the `workdir` field, the + * status difference between the workdir and `tree`, ignoring the state of the + * index. Otherwise, return, in the `workdir` field, the difference between + * the workir and the index; and in the `staged` field, the difference between + * the index and `tree`. Note that when `ignoreIndex` is true, the returned + * `staged` field will always be `{}`. Note also that conflicts are ignored; we + * don't have enough information here to handle them properly. * * @param {NodeGit.Repository} repo * @param {NodeGit.Tree|null} tree * @param {String []} paths * @param {Boolean} ignoreIndex - * @param {Boolean} allUntracked + * @param {String} untrackedFilesOption * @return {Object} * @return {Object} return.staged path to FILESTATUS of staged changes * @return {Object} return.workdir path to FILESTATUS of workdir changes @@ -118,27 +124,39 @@ exports.getRepoStatus = co.wrap(function *(repo, tree, paths, ignoreIndex, - allUntracked) { + untrackedFilesOption) { assert.instanceOf(repo, NodeGit.Repository); if (null !== tree) { assert.instanceOf(tree, NodeGit.Tree); } assert.isArray(paths); assert.isBoolean(ignoreIndex); - assert.isBoolean(allUntracked); + if (!untrackedFilesOption) { + untrackedFilesOption = exports.UNTRACKED_FILES_OPTIONS.NORMAL; + } const options = { ignoreSubmodules: 1, - flags: NodeGit.Diff.OPTION.INCLUDE_UNTRACKED | - NodeGit.Diff.OPTION.IGNORE_SUBMODULES, + flags: NodeGit.Diff.OPTION.IGNORE_SUBMODULES, }; if (0 !== paths.length) { options.pathspec = paths; } - if (allUntracked) { - options.flags = options.flags | - NodeGit.Diff.OPTION.RECURSE_UNTRACKED_DIRS; + + switch (untrackedFilesOption) { + case exports.UNTRACKED_FILES_OPTIONS.ALL: + options.flags = options.flags | + NodeGit.Diff.OPTION.INCLUDE_UNTRACKED | + NodeGit.Diff.OPTION.RECURSE_UNTRACKED_DIRS; + break; + case exports.UNTRACKED_FILES_OPTIONS.NORMAL: + options.flags = options.flags | + NodeGit.Diff.OPTION.INCLUDE_UNTRACKED; + break; + case exports.UNTRACKED_FILES_OPTIONS.NO: + break; } + if (ignoreIndex) { const workdirToTreeDiff = yield NodeGit.Diff.treeToWorkdir(repo, tree, options); diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 3b976f3cb..34b257502 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -36,6 +36,7 @@ const colors = require("colors"); const NodeGit = require("nodegit"); const ConfigUtil = require("./config_util"); +const DiffUtil = require("./diff_util"); const GitUtil = require("./git_util"); const Open = require("./open"); const PrintStatusUtil = require("./print_status_util"); @@ -819,7 +820,7 @@ exports.makeShadowCommit = co.wrap(function *(repo, const status = yield StatusUtil.getRepoStatus(repo, { showMetaChanges: includeMeta, - showAllUntracked: true, + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, ignoreIndex: false, paths: includedSubrepos, }); diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index fb42247fd..8e9ad32b0 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -440,7 +440,7 @@ const getSubRepoStatus = co.wrap(function *(repo, options, headCommit) { subRepo = yield SubmoduleUtil.getRepo(repo, name); status = yield exports.getRepoStatus(subRepo, { paths: filtering ? filterPaths[name] : [], - showAllUntracked: options.showAllUntracked, + untrackedFilesOption: options.untrackedFilesOption, ignoreIndex: options.ignoreIndex, showMetaChanges: true, }); @@ -467,16 +467,17 @@ const getSubRepoStatus = co.wrap(function *(repo, options, headCommit) { /** * Return a description of the status of changes to the specified `repo`. If - * the optionally specified `options.showAllUntracked` is true (default false), - * return each untracked file individually rather than rolling up to the - * directory. If the optionally specified `options.paths` is non-empty - * (default []), list the status only of the files contained in `paths`. If - * the optionally specified `options.showMetaChanges` is provided (default - * true), return the status of changes in `repo`; otherwise, show only changes - * in submodules. If the optionally specified `ignoreIndex` is specified, - * calculate the status matching the workdir to the underlying commit rather - * than against the index. If the specified `options.cwd` is provided, resolve - * paths in the context of that directory. + * the optionally specified `options.untrackedFilesOption` is ALL (default + * ALL), return each untracked file individually. If it is NORMAL, roll + * untracked files up to the directory. If it is NO, don't show untracked files. + * If the optionally specified `options.paths` is non-empty (default []), list + * the status only of the files contained in `paths`. If the optionally + * specified `options.showMetaChanges` is provided (default true), return the + * status of changes in `repo`; otherwise, show only changes in submodules. If + * the optionally specified `ignoreIndex` is specified, calculate the status + * matching the workdir to the underlying commit rather than against the index. + * If the specified `options.cwd` is provided, resolve paths in the context of + * that directory. * * TODO: Note that this function is broken when * `true === ignoreIndex && true === showMetaChanges` and @@ -487,7 +488,7 @@ const getSubRepoStatus = co.wrap(function *(repo, options, headCommit) { * @async * @param {NodeGit.Repository} repo * @param {Object} [options] - * @param {Boolean} [options.showAllUntracked] + * @param {String} [options.untrackedFilesOption] * @param {String []} [options.paths] * @param {String} [options.cwd] * @param {Boolean} [options.showMetaChanges] @@ -505,11 +506,11 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { else { assert.isObject(options); } - if (undefined === options.showAllUntracked) { - options.showAllUntracked = false; + if (undefined === options.untrackedFilesOption) { + options.untrackedFilesOption = DiffUtil.UNTRACKED_FILES_OPTIONS.NORMAL; } else { - assert.isBoolean(options.showAllUntracked); + assert.isString(options.untrackedFilesOption); } if (undefined === options.paths) { options.paths = []; @@ -574,11 +575,12 @@ exports.getRepoStatus = co.wrap(function *(repo, options) { if (!options.showMetaChanges && (!paths || paths.length === 0)) { paths = [SubmoduleConfigUtil.modulesFileName]; } - const status = yield DiffUtil.getRepoStatus(repo, - tree, - paths, - options.ignoreIndex, - options.showAllUntracked); + const status = yield DiffUtil.getRepoStatus( + repo, + tree, + paths, + options.ignoreIndex, + options.untrackedFilesOption); // if showMetaChanges is off, keep .gitmodules changes only if (options.showMetaChanges) { args.staged = status.staged; diff --git a/node/test/util/diff_util.js b/node/test/util/diff_util.js index 7465eb86f..b1a946954 100644 --- a/node/test/util/diff_util.js +++ b/node/test/util/diff_util.js @@ -129,7 +129,7 @@ describe("DiffUtil", function () { }, "workdir - added deep all untracked": { input: "x=S:W x/y=y", - all: true, + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, workdir: { "x/y": FILESTATUS.ADDED }, }, "workdir - removed": { @@ -207,7 +207,7 @@ describe("DiffUtil", function () { "workdir dir path": { input: "x=S:W x/y/z=foo,x/r/z=bar,README.md", paths: [ "x" ], - all: true, + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, workdir: { "x/y/z": FILESTATUS.ADDED, "x/r/z": FILESTATUS.ADDED @@ -216,7 +216,7 @@ describe("DiffUtil", function () { "workdir dir paths": { input: "x=S:W x/y/z=foo,x/r/z=bar,README.md", paths: [ "x/y", "x/r" ], - all: true, + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, workdir: { "x/y/z": FILESTATUS.ADDED, "x/r/z": FILESTATUS.ADDED @@ -225,7 +225,7 @@ describe("DiffUtil", function () { "workdir all paths": { input: "x=S:W x/y/z=foo,x/r/z=bar,README.md", paths: [ "x/y/z", "x/r/z", "README.md" ], - all: true, + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, workdir: { "x/y/z": FILESTATUS.ADDED, "x/r/z": FILESTATUS.ADDED, @@ -235,7 +235,7 @@ describe("DiffUtil", function () { "many changes": { input: ` x=S:C2 a/b=c,a/c=d,t=u;H=2;I a/b,a/q=r,f=x;W a/b=q,a/c=f,a/y=g,f`, - all: true, + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, workdir: { "a/b": FILESTATUS.ADDED, "a/c": FILESTATUS.MODIFIED, @@ -251,7 +251,7 @@ x=S:C2 a/b=c,a/c=d,t=u;H=2;I a/b,a/q=r,f=x;W a/b=q,a/c=f,a/y=g,f`, "many changes with path": { input: ` x=S:C2 a/b=c,a/c=d,t=u;H=2;I a/b,a/q=r,f=x;W a/b=q,a/c=f,a/y=g,f`, - all: true, + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, paths: ["f"], workdir: { "f": FILESTATUS.REMOVED, @@ -281,7 +281,7 @@ x=S:C2 a/b=c,a/c=d,t=u;H=2;I a/b,a/q=r,f=x;W a/b=q,a/c=f,a/y=g,f`, input: "x=S:C2-1;W README.md=3;Bmaster=2", staged: { "2": FILESTATUS.ADDED }, workdir: { "README.md": FILESTATUS.MODIFIED }, - all: true, + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, from: "HEAD^", }, "HEAD^ changed in index": { @@ -325,7 +325,7 @@ x=S:C2 a/b=c,a/c=d,t=u;H=2;I a/b,a/q=r,f=x;W a/b=q,a/c=f,a/y=g,f`, "2": FILESTATUS.ADDED, "README.md": FILESTATUS.REMOVED, }, - all: true, + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, from: "HEAD^", }, "HEAD^ removed in workdir": { @@ -346,7 +346,7 @@ x=S:C2 a/b=c,a/c=d,t=u;H=2;I a/b,a/q=r,f=x;W a/b=q,a/c=f,a/y=g,f`, workdir: { "README.md": FILESTATUS.REMOVED, }, - all: true, + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, from: "HEAD^", }, "HEAD^ ignore submodule add": { @@ -424,11 +424,12 @@ x=S:C2-1 README.md=3;W README.md=hello world;Bmaster=2`, tree = yield NodeGit.Tree.lookup(repo, treeId); } const result = yield DiffUtil.getRepoStatus( - repo, - tree, - c.paths || [], - c.workdirToTree || false, - c.all || false); + repo, + tree, + c.paths || [], + c.workdirToTree || false, + c.untrackedFilesOption || + DiffUtil.UNTRACKED_FILES_OPTIONS.NORMAL); const expected = { staged: c.staged || {}, workdir: c.workdir || {}, diff --git a/node/test/util/status_util.js b/node/test/util/status_util.js index f2cceaf17..50a926fba 100644 --- a/node/test/util/status_util.js +++ b/node/test/util/status_util.js @@ -35,6 +35,7 @@ const co = require("co"); const path = require("path"); const NodeGit = require("nodegit"); +const DiffUtil = require("../../lib/util/diff_util"); const Rebase = require("../../lib/util/rebase"); const RepoAST = require("../../lib/util/repo_ast"); const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util"); @@ -614,7 +615,10 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, "x/y/q": FILESTATUS.ADDED, }, }), - options: { showAllUntracked: true, showMetaChanges: true }, + options: { + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, + showMetaChanges: true + }, }, "ignore meta": { state: "x=S:I README.md=whoohoo", @@ -659,7 +663,7 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, // // - make sure that they're included, even if they have been // removed in the index or added in the index - // - `showAllUntracked` propagates + // - `untrackedFilesOption` propagates // - path filtering works "sub no show all added": { @@ -699,7 +703,9 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`, }), }, }), - options: { showAllUntracked: true, }, + options: { + untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL, + }, }, "sub added to index": { state: "a=S|x=S:I s=Sa:1", From 711d5302d2c45af78529a3b23a053b02e73902a0 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 16 Mar 2021 11:59:18 -0400 Subject: [PATCH 354/402] Support an alternate submodule repository for stitching --- doc/stitch.md | 69 ++++++++++++++++++++++++++++++++++++ node/lib/util/stitch_util.js | 21 ++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 doc/stitch.md diff --git a/doc/stitch.md b/doc/stitch.md new file mode 100644 index 000000000..e4bc335e5 --- /dev/null +++ b/doc/stitch.md @@ -0,0 +1,69 @@ + + +# Overview + +The stitcher stitches a git-meta repository into a single unified +repository (perhaps leaving behind some submodules, depending on +configuration). + +# Motivation + +You might want this because you are migrating away from git-meta. Or +you might just want a unified repo to power code search or code review +tooling. + +There's also a destitched, which reverses the process. Of course, +that's a little trickier: if you create a new subdirectory which +itself contains two subdirectories, how many sub*modules* do you +create? + +# An implementation note + +The most efficient repository layout that we have yet discovered for +stitching has three repositories: +1. just the meta objects +2. just the submodule objects +3. the stitched commits, which has objects/info/alternates pointed at +(1) and (2). + +This makes fetches from the meta repo faster, without making other +fetches slower. + +If you can managed to remove the alternates for meta commits during +pushes from the local unity repo to a remote one, that's even faster. + +The configuration variable gitmeta.stitchSubmodulesRepository can hold +the path to the submodule-only repo; if it's present, the stitcher +assumes that the alternates configuration is set up correctly. + +We have not yet considered the efficiency of destitching, and it does +not support this configuration variable. \ No newline at end of file diff --git a/node/lib/util/stitch_util.js b/node/lib/util/stitch_util.js index f22715093..fcf5adf08 100644 --- a/node/lib/util/stitch_util.js +++ b/node/lib/util/stitch_util.js @@ -36,6 +36,7 @@ const NodeGit = require("nodegit"); const BulkNotesUtil = require("./bulk_notes_util"); const Commit = require("./commit"); +const ConfigUtil = require("./config_util"); const DoWorkQueue = require("./do_work_queue"); const GitUtil = require("./git_util"); const SubmoduleConfigUtil = require("./submodule_config_util"); @@ -961,13 +962,31 @@ exports.stitch = co.wrap(function *(repoPath, adjustPath); console.log("Found", Object.keys(fetches).length, "subs to fetch."); const subNames = Object.keys(fetches); + let subsRepo = repo; + const config = yield repo.config(); + /* + * The stitch submodules repository is a separate repository + * to hold just the submodules that are being stitched (no + * meta repository commits). It must be an alternate + * of this repository (the repository that stitching is + * being done in), so that we can access the objects + * that we fetch to it. This lets us have a second alternate + * repository for just the meta repository commits. And that + * saves a few seconds on meta repository fetches. + */ + const subsRepoPath = yield ConfigUtil.getConfigString( + config, + "gitmeta.stitchSubmodulesRepository"); + if (subsRepoPath !== null) { + subsRepo = yield NodeGit.Repository.open(subsRepoPath); + } const doFetch = co.wrap(function *(name, i) { const subFetches = fetches[name]; const fetchTimeMessage = `\ (${i + 1}/${subNames.length}) -- fetched ${subFetches.length} SHAs for \ ${name}`; console.time(fetchTimeMessage); - yield exports.fetchSubCommits(repo, name, url, subFetches); + yield exports.fetchSubCommits(subsRepo, name, url, subFetches); console.timeEnd(fetchTimeMessage); }); yield DoWorkQueue.doInParallel(subNames, From 3796d6b4975769b3bceacecfac08f74dd5628cf8 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 16 Mar 2021 16:33:19 -0400 Subject: [PATCH 355/402] Fail git meta open if you request a sub that does not exist --- node/lib/cmd/open.js | 3 ++- node/lib/util/submodule_util.js | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index bd776cee6..090ccc1d4 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -156,7 +156,8 @@ exports.executeableSubcommand = co.wrap(function *(args) { let subsToOpen = yield SubmoduleUtil.resolveSubmoduleNames(workdir, cwd, subs, - paths); + paths, + true); subsToOpen = Array.from(new Set(subsToOpen)); const index = yield repo.index(); const shas = yield SubmoduleUtil.getCurrentSubmoduleShas(index, diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index ca766c724..f88bb9589 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -586,19 +586,22 @@ exports.getSubmodulesInPath = function (dir, indexSubNames, includeParents) { * Return the list of submodules found in the specified `paths` in the * specified meta-repo `workdir`, containing the submodules having the * specified `submoduleNames`. Treat paths as being relative to the specified - * `cwd`. Throw a `UserError` if an invalid path is encountered, and log - * warnings for valid paths containing no submodules. + * `cwd`. Throw a `UserError` if an path outside of the workdir is + * encountered. If a path inside the workdir contains no submodules, + * either log a warning, or, if throwOnMissing is set, throw a `UserError`. * * @param {String} workdir * @param {String} cwd * @param {String[]} submoduleNames * @param {String[]} paths + * @param {Boolean} throwOnMissing * @return {String[]} */ exports.resolveSubmoduleNames = function (workdir, cwd, submoduleNames, - paths) { + paths, + throwOnMissing) { assert.isString(workdir); assert.isString(cwd); assert.isArray(submoduleNames); @@ -614,8 +617,13 @@ exports.resolveSubmoduleNames = function (workdir, submoduleNames, true); if (0 === result.length) { - console.warn(`\ -No submodules found from ${colors.yellow(filename)}.`); + const msg = `\ +No submodules found from ${colors.yellow(filename)}.`; + if (throwOnMissing) { + throw new UserError(msg); + } else { + console.warn(msg); + } } return result; }); From 692f242d6994ff168d847f8fffb431c2a81b0394 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 8 Apr 2021 11:32:28 -0400 Subject: [PATCH 356/402] Fail git meta close if you request a sub that does not exist, instead of closing the current sub(!) This was probably caused by a change to `git meta open` to open foo/bar if you request foo/bar/baz. That's OK for open, but it's not great for close. --- node/lib/cmd/open.js | 1 + node/lib/util/git_util.js | 3 +++ node/lib/util/submodule_util.js | 5 +++-- node/test/util/close_util.js | 5 +++++ node/test/util/submodule_util.js | 4 +++- 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 090ccc1d4..35c82e51d 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -157,6 +157,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { cwd, subs, paths, + true, true); subsToOpen = Array.from(new Set(subsToOpen)); const index = yield repo.index(); diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 06b137926..40b961ec2 100755 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -741,6 +741,9 @@ exports.resolveRelativePath = function (workdir, cwd, filename) { assert.isString(cwd); assert.isString(filename); + if (filename === "") { + throw new UserError(`Empty path`); + } const absPath = path.resolve(cwd, filename); const relPath = path.relative(workdir, absPath); if ("" !== relPath && "." === relPath[0]) { diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index f88bb9589..6e14dbb28 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -601,7 +601,8 @@ exports.resolveSubmoduleNames = function (workdir, cwd, submoduleNames, paths, - throwOnMissing) { + throwOnMissing, + includeParents) { assert.isString(workdir); assert.isString(cwd); assert.isArray(submoduleNames); @@ -615,7 +616,7 @@ exports.resolveSubmoduleNames = function (workdir, filename); const result = exports.getSubmodulesInPath(relPath, submoduleNames, - true); + includeParents); if (0 === result.length) { const msg = `\ No submodules found from ${colors.yellow(filename)}.`; diff --git a/node/test/util/close_util.js b/node/test/util/close_util.js index a79f5d2d5..c37659bb1 100644 --- a/node/test/util/close_util.js +++ b/node/test/util/close_util.js @@ -118,6 +118,11 @@ a/b/c/d/e/s4=Sa:1;Bmaster=2`, expected: "x=S:C2-1 s=Sa:1,t=Sa:1;Bmaster=2;Os I a=b", fails: true, }, + "a/b doesn't close a": { + state: "a=B|x=U:Os", + paths: ["s/t"], + expected: "a=B|x=U:Os", + }, }; Object.keys(cases).forEach(caseName => { for (const sparse of [true, false]) { diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index 811646e8d..cca25109f 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -862,7 +862,9 @@ describe("SubmoduleUtil", function () { x.workdir(), cwd, subs, - paths); + paths, + false, + true); } catch (e) { if (!(e instanceof UserError)) { From 9bc87c89a97730505ebce56f0fe36cf47ee0e234 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 14 Apr 2021 22:38:26 -0400 Subject: [PATCH 357/402] Use better notes ref update method First, add retries. But second, there was a TOCTOU race in updating. In order to fix this, I first tried to use existing NodeGit methods, but they call into a function of libgit2 which didn't support asserting that a ref was new. I filed a PR against libgit2 to fix this, but of course it will take some time to get merged. So in the meantime, we shell out to git. --- node/lib/util/bulk_notes_util.js | 94 +++++++++++++++++++++++++------- 1 file changed, 73 insertions(+), 21 deletions(-) diff --git a/node/lib/util/bulk_notes_util.js b/node/lib/util/bulk_notes_util.js index 5d8b18e7d..53a3ff52f 100644 --- a/node/lib/util/bulk_notes_util.js +++ b/node/lib/util/bulk_notes_util.js @@ -31,6 +31,7 @@ "use strict"; const assert = require("chai").assert; +const ChildProcess = require("child-process-promise"); const co = require("co"); const NodeGit = require("nodegit"); const path = require("path"); @@ -53,29 +54,25 @@ exports.shardSha = function (sha) { }; /** - * Write the specified `contents` to the note having the specified `refName` in - * the specified `repo`. - * - * Writing notes oneo-at-a-time is slow. This method let's you write them in - * bulk, far more efficiently. - * - * @param {NodeGit.Repository} repo - * @param {String} refName - * @param {Object} contents SHA to data + * This is a workaround for a missing libgit2 feature. Normally, we + * would use something like Reference.createMatching, but it doesn't support + * asserting that a ref didn't previously exist. See + * https://github.com/libgit2/libgit2/pull/5842 */ -exports.writeNotes = co.wrap(function *(repo, refName, contents) { - assert.instanceOf(repo, NodeGit.Repository); - assert.isString(refName); - assert.isObject(contents); - - if (0 === Object.keys(contents).length) { - // Nothing to do if no contents; no point in making an empty commit or - // in making clients check themselves. - return; // RETURN +const updateRef = co.wrap(function*(repo, refName, commit, old, reflog) { + try { + yield ChildProcess.exec( + `git -C ${repo.path()} update-ref -m '${reflog}' ${refName} \ +${commit} ${old}`); + return true; + } catch (e) { + return false; } +}); - // We're going to directly write the tree/commit for a new note containing - // `contents`. +const tryWriteNotes = co.wrap(function *(repo, refName, contents) { + // We're going to directly write the tree/commit for a new + // note containing `contents`. let currentTree = null; const parents = []; @@ -108,9 +105,64 @@ exports.writeNotes = co.wrap(function *(repo, refName, contents) { newTree, parents.length, parents); - yield NodeGit.Reference.create(repo, refName, commit, 1, "updated"); + + let old; + if (null === ref) { + old = "0000000000000000000000000000000000000000"; + } else { + old = ref.target().tostrS(); + } + return yield updateRef(repo, refName, commit.tostrS(), old, "updated"); }); +/** + * Write the specified `contents` to the note having the specified `refName` in + * the specified `repo`. + * + * Writing notes oneo-at-a-time is slow. This method let's you write them in + * bulk, far more efficiently. + * + * @param {NodeGit.Repository} repo + * @param {String} refName + * @param {Object} contents SHA to data + */ +exports.writeNotes = co.wrap(function *(repo, refName, contents) { + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(refName); + assert.isObject(contents); + + if (0 === Object.keys(contents).length) { + // Nothing to do if no contents; no point in making an empty commit or + // in making clients check themselves. + return; // RETURN + } + + const retryCount = 3; + let success; + function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + for (let i = 0; i < retryCount; i++) { + success = yield tryWriteNotes(repo, refName, contents); + if (success) { + return; + } else { + let suffix; + if (i === retryCount - 1) { + suffix = "giving up"; + } else { + suffix = "retrying"; + yield sleep(500); + } + console.warn(`Failed to update notes ref ${refName}, ${suffix}`); + } + } + if (!success) { + throw new Error("Failed to update notes ref ${refName} after retries"); + } +}); + + /** * Load, into the specified `result`, note entries found in the specified * `tree`, prefixing their key with the specified `basePath`. If subtrees are From 056367e77b820f7d0e68512ee5c3788fd1df71ab Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 27 Apr 2021 14:39:38 -0400 Subject: [PATCH 358/402] stitch: Handle missing converted commit Handle the case where a notes is present but the converted commit to is not present. This can happen if you are stitching and pushing notes in parallel: it's possible that you push a note for a commit that doesn't end up getting pushed. That would cause later conversions which fetch the notes to fail, unless we handle that case. --- node/lib/util/stitch_util.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/node/lib/util/stitch_util.js b/node/lib/util/stitch_util.js index fcf5adf08..58d913242 100644 --- a/node/lib/util/stitch_util.js +++ b/node/lib/util/stitch_util.js @@ -778,8 +778,15 @@ exports.readConvertedCommits = co.wrap(function *(repo) { const result = yield BulkNotesUtil.readNotes(repo, exports.convertedNoteRef); - for (let key in result) { - result[key] = exports.readConvertedContent(result[key]); + for (const [key, oldSha] of Object.entries(result)) { + const sha = exports.readConvertedContent(oldSha); + try { + yield repo.getCommit(sha); + result[key] = sha; + } catch (e) { + // We have the note but not the commit, delete from cache + delete result[key]; + } } return result; }); @@ -804,6 +811,14 @@ exports.makeGetConvertedCommit = function (repo, cache) { return cache[sha]; } const result = yield exports.readConvertedCommit(repo, sha); + + try { + yield repo.getCommit(result); + } catch (e) { + // We have the note but not the commit; treat as missing + return undefined; + } + cache[sha] = result; return result; }); From e00aebc08572c16f091dda84454b49ba0a4e4281 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 9 May 2021 04:21:03 +0000 Subject: [PATCH 359/402] Bump handlebars from 4.7.6 to 4.7.7 in /node Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.7.6 to 4.7.7. - [Release notes](https://github.com/wycats/handlebars.js/releases) - [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md) - [Commits](https://github.com/wycats/handlebars.js/compare/v4.7.6...v4.7.7) Signed-off-by: dependabot[bot] --- node/yarn.lock | 222 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 198 insertions(+), 24 deletions(-) diff --git a/node/yarn.lock b/node/yarn.lock index 9626abc8c..e8de384ee 100644 --- a/node/yarn.lock +++ b/node/yarn.lock @@ -184,6 +184,14 @@ buffer-fill@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + camelcase@^5.0.0: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -396,7 +404,7 @@ deeper@: resolved "https://registry.yarnpkg.com/deeper/-/deeper-2.1.0.tgz#bc564e5f73174fdf201e08b00030e8a14da74368" integrity sha1-vFZOX3MXT98gHgiwADDooU2nQ2g= -define-properties@^1.1.2: +define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== @@ -486,6 +494,28 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== +es-abstract@^1.18.0-next.2: + version "1.18.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4" + integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.2" + is-callable "^1.2.3" + is-negative-zero "^2.0.1" + is-regex "^1.1.2" + is-string "^1.0.5" + object-inspect "^1.9.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.0" + es-abstract@^1.5.1: version "1.13.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" @@ -507,6 +537,15 @@ es-to-primitive@^1.2.0: is-date-object "^1.0.1" is-symbol "^1.0.2" +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -601,10 +640,12 @@ flat@^4.1.0: dependencies: is-buffer "~2.0.3" -foreachasync@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" - integrity sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY= +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" forever-agent@~0.6.1: version "0.6.1" @@ -698,6 +739,15 @@ get-func-name@^2.0.0: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -772,9 +822,9 @@ growl@1.10.5: integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== handlebars@^4.0.1: - version "4.7.6" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" - integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== dependencies: minimist "^1.2.5" neo-async "^2.6.0" @@ -796,6 +846,11 @@ har-validator@~5.1.0: ajv "^6.5.5" har-schema "^2.0.0" +has-bigints@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" @@ -811,6 +866,11 @@ has-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= +has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -890,11 +950,28 @@ invert-kv@^2.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== +is-bigint@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a" + integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA== + +is-boolean-object@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.1.tgz#3c0878f035cb821228d350d2e1e36719716a3de8" + integrity sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng== + dependencies: + call-bind "^1.0.2" + is-buffer@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== +is-callable@^1.1.3, is-callable@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" + integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== + is-callable@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" @@ -917,6 +994,16 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= +is-negative-zero@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" + integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== + +is-number-object@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb" + integrity sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw== + is-regex@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" @@ -924,11 +1011,24 @@ is-regex@^1.0.4: dependencies: has "^1.0.1" +is-regex@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f" + integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ== + dependencies: + call-bind "^1.0.2" + has-symbols "^1.0.2" + is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +is-string@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f" + integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w== + is-symbol@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" @@ -936,6 +1036,13 @@ is-symbol@^1.0.2: dependencies: has-symbols "^1.0.0" +is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -1152,12 +1259,7 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= - -minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -1408,7 +1510,12 @@ object-assign@^4.0.1, object-assign@^4.1.0: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object-keys@^1.0.11, object-keys@^1.0.12: +object-inspect@^1.9.0: + version "1.10.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" + integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -1423,6 +1530,16 @@ object.assign@4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" +object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + object.getownpropertydescriptors@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" @@ -1431,6 +1548,15 @@ object.getownpropertydescriptors@^2.0.3: define-properties "^1.1.2" es-abstract "^1.5.1" +object.getownpropertydescriptors@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz#1bd63aeacf0d5d2d2f31b5e393b03a7c601a23f7" + integrity sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1637,6 +1763,13 @@ readable-stream@^2.0.6, readable-stream@^2.3.0, readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readdir-withfiletypes@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/readdir-withfiletypes/-/readdir-withfiletypes-1.0.2.tgz#f54e6ed97e195c2823851c45477f35f500669f61" + integrity sha512-aERxJxl5lKupnqBdqatmaWOCj8YhHAB+/EdcBQU+z/vyII6dYysmNHxR9OVjdJOnhgq+5eX6wbo53ysFvu5UIw== + dependencies: + util.promisify "^1.0.0" + request-promise-core@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" @@ -1852,6 +1985,22 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -2046,9 +2195,19 @@ type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== uglify-js@^3.1.4: - version "3.10.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.10.0.tgz#397a7e6e31ce820bfd1cb55b804ee140c587a9e7" - integrity sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA== + version "3.13.5" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.5.tgz#5d71d6dbba64cf441f32929b1efce7365bb4f113" + integrity sha512-xtB8yEqIkn7zmOyS2zUNBsYCBRhDkvlNxMMY2smuJ/qA8NCHeQvKCF3i9Z4k8FJH4+PJvZRtMrPynfZ75+CSZw== + +unbox-primitive@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" + integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.1" + has-symbols "^1.0.2" + which-boxed-primitive "^1.0.2" uniq@^1.0.1: version "1.0.1" @@ -2072,6 +2231,17 @@ util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +util.promisify@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.1.1.tgz#77832f57ced2c9478174149cae9b96e9918cd54b" + integrity sha512-/s3UsZUrIfa6xDhr7zZhnE9SLQ5RIXyYfiVnMMyMDzOc8WhWN4Nbh36H842OyurKbCDAesZOJaVyvmSl6fhGQw== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + for-each "^0.3.3" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.1" + uuid@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" @@ -2086,12 +2256,16 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -walk@: - version "2.3.14" - resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.14.tgz#60ec8631cfd23276ae1e7363ce11d626452e1ef3" - integrity sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg== - dependencies: - foreachasync "^3.0.0" +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" which-module@^2.0.0: version "2.0.0" From 760767518c9060cef0343e440547d7ac23b27928 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 May 2021 03:07:12 +0000 Subject: [PATCH 360/402] Bump lodash from 4.17.19 to 4.17.21 in /node Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21) Signed-off-by: dependabot[bot] --- node/yarn.lock | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/node/yarn.lock b/node/yarn.lock index e8de384ee..ce9728820 100644 --- a/node/yarn.lock +++ b/node/yarn.lock @@ -1189,15 +1189,10 @@ lodash.flatten@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= -lodash@^4.17.11, lodash@~4.17.11: - version "4.17.19" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" - integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== - -lodash@^4.17.14: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@^4.17.11, lodash@^4.17.14, lodash@~4.17.11: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-symbols@2.2.0: version "2.2.0" From b77d742d6b1a64caeca0f542797579fafde3ebd8 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 29 Jun 2021 13:23:22 -0400 Subject: [PATCH 361/402] git meta checkout should write to stderr --- node/lib/cmd/checkout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js index 783cc30ba..fb0b20544 100644 --- a/node/lib/cmd/checkout.js +++ b/node/lib/cmd/checkout.js @@ -203,7 +203,7 @@ file(s) known to git.`); // In this case, we're not making a branch; just let the user know what // we checked out. - console.log(`Checked out ${colors.green(committish)}.`); + process.stderr.write(`Checked out ${colors.green(committish)}.\n`); } // Run post-checkout hook. From 084aa8088f323fe8a45939df3c0a94929ac8e934 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 29 Jun 2021 14:27:03 -0400 Subject: [PATCH 362/402] Better error message for open with implicit ./ If you say git meta open "foo", we interpret it as "git meta open ./foo". Then, if it's already open, we say, "Sorry, $repo is already open" (where $repo is the submodule that your cwd is inside. But you might have meant "git meta open $(git meta root)/foo", and thus you might be confused. Note that this can happen whether or not "./foo" exists -- we don't check that because in the case that it doesn't, checking would require half-opening the repo. In this patch, we notice that you have done this, and say, "$repo (for filename foo) is already open". --- node/lib/cmd/open.js | 32 +++++++++++----- node/lib/util/submodule_util.js | 68 +++++++++++++++++++++++++++++++-- 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 35c82e51d..b66e98d80 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -36,6 +36,7 @@ const assert = require("chai").assert; const co = require("co"); +const path = require("path"); const NodeGit = require("nodegit"); const GitUtil = require("../util/git_util"); const UserError = require("../util/user_error"); @@ -153,16 +154,15 @@ exports.executeableSubcommand = co.wrap(function *(args) { const subs = yield SubmoduleUtil.getSubmoduleNames(repo); const paths = yield parseArgs(repo, args); - let subsToOpen = yield SubmoduleUtil.resolveSubmoduleNames(workdir, - cwd, - subs, - paths, - true, - true); - subsToOpen = Array.from(new Set(subsToOpen)); + const subsToOpen = yield SubmoduleUtil.resolveSubmodules(workdir, + cwd, + subs, + paths, + true); const index = yield repo.index(); + const subNames = Object.keys(subsToOpen); const shas = yield SubmoduleUtil.getCurrentSubmoduleShas(index, - subsToOpen); + subNames); const head = yield repo.getHeadCommit(); const fetcher = new SubmoduleFetcher(repo, head); @@ -175,7 +175,19 @@ exports.executeableSubcommand = co.wrap(function *(args) { const opener = co.wrap(function *(name, idx) { if (openSubs.has(name)) { - console.warn(`Submodule ${colors.cyan(name)} is already open.`); + let provenance = ""; + for (let filename of subsToOpen[name]) { + let resolved = path.relative( + workdir, + path.resolve(cwd, filename)); + if (resolved !== name) { + provenance = ` (for filename ${resolved})`; + break; + } + } + + console.warn( + `Submodule ${colors.cyan(name)}${provenance} is already open.`); return; // RETURN } @@ -212,7 +224,7 @@ Opening ${colors.blue(name)} on ${colors.green(shas[idx])}.`); } } }); - yield DoWorkQueue.doInParallel(subsToOpen, opener, 10); + yield DoWorkQueue.doInParallel(subNames, opener, 10); // Make sure the index entries are updated in case we're in sparse mode. diff --git a/node/lib/util/submodule_util.js b/node/lib/util/submodule_util.js index 6e14dbb28..8467e628d 100644 --- a/node/lib/util/submodule_util.js +++ b/node/lib/util/submodule_util.js @@ -601,8 +601,7 @@ exports.resolveSubmoduleNames = function (workdir, cwd, submoduleNames, paths, - throwOnMissing, - includeParents) { + throwOnMissing) { assert.isString(workdir); assert.isString(cwd); assert.isArray(submoduleNames); @@ -616,7 +615,7 @@ exports.resolveSubmoduleNames = function (workdir, filename); const result = exports.getSubmodulesInPath(relPath, submoduleNames, - includeParents); + false); if (0 === result.length) { const msg = `\ No submodules found from ${colors.yellow(filename)}.`; @@ -631,6 +630,69 @@ No submodules found from ${colors.yellow(filename)}.`; return subLists.reduce((a, b) => a.concat(b), []); }; +/** + * Return a map from `paths` to the list of of submodules found under those + * paths in the specified meta-repo `workdir`, containing the submodules + * having the specified `submoduleNames`. Treat paths as being relative to + * the specified `cwd`. Throw a `UserError` if an path outside of the workdir + * is encountered. If a path inside the workdir contains no submodules, + * either log a warning, or, if throwOnMissing is set, throw a `UserError`. + * + * @param {String} workdir + * @param {String} cwd + * @param {String[]} submoduleNames + * @param {String[]} paths + * @param {Boolean} throwOnMissing + * @return {String[]} + */ +exports.resolveSubmodules = function (workdir, + cwd, + submoduleNames, + paths, + throwOnMissing) { + assert.isString(workdir); + assert.isString(cwd); + assert.isArray(submoduleNames); + assert.isArray(paths); + + const byFilename = {}; + paths.forEach(filename => { + // Compute the relative path for `filename` from the root of the repo, + // and check for invalid values. + const relPath = GitUtil.resolveRelativePath(workdir, + cwd, + filename); + const result = exports.getSubmodulesInPath(relPath, + submoduleNames, + true); + if (0 === result.length) { + const msg = `\ +No submodules found from ${colors.yellow(filename)}.`; + if (throwOnMissing) { + throw new UserError(msg); + } else { + console.warn(msg); + } + } + byFilename[filename] = result; + }); + + const out = {}; + for (let [filename, paths] of Object.entries(byFilename)) { + for (const path of paths) { + if (out[path]) { + if (!out[path].includes(filename)) { + out[path].push(filename); + } + } else { + out[path] = [filename]; + } + } + } + return out; +}; + + /** * Return a map from submodule name to an array of paths (relative to the root * of each submodule) identified by the specified `paths`, indicating one of From ab0f0e51dccbca151178dea54b1b8b7cf0e9b433 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 29 Jun 2021 22:17:23 -0400 Subject: [PATCH 363/402] Move commit message request mostly after pre-commit hooks The exception, of course, is interactive commits. There, we need the message to know what submodules to commit. --- node/lib/util/commit.js | 286 +++++++++++++++++++++++++++++---------- node/test/util/commit.js | 17 ++- 2 files changed, 229 insertions(+), 74 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 08f3060dc..1b17d1115 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -77,6 +77,12 @@ exports.ensureEolOnLastLine = function (message) { return message.endsWith("\n") ? message : (message + "\n"); }; +function abortIfNoMessage(message) { + if (message === "") { + throw new UserError("Aborting commit due to empty commit message."); + } +} + /** * Return the `NodeGit.Tree` object for the (left) parent of the head commit * in the specified `repo`, or null if the commit has no parent. @@ -550,23 +556,19 @@ const updateHead = co.wrap(function *(repo, sha) { exports.commit = co.wrap(function *(metaRepo, all, metaStatus, - message, + messageFunc, subMessages, noVerify, mergeParent) { assert.instanceOf(metaRepo, NodeGit.Repository); assert.isBoolean(all); assert.instanceOf(metaStatus, RepoStatus); - assert(exports.shouldCommit(metaStatus, message === null, subMessages), + assert(exports.shouldCommit(metaStatus, !!subMessages, subMessages), "nothing to commit"); - if (null !== message) { - assert.isString(message); - } + assert.isFunction(messageFunc); if (undefined !== subMessages) { assert.isObject(subMessages); } - assert(null !== message || undefined !== subMessages, - "if no meta message, sub messages must be specified"); assert.isBoolean(noVerify); let mergeTree = null; @@ -583,7 +585,38 @@ exports.commit = co.wrap(function *(metaRepo, // workdir changes. const subCommits = {}; const subRepos = {}; + const subStageData = {}; + const writeSubmoduleIndex = co.wrap(function *(name) { + const status = submodules[name]; + const repoStatus = (status.workdir && status.workdir.status) || null; + + if (null !== repoStatus && + 0 !== Object.keys(repoStatus.staged).length) { + const subRepo = yield SubmoduleUtil.getRepo(metaRepo, name); + + yield prepareIndexAndRunHooks(subRepo, + repoStatus.staged, + all, + noVerify); + const index = yield subRepo.index(); + subStageData[name] = { + repo : subRepo, + index : index, + }; + + } + }); + + yield DoWorkQueue.doInParallel(Object.keys(submodules), + writeSubmoduleIndex); + + const message = yield messageFunc(); + const commitSubmodule = co.wrap(function *(name) { + const data = subStageData[name]; + const index = data.index; + const subRepo = data.repo; + let subMessage = message; // If we're explicitly providing submodule messages, look the commit @@ -595,49 +628,37 @@ exports.commit = co.wrap(function *(metaRepo, return; // RETURN } } - const status = submodules[name]; - const repoStatus = (status.workdir && status.workdir.status) || null; - if (null !== repoStatus && - 0 !== Object.keys(repoStatus.staged).length) { - const subRepo = yield SubmoduleUtil.getRepo(metaRepo, name); - - yield prepareIndexAndRunHooks(subRepo, - repoStatus.staged, - all, - noVerify); + const headCommit = yield subRepo.getHeadCommit(); + const parents = []; + if (headCommit !== null) { + parents.push(headCommit); + } - const headCommit = yield subRepo.getHeadCommit(); - const parents = []; - if (headCommit !== null) { - parents.push(headCommit); + if (mergeTree !== null) { + const mergeSubParent = yield mergeTree.entryByPath(name); + if (mergeSubParent.id() !== headCommit.id()) { + parents.push(mergeSubParent.id()); } + } - if (mergeTree !== null) { - const mergeSubParent = yield mergeTree.entryByPath(name); - if (mergeSubParent.id() !== headCommit.id()) { - parents.push(mergeSubParent.id()); - } - } - const index = yield subRepo.index(); - const tree = yield index.writeTree(); + const tree = yield index.writeTree(); - const commit = yield subRepo.createCommit( - null, - signature, - signature, - exports.ensureEolOnLastLine(subMessage), - tree, - parents); + const commit = yield subRepo.createCommit( + null, + signature, + signature, + exports.ensureEolOnLastLine(subMessage), + tree, + parents); - subRepos[name] = subRepo; - subCommits[name] = commit.tostrS(); - } + subRepos[name] = subRepo; + subCommits[name] = commit.tostrS(); }); - const index = yield metaRepo.index(); + yield DoWorkQueue.doInParallel(Object.keys(subStageData), commitSubmodule); - yield DoWorkQueue.doInParallel(Object.keys(submodules), commitSubmodule); + const index = yield metaRepo.index(); if (all) { for (const subName of Object.keys(metaStatus.staged)) { @@ -709,6 +730,113 @@ const isExecutable = co.wrap(function *(repo, filename) { } }); +/** + * Create a temp index and run the pre-commit hooks for the specified + * `repo` having the specified `status` using the specified commit + * `message` and return the ID of the new commit. Note that this + * method records staged commits for submodules but does not recurse + * into their repositories. Note also that changes that would involve + * altering `.gitmodules` -- additions, removals, and URL changes -- + * are ignored. HEAD and the main on-disk index file are not changed, + * although the in-memory index is altered. + * + * @param {NodeGit.Repository} repo + * @param {RepoStatus} status + * @param {String} message + * @return {String} + */ +const writeSubmoduleIndex = co.wrap(function *(repo, status, noVerify) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(status, RepoStatus); + + const headCommit = yield repo.getHeadCommit(); + const changes = {}; + const staged = status.staged; + const FILEMODE = NodeGit.TreeEntry.FILEMODE; + const FILESTATUS = RepoStatus.FILESTATUS; + const Change = TreeUtil.Change; + + // We do a soft reset later, which means that we don't touch the index. + // Therefore, all of our files must be staged. + + const index = yield repo.index(); + yield index.readTree(yield headCommit.getTree()); + + // Handle "normal" file changes. + + for (let filename in staged) { + const stat = staged[filename]; + if (FILESTATUS.REMOVED === stat) { + yield index.removeByPath(filename); + changes[filename] = null; + } + else { + const blobId = yield TreeUtil.hashFile(repo, filename); + const executable = yield isExecutable(repo, filename); + const mode = executable ? FILEMODE.EXECUTABLE : FILEMODE.BLOB; + changes[filename] = new Change(blobId, mode); + yield index.addByPath(filename); + } + } + + // We ignore submodules, because we assume that there are no nested + // submodules in a git-meta repo. + + if (!noVerify) { + yield runPreCommitHook(repo, index); + } + + + return index; +}); + +const createCommitFromIndex = co.wrap(function*(repo, index, message) { + const headCommit = yield repo.getHeadCommit(); + + // Use 'TreeUtil' to create a new tree having the required paths. + const treeId = yield index.writeTree(); + const tree = yield NodeGit.Tree.lookup(repo, treeId); + + // Create a commit with this tree. + + const sig = yield ConfigUtil.defaultSignature(repo); + const parents = [headCommit]; + const commitId = yield NodeGit.Commit.create( + repo, + 0, + sig, + sig, + 0, + exports.ensureEolOnLastLine(message), + tree, + parents.length, + parents); + + // Now, reload the index to get rid of the changes we made to it. + // In theory, this should be index.read(true), but that doesn't + // work for some reason. + yield GitUtil.overwriteIndexFromFile(index, index.path()); + + // ...and apply the changes from the commit that we just made. + // I'm pretty sure this doesn't do rename/copy detection, but the nodegit + // API docs are pretty vague, as are the libgit2 docs. + const commit = yield NodeGit.Commit.lookup(repo, commitId); + const diffs = yield commit.getDiff(); + const diff = diffs[0]; + for (let i = 0; i < diff.numDeltas(); i++) { + const delta = diff.getDelta(i); + const newFile = delta.newFile(); + if (GitUtil.isZero(newFile.id())) { + index.removeByPath(delta.oldFile().path()); + } else { + index.addByPath(newFile.path()); + } + } + + return commitId; +}); + + /** * Create a temp index and run the pre-commit hooks for the specified * `repo` having the specified `status` using the specified commit @@ -727,7 +855,6 @@ const isExecutable = co.wrap(function *(repo, filename) { exports.writeRepoPaths = co.wrap(function *(repo, status, message, noVerify) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(status, RepoStatus); - assert.isString(message); const headCommit = yield repo.getHeadCommit(); const changes = {}; @@ -843,10 +970,10 @@ exports.writeRepoPaths = co.wrap(function *(repo, status, message, noVerify) { * @return {String} return.metaCommit * @return {Object} return.submoduleCommits map from sub name to commit id */ -exports.commitPaths = co.wrap(function *(repo, status, message, noVerify) { +exports.commitPaths = co.wrap(function *(repo, status, messageFunc, noVerify) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(status, RepoStatus); - assert.isString(message); + assert.isFunction(messageFunc); assert.isBoolean(noVerify); const subCommits = {}; // map from name to sha @@ -854,7 +981,9 @@ exports.commitPaths = co.wrap(function *(repo, status, message, noVerify) { const committedSubs = {}; // map from name to RepoAST.Submodule const subs = status.submodules; - const writeSubPaths = co.wrap(function *(subName) { + const subStageData = {}; + + const writeSubIndexes = co.wrap(function*(subName) { const sub = subs[subName]; const workdir = sub.workdir; @@ -874,8 +1003,26 @@ exports.commitPaths = co.wrap(function *(repo, status, message, noVerify) { const wdStatus = workdir.status; const subRepo = yield SubmoduleUtil.getRepo(repo, subName); - const oid = yield exports.writeRepoPaths(subRepo, wdStatus, message, - noVerify); + const index = yield writeSubmoduleIndex(subRepo, wdStatus, noVerify); + + subStageData[subName] = {sub : sub, + index : index, + repo : subRepo, + wdStatus: wdStatus}; + }); + + yield DoWorkQueue.doInParallel(Object.keys(subs), writeSubIndexes); + + const message = yield messageFunc(); + + const writeSubmoduleCommits = co.wrap(function*(subName) { + const data = subStageData[subName]; + const sub = data.sub; + const index = data.index; + const subRepo = data.repo; + const wdStatus = data.wdStatus; + + const oid = yield createCommitFromIndex(subRepo, index, message); const sha = oid.tostrS(); subCommits[subName] = sha; const oldIndex = sub.index; @@ -891,7 +1038,7 @@ exports.commitPaths = co.wrap(function *(repo, status, message, noVerify) { committedSubs[subName].repo = subRepo; }); - yield DoWorkQueue.doInParallel(Object.keys(subs), writeSubPaths); + yield DoWorkQueue.doInParallel(Object.keys(subs), writeSubmoduleCommits); for (const subName of Object.keys(committedSubs)) { const sub = committedSubs[subName]; @@ -2164,10 +2311,6 @@ configuration changes.`); } } -function abortForNoMessage() { - throw new UserError("Aborting commit due to empty commit message."); -} - /** * Perform the commit command in the specified `repo`. Consider the values in * the specified `paths` to be relative to the specified `cwd`, and format @@ -2273,6 +2416,7 @@ exports.doCommitCommand = co.wrap(function *(repo, } let subMessages; + let messageFunc; if (interactive) { // If 'interactive' mode is requested, ask the user to specify which @@ -2286,7 +2430,7 @@ exports.doCommitCommand = co.wrap(function *(repo, const userText = yield GitUtil.editMessage(repo, prompt, editMessage, !noVerify); const userData = exports.parseSplitCommitMessages(userText); - message = userData.metaMessage; + messageFunc = co.wrap(function*() { return userData.metaMessage; }); subMessages = userData.subMessages; // Check if there's actually anything to commit. @@ -2295,31 +2439,37 @@ exports.doCommitCommand = co.wrap(function *(repo, console.log("Nothing to commit."); return; } - } - else if (null === message) { - // If no message on the command line, prompt for one. - - const initialMessage = exports.formatEditorPrompt(repoStatus, cwd); - message = yield GitUtil.editMessage(repo, initialMessage, editMessage, - !noVerify); - } - message = GitUtil.stripMessage(message); + } else if (message === null) { + // If no message on the command line, later we will prompt for one. + messageFunc = co.wrap(function*() { + const initialMessage = exports.formatEditorPrompt(repoStatus, cwd); + let message = yield GitUtil.editMessage(repo, initialMessage, + editMessage, !noVerify); + message = GitUtil.stripMessage(message); + abortIfNoMessage(message); + return message; + }); + } else { + // we strip the message here even though we may need to strip + // it later, just so that we can abort correctly before + // running hooks. + message = GitUtil.stripMessage(message); - if ("" === message) { - abortForNoMessage(); + abortIfNoMessage(message); + messageFunc = co.wrap(function*() { return message; }); } if (usingPaths) { return yield exports.commitPaths(repo, repoStatus, - message, + messageFunc, noVerify); } else { return yield exports.commit(repo, all, repoStatus, - message, + messageFunc, subMessages, noVerify, mergeParent); @@ -2427,9 +2577,7 @@ repository independently.`; } } - if ("" === message) { - abortForNoMessage(); - } + abortIfNoMessage(message); if (!exports.shouldCommit(status, null === message, diff --git a/node/test/util/commit.js b/node/test/util/commit.js index f8fb725c9..225c2acb2 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -46,6 +46,12 @@ const TestUtil = require("../../lib/util/test_util"); const UserError = require("../../lib/util/user_error"); +function msgfunc(msg) { + return co.wrap(function*() { + return msg; + }); +} + function mapCommitResult(commitResult) { // Return a map from physical to computed logical sha for the commit ids in // the specified `commitResul` (as returned by `Commit.commit` and @@ -74,8 +80,8 @@ const committer = co.wrap(function *(doAll, message, repos, subMessages) { showMetaChanges: true, all: doAll, }); - const result = yield Commit.commit(x, doAll, status, message, subMessages, - false); + const result = yield Commit.commit(x, doAll, status, msgfunc(message), + subMessages, false); return { commitMap: mapCommitResult(result), }; @@ -2081,7 +2087,7 @@ x=S:C2-1 q/r/s=Sa:1;Bmaster=2;Oq/r/s H=a`, const message = c.message || "message"; const result = yield Commit.commitPaths(repo, status, - message, + msgfunc(message), false); const commitMap = {}; commitMap[result.metaCommit] = "x"; @@ -3384,7 +3390,7 @@ x=U:Cmsg\n#x-2 s=Sa:s;Bmaster=x;Os Cmsg\n#s-1 README.md=foo,addedbyhook=bar`; const result = yield Commit.commitPaths(repo, status, - message, + msgfunc(message), false); const commitMap = {}; commitMap[result.metaCommit] = "x"; @@ -3458,7 +3464,8 @@ describe("commit mid-merge", function() { paths: [], }); - yield Commit.commit(repo, true, repoStatus, "commit message", + yield Commit.commit(repo, true, repoStatus, + msgfunc("commit message"), undefined, true, m4); const head = yield repo.getHeadCommit(); From d58f3c8adca573f9715691bec61a42bfdcb81086 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 8 Jul 2021 14:37:49 -0400 Subject: [PATCH 364/402] fix jshint --- node/test/util/commit.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 225c2acb2..d7ecdfc76 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -48,6 +48,10 @@ const UserError = require("../../lib/util/user_error"); function msgfunc(msg) { return co.wrap(function*() { + // This bogosity is to satisfy jshint + yield new Promise(function(resolve) { + resolve(null); + }); return msg; }); } From 174a59a82d9271552f5e9012308a346f7c28fa5f Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 8 Jul 2021 15:54:53 -0400 Subject: [PATCH 365/402] Fix stitch tests --- node/test/util/stitch_util.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/node/test/util/stitch_util.js b/node/test/util/stitch_util.js index c42b7eb61..263c41724 100644 --- a/node/test/util/stitch_util.js +++ b/node/test/util/stitch_util.js @@ -1321,14 +1321,20 @@ S:C2-1;B2=2;N refs/notes/stitched/converted 1=; N refs/notes/stitched/converted 2=1`); const repo = written.repo; const one = yield repo.getHeadCommit(); - const oneSha = one.id().tostrS(); + const oldGetCommit = repo.getCommit.bind(repo); + repo.getCommit = co.wrap(function* (sha) { + if (sha === "1") { + return one; + } + return yield oldGetCommit(sha); + }); + const two = yield repo.getBranchCommit("2"); const twoSha = two.id().tostrS(); const result = yield StitchUtil.readConvertedCommits(repo); const expected = {}; - expected[oneSha] = null; expected[twoSha] = "1"; - assert.deepEqual(result, expected); + assert.deepEqual(expected, result); })); }); describe("makeGetConvertedCommit", function () { @@ -1349,13 +1355,13 @@ S:N refs/notes/stitched/converted 1=`); const headSha = head.id().tostrS(); const fun = StitchUtil.makeGetConvertedCommit(repo, {}); const result = yield fun(headSha); - assert.isNull(result); + assert.isUndefined(result); // Now delete and make sure we're remembering the result. NodeGit.Reference.remove(repo, StitchUtil.convertedNoteRef); const nextResult = yield fun(headSha); - assert.isNull(nextResult); + assert.isUndefined(nextResult); })); it("got one from cache", co.wrap(function *() { const written = yield RepoASTTestUtil.createRepo(`S`); @@ -1386,7 +1392,7 @@ describe("listCommitsToStitch", function () { assert.deepEqual([headSha], resultShas); })); - it("skipped", co.wrap(function *() { + it("note present, stitched commit missing", co.wrap(function *() { const written = yield RepoASTTestUtil.createRepo("S"); const repo = written.repo; const head = yield repo.getHeadCommit(); @@ -1400,7 +1406,7 @@ describe("listCommitsToStitch", function () { const result = yield StitchUtil.listCommitsToStitch(repo, head, getConv); const resultShas = result.map(c => c.id().tostrS()); - assert.deepEqual([], resultShas); + assert.deepEqual([headSha], resultShas); })); it("with parents", co.wrap(function *() { From b8e3bdec9ff2830d4d0850cf1e79eacffbcf039e Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 9 Jul 2021 17:21:26 -0400 Subject: [PATCH 366/402] Consider the index when comparing the tree to worktree during a commit. It turns out that libgit2 has a function for that, which is good, because the alternative involved much painful hacking of git_tree_to_workdir. --- node/lib/util/diff_util.js | 12 +++++++++++- node/test/util/commit.js | 12 ++++++++++-- node/test/util/diff_util.js | 3 ++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/node/lib/util/diff_util.js b/node/lib/util/diff_util.js index 9c587976c..3674e98b0 100644 --- a/node/lib/util/diff_util.js +++ b/node/lib/util/diff_util.js @@ -86,6 +86,14 @@ function readDiff(diff) { delta.newFile(); const path = file.path(); + if (FILESTATUS.MODIFIED === fileStatus && + delta.newFile().id().equal(delta.oldFile().id())) { + // This file isn't actually changed -- it's just being reported + // as changed because it differs from the index (even though + // the index isn't really for contents). + continue; + } + // Skip the all submodule changes; they're handled separately. if (NodeGit.TreeEntry.FILEMODE.COMMIT !== file.mode()) { result[path] = fileStatus; @@ -159,7 +167,9 @@ exports.getRepoStatus = co.wrap(function *(repo, if (ignoreIndex) { const workdirToTreeDiff = - yield NodeGit.Diff.treeToWorkdir(repo, tree, options); + yield NodeGit.Diff.treeToWorkdirWithIndex(repo, + tree, + options); const workdirToTreeStatus = readDiff(workdirToTreeDiff); return { staged: {}, diff --git a/node/test/util/commit.js b/node/test/util/commit.js index 225c2acb2..c83693286 100644 --- a/node/test/util/commit.js +++ b/node/test/util/commit.js @@ -3449,12 +3449,20 @@ describe("commit mid-merge", function() { const index = yield sRepo.index(); const sEntry = index.getByIndex(1); const newEntry = new NodeGit.IndexEntry(); - newEntry.flags = sEntry.flags; + // mask off the stage bits here, so that the new entry we write is + // in stage zero. + newEntry.flags = sEntry.flags & ~0x3000; newEntry.flagsExtended = sEntry.flagsExtended; newEntry.mode = sEntry.mode; newEntry.id = sEntry.id; newEntry.path = sEntry.path; - + newEntry.fileSize = sEntry.fileSize; + newEntry.gid = 0; + newEntry.uid = 0; + newEntry.ino = 0; + newEntry.dev = 0; + newEntry.mtime = sEntry.mtime; + newEntry.ctime = sEntry.ctime; yield index.conflictCleanup(); yield index.add(newEntry); yield index.write(); diff --git a/node/test/util/diff_util.js b/node/test/util/diff_util.js index b1a946954..7da94254c 100644 --- a/node/test/util/diff_util.js +++ b/node/test/util/diff_util.js @@ -375,7 +375,8 @@ x=S:C2-1 s=Sa:1;C3-2 s;Bmaster=3`, }, "HEAD^ unmodified": { input: ` -x=S:C2-1 README.md=3;W README.md=hello world;Bmaster=2`, +x=S:C2-1 README.md=3;I README.md=hello world;W README.md=hello world; +Bmaster=2`, workdirToTree: true, from: "HEAD^", }, From a57722c48e6f96d1161ae8d6c60413bc6bb53df1 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 13 Jul 2021 14:42:52 -0400 Subject: [PATCH 367/402] add -f option to open --- node/lib/cmd/open.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index b66e98d80..9a87d491d 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -81,6 +81,11 @@ exports.configureParser = function (parser) { type: "string", help: "open all submodules modified in a commit", }); + parser.addArgument(["-f", "--force"], { + action: "storeTrue", + help: + "open existing submodules even if some requested ones don't exist", + }); }; const parseArgs = co.wrap(function *(repo, args) { @@ -158,7 +163,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { cwd, subs, paths, - true); + !args.force); const index = yield repo.index(); const subNames = Object.keys(subsToOpen); const shas = yield SubmoduleUtil.getCurrentSubmoduleShas(index, From a829a3336b6a95efeab2a7f892267c4d36ef41e1 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 20 Jul 2021 15:28:21 -0400 Subject: [PATCH 368/402] Fix hook chdir race Instead of running chdir in the parent process before running the hooks, pass the cwd arg to spawn. This eliminates a race condition where perhaps one chdir happens, and then a different submodule's chdir happens, and then the first submodule's hooks get run out of the second submodule. --- node/lib/util/hook.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/lib/util/hook.js b/node/lib/util/hook.js index af26dbf39..eb4f2a6dc 100644 --- a/node/lib/util/hook.js +++ b/node/lib/util/hook.js @@ -73,11 +73,11 @@ exports.execHook = co.wrap(function*(repo, name, args=[], env={}) { } try { - process.chdir(repo.workdir()); const subEnv = {}; Object.assign(subEnv, process.env); Object.assign(subEnv, env); - yield spawn(absPath, args, { stdio: "inherit", env: subEnv }); + yield spawn(absPath, args, { stdio: "inherit", env: subEnv, + cwd: repo.workdir()}); return true; } catch (e) { if (e.code === "EACCES") { From 9b6f72bde875a1101088184f713ff6713597043e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Aug 2021 01:16:59 +0000 Subject: [PATCH 369/402] Bump tar from 4.4.9 to 4.4.15 in /node Bumps [tar](https://github.com/npm/node-tar) from 4.4.9 to 4.4.15. - [Release notes](https://github.com/npm/node-tar/releases) - [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/npm/node-tar/compare/v4.4.9...v4.4.15) --- updated-dependencies: - dependency-name: tar dependency-type: indirect ... Signed-off-by: dependabot[bot] --- node/yarn.lock | 68 ++++++++++++++++++++------------------------------ 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/node/yarn.lock b/node/yarn.lock index ce9728820..27d1553ac 100644 --- a/node/yarn.lock +++ b/node/yarn.lock @@ -238,9 +238,9 @@ child-process-promise@: promise-polyfill "^6.0.1" chownr@^1.0.1, chownr@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" - integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== circular-json@^0.5.9: version "0.5.9" @@ -684,11 +684,11 @@ fs-extra@^7.0.0: universalify "^0.1.0" fs-minipass@^1.2.5: - version "1.2.6" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" - integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ== + version "1.2.7" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" + integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== dependencies: - minipass "^2.2.1" + minipass "^2.6.0" fs-promise@: version "2.0.3" @@ -1259,15 +1259,7 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minipass@^2.2.1, minipass@^2.3.5: - version "2.3.5" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" - integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minipass@^2.8.6: +minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== @@ -1276,19 +1268,26 @@ minipass@^2.8.6: yallist "^3.0.0" minizlib@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" - integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== + version "1.3.3" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== dependencies: - minipass "^2.2.1" + minipass "^2.9.0" -mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= dependencies: minimist "0.0.8" +mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + mocha-jshint@: version "2.3.1" resolved "https://registry.yarnpkg.com/mocha-jshint/-/mocha-jshint-2.3.1.tgz#303f27e533938559d20f26a4123d5bcb5645a546" @@ -2088,23 +2087,10 @@ tar-stream@^1.1.2: to-buffer "^1.1.1" xtend "^4.0.0" -tar@^4: - version "4.4.9" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.9.tgz#058fbb152f6fc45733e84585a40c39e59302e1b3" - integrity sha512-xisFa7Q2i3HOgfn+nmnWLGHD6Tm23hxjkx6wwGmgxkJFr6wxwXnJOdJYcZjL453PSdF0+bemO03+flAzkIdLBQ== - dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.3.5" - minizlib "^1.2.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.3" - -tar@^4.4.8: - version "4.4.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" - integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== +tar@^4, tar@^4.4.8: + version "4.4.15" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.15.tgz#3caced4f39ebd46ddda4d6203d48493a919697f8" + integrity sha512-ItbufpujXkry7bHH9NpQyTXPbJ72iTlXgkBAYsAjDXk3Ds8t/3NfO5P4xZGy7u+sYuQUbimgzswX4uQIEeNVOA== dependencies: chownr "^1.1.1" fs-minipass "^1.2.5" @@ -2320,9 +2306,9 @@ yallist@^2.1.2: integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= yallist@^3.0.0, yallist@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" - integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yargs-parser@13.0.0, yargs-parser@^13.0.0: version "13.0.0" From 25b6d60c5d611a0d0046411821a8dca1bf159813 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 5 Aug 2021 13:31:05 -0400 Subject: [PATCH 370/402] replace travis with github actions --- .github/workflows/ci.yml | 17 ++++++++++++++++ .travis.yml | 29 ---------------------------- node/lib/util/write_repo_ast_util.js | 15 ++++++++++++-- 3 files changed, 30 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..517a1264b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +name: CI +on: [push] +jobs: + Test-Git-Meta: + runs-on: ubuntu-latest + steps: + - run: echo "The job was automatically triggered by a ${{ github.event_name }} event." + - run: sudo apt-get install libkrb5-dev + - name: Check out repository code + uses: actions/checkout@v2 + - run: git config --global user.name "A U Thor" + - run: git config --global user.email author@example.com + - run: yarn install + working-directory: ${{ github.workspace }}/node + - run: yarn test + working-directory: ${{ github.workspace }}/node + - run: echo "This job's status is ${{ job.status }}." diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 45dd5860d..000000000 --- a/.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -language: node_js -node_js: - - 8 - -env: - - CC=gcc-5 -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - gcc-5 - - g++-5 - -before_install: - - sudo add-apt-repository ppa:git-core/ppa -y - - sudo apt-get update -q - - sudo apt-get install git -y - - git version - - cd node - -install: - - yarn install - -script: - - yarn test - -notifications: - email: false diff --git a/node/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js index 6184a2880..1db6274a6 100644 --- a/node/lib/util/write_repo_ast_util.js +++ b/node/lib/util/write_repo_ast_util.js @@ -496,11 +496,22 @@ git -C '${repo.workdir()}' checkout ${toCheckout} // not. I didn't see anything about `clean` and `Checkout.index` // didn't seem to work.. - const filesStr = ast.sparse ? "-- .gitmodules" : "-a"; + let checkoutStr; + if (ast.sparse) { + const index = yield repo.index(); + if (index.getByPath(".gitmodules")) { + checkoutStr = ` +git -C '${repo.workdir()}' checkout-index -f .gitmodules`; + } else { + checkoutStr = ""; + } + } else { + checkoutStr = `git -C '${repo.workdir()}' checkout-index -f -a`; + } const checkoutIndexStr = `\ git -C '${repo.workdir()}' checkout -- git -C '${repo.workdir()}' clean -f -d -git -C '${repo.workdir()}' checkout-index -f ${filesStr} +${checkoutStr} `; yield exec(checkoutIndexStr); From 3895dcbcc17048348335a660b757f4cec702c029 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Mon, 9 Aug 2021 21:30:04 -0400 Subject: [PATCH 371/402] allow half open submodules from a commit --- node/.gitignore | 2 ++ node/lib/cmd/open.js | 72 +++++++++++++++++++++++++++++++------------- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/node/.gitignore b/node/.gitignore index ee7111194..de9a96fbc 100644 --- a/node/.gitignore +++ b/node/.gitignore @@ -1,3 +1,5 @@ /node_modules/ /coverage /yarn/ +.vscode + diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 9a87d491d..df826445e 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -79,37 +79,38 @@ exports.configureParser = function (parser) { parser.addArgument(["-c"], { action: "store", type: "string", - help: "open all submodules modified in a commit", + help: "open all submodules modified in a commit, or half open \ + the submodules from this commit", }); parser.addArgument(["-f", "--force"], { action: "storeTrue", help: "open existing submodules even if some requested ones don't exist", }); + parser.addArgument(["--half"], { + action: "storeTrue", + help:"open the submodule in .git/modules only", + }); }; -const parseArgs = co.wrap(function *(repo, args) { +const parseArgs = co.wrap(function *(repo, args, commit) { assert.instanceOf(repo, NodeGit.Repository); args.path = Array.from(new Set(args.path)); if (args.path.length > 0) { - if (args.c) { - // Of course, args.path and args.c should be mutually exclusive, - // but unfortunately, node's argparse doesn't support this. + if (!args.half && args.c) { + // one can half open a path from a commit, or fully + // open all path in a commit, but cannot fully open + // some paths from a different commit, because that + // will mess up the workspace. throw new UserError("-c should take a single argument"); } return Array.from(new Set(args.path)); } - const commitish = args.c; - if (commitish === null) { + if (args.c === null) { throw new UserError( "Please supply a submodule to open, or -c $commitish"); } - const annotated = yield GitUtil.resolveCommitish(repo, commitish); - if (annotated === null) { - throw new UserError("Cannot resolve " + commitish + " to a commit"); - } - const commit = yield NodeGit.Commit.lookup(repo, annotated.id()); const tree = yield commit.getTree(); const parent = yield commit.parent(0); let parentTree = null; @@ -134,6 +135,34 @@ const parseArgs = co.wrap(function *(repo, args) { return Array.from(out); }); +/** + * If commitish is given, return resolved commit object and an in-memory + * index loaded with the corresponding tree. + * + * Otherwise, return the head commit and default repo index. + */ +const getCommitAndIndex = co.wrap(function *(repo, commitish) { + if (commitish) { + const annotated = yield GitUtil.resolveCommitish(repo, commitish); + if (annotated === null) { + throw new UserError("Cannot resolve " + commitish + " to a commit"); + } + const commit = yield NodeGit.Commit.lookup(repo, annotated.id()); + const tree = yield commit.getTree(); + const index = yield repo.refreshIndex(); + yield index.readTree(tree); + return { + commit: commit, + index: index + }; + } else { + return { + index: yield repo.index(), + commit: yield repo.getHeadCommit(), + }; + } +}); + /** * Execute the `open` command according to the specified `args`. * @@ -145,7 +174,6 @@ exports.executeableSubcommand = co.wrap(function *(args) { const colors = require("colors"); const DoWorkQueue = require("../util/do_work_queue"); - const GitUtil = require("../util/git_util"); const Open = require("../util/open"); const SparseCheckoutUtil = require("../util/sparse_checkout_util"); const SubmoduleConfigUtil = require("../util/submodule_config_util"); @@ -156,20 +184,21 @@ exports.executeableSubcommand = co.wrap(function *(args) { const repo = yield GitUtil.getCurrentRepo(); const workdir = repo.workdir(); const cwd = process.cwd(); - const subs = yield SubmoduleUtil.getSubmoduleNames(repo); - const paths = yield parseArgs(repo, args); + const {commit, index} = yield getCommitAndIndex(repo, args.c); + + const subs = yield SubmoduleUtil.getSubmoduleNamesForCommit(repo, commit); + + const paths = yield parseArgs(repo, args, commit); const subsToOpen = yield SubmoduleUtil.resolveSubmodules(workdir, cwd, subs, paths, !args.force); - const index = yield repo.index(); const subNames = Object.keys(subsToOpen); const shas = yield SubmoduleUtil.getCurrentSubmoduleShas(index, subNames); - const head = yield repo.getHeadCommit(); - const fetcher = new SubmoduleFetcher(repo, head); + const fetcher = new SubmoduleFetcher(repo, commit); let failed = false; let subsOpenSuccessfully = []; @@ -214,7 +243,7 @@ Opening ${colors.blue(name)} on ${colors.green(shas[idx])}.`); name, shas[idx], templatePath, - false); + args.half); subsOpenSuccessfully.push(name); console.log(`Finished opening ${colors.blue(name)}.`); } @@ -232,8 +261,9 @@ Opening ${colors.blue(name)} on ${colors.green(shas[idx])}.`); yield DoWorkQueue.doInParallel(subNames, opener, 10); // Make sure the index entries are updated in case we're in sparse mode. - - yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); + if (!args.half) { + yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index); + } if (failed) { process.exit(1); From d2eae13b79c83f04c97b85e34e798eb9a65b8a90 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 5 Aug 2021 12:51:55 -0400 Subject: [PATCH 372/402] Stash deletion of submodules --- node/lib/cmd/stash.js | 4 + node/lib/util/repo_status.js | 3 +- node/lib/util/rm.js | 18 ++- node/lib/util/stash_util.js | 216 +++++++++++++++++++++++++++++------ node/lib/util/status_util.js | 4 +- 5 files changed, 206 insertions(+), 39 deletions(-) diff --git a/node/lib/cmd/stash.js b/node/lib/cmd/stash.js index 7ddfa2720..76a653287 100644 --- a/node/lib/cmd/stash.js +++ b/node/lib/cmd/stash.js @@ -117,6 +117,10 @@ function cleanSubs(status, includeUntracked) { for (let subName in subs) { const sub = subs[subName]; const wd = sub.workdir; + if (sub.index === null) { + // This sub was deleted + return false; + } if (sub.commit.sha !== sub.index.sha) { // The submodule has a commit which is staged in the meta repo's // index diff --git a/node/lib/util/repo_status.js b/node/lib/util/repo_status.js index 4abd820a6..f3e40f6e8 100644 --- a/node/lib/util/repo_status.js +++ b/node/lib/util/repo_status.js @@ -372,7 +372,8 @@ class Submodule { */ isCommittable () { return !(this.isNew() && - null === this.d_index.sha && + (null === this.d_index || + null === this.d_index.sha) && (null === this.d_workdir || null === this.d_workdir.status.headCommit && this.d_workdir.status.isIndexClean())); diff --git a/node/lib/util/rm.js b/node/lib/util/rm.js index 2036c3fa7..b27860337 100644 --- a/node/lib/util/rm.js +++ b/node/lib/util/rm.js @@ -461,7 +461,23 @@ exports.rmPaths = co.wrap(function *(repo, paths, options) { } if (stat !== null) { if (stat.isDirectory()) { - yield fs.rmdir(fullpath); + try { + yield fs.rmdir(fullpath); + } catch (e) { + if ("ENOTEMPTY" === e.code) { + // Repo still exists for some reason -- + // perhaps it was reported as deleted + // because it's not in the index, but it + // does exist on disk. For safety, do not + // delete it; this is a weird case. + console.error( + `Could not remove ${fullpath} -- it's not +empty. If you are sure it is not needed, you can remove it yourself.` + ); + } else { + throw e; + } + } } else { yield fs.unlink(fullpath); } diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 34b257502..a28449bd7 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -33,8 +33,10 @@ const assert = require("chai").assert; const co = require("co"); const colors = require("colors"); +const fs = require("fs-promise"); const NodeGit = require("nodegit"); +const CloseUtil = require("./close_util"); const ConfigUtil = require("./config_util"); const DiffUtil = require("./diff_util"); const GitUtil = require("./git_util"); @@ -44,11 +46,19 @@ const RepoStatus = require("./repo_status"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const StatusUtil = require("./status_util"); const SubmoduleUtil = require("./submodule_util"); +const SubmoduleConfigUtil = require("./submodule_config_util"); const SubmoduleRebaseUtil = require("./submodule_rebase_util"); const TreeUtil = require("./tree_util"); const UserError = require("./user_error"); const Commit = NodeGit.Commit; +const Change = TreeUtil.Change; +const FILEMODE = NodeGit.TreeEntry.FILEMODE; + +const MAGIC_DELETED_SHA = NodeGit.Oid.fromString( + "de1e7ed0de1e7ed0de1e7ed0de1e7ed0de1e7ed0"); + +const GITMODULES = SubmoduleConfigUtil.modulesFileName; /** * Return the IDs of tress reflecting the current state of the index and @@ -107,6 +117,63 @@ exports.makeLogMessage = co.wrap(function *(repo) { WIP on ${branchDesc}: ${GitUtil.shortSha(head.id().tostrS())} ${message}`; }); + +function getNewGitModuleSha(diff) { + const numDeltas = diff.numDeltas(); + for (let i = 0; i < numDeltas; ++i) { + const delta = diff.getDelta(i); + // We assume that the user hasn't deleted the .gitmodules file. + // That would be bonkers. + const file = delta.newFile(); + const path = file.path(); + if (path === GITMODULES) { + return delta.newFile().id(); + } + } + // diff does not include .gitmodules + return null; +} + + +const stashGitModules = co.wrap(function *(repo, status, headTree) { + assert.instanceOf(repo, NodeGit.Repository); + assert.instanceOf(status, RepoStatus); + + const result = {}; + // RepoStatus throws away the diff new sha, and rather than hack + // it, since it's used all over the codebase, we'll just redo the + // diffs for this one file. + + const workdirToTreeDiff = + yield NodeGit.Diff.treeToWorkdir(repo, + headTree, + {paths: [GITMODULES]}); + + + const newWorkdir = getNewGitModuleSha(workdirToTreeDiff); + if (newWorkdir !== null) { + result.workdir = newWorkdir; + } + + const indexToTreeDiff = + yield NodeGit.Diff.treeToIndex(repo, + headTree, + yield repo.index(), + {paths: [GITMODULES]}); + + const newIndex = getNewGitModuleSha(indexToTreeDiff); + if (newIndex !== null) { + result.staged = newIndex; + } + + yield NodeGit.Checkout.tree(repo, headTree, { + paths: [GITMODULES], + checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, + }); + + return result; +}); + /** * Save the state of the submodules in the specified, `repo` having the * specified `status` and clean the sub-repositories to match their respective @@ -149,6 +216,8 @@ exports.save = co.wrap(function *(repo, status, includeUntracked, message) { const subRepos = {}; // name to submodule open repo const sig = yield ConfigUtil.defaultSignature(repo); + const head = yield repo.getHeadCommit(); + const headTree = yield head.getTree(); // First, we process the submodules. If a submodule is open and dirty, // we'll create the stash commits in its repo, populate `subResults` with @@ -178,34 +247,42 @@ report this. Continuing stash anyway.`); if (null === wd) { // closed submodule - if (sub.commit.sha === sub.index.sha) { - // ... with no staged changes - return; // RETURN - } - // This is a case that regular git stash doesn't really have - // to handle. In a normal stash commit, the tree points - // to the working directory tree, but here, there is no working - // directory. But if there were, we would want to have - // this commit checked out. - - const subRepo = yield SubmoduleUtil.getRepo(repo, name); + if (sub.index === null || sub.index.sha === null) { + // deleted submodule + stashId = MAGIC_DELETED_SHA; + yield NodeGit.Checkout.tree(repo, headTree, { + paths: [name], + checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, + }); + } else { + if (sub.commit.sha === sub.index.sha) { + // ... with no staged changes + return; // RETURN + } + // This is a case that regular git stash doesn't really have + // to handle. In a normal stash commit, the tree points + // to the working directory tree, but here, there is no working + // directory. But if there were, we would want to have + // this commit checked out. - const subCommit = yield Commit.lookup(subRepo, sub.commit.sha); - const indexCommit = yield Commit.lookup(subRepo, sub.index.sha); - const indexTree = yield indexCommit.getTree(); - stashId = yield Commit.create(subRepo, - null, - sig, - sig, - null, - "stash", - indexTree, - 4, - [subCommit, - indexCommit, - indexCommit, - indexCommit]); + const subRepo = yield SubmoduleUtil.getRepo(repo, name); + const subCommit = yield Commit.lookup(subRepo, sub.commit.sha); + const indexCommit = yield Commit.lookup(subRepo, sub.index.sha); + const indexTree = yield indexCommit.getTree(); + stashId = yield Commit.create(subRepo, + null, + sig, + sig, + null, + "stash", + indexTree, + 4, + [subCommit, + indexCommit, + indexCommit, + indexCommit]); + } } else { // open submodule if (sub.commit.sha !== sub.index.sha && @@ -318,13 +395,42 @@ commit to have two parents`); subResults[name] = stashId.tostrS(); // Record the values we've created. - subChanges[name] = new TreeUtil.Change( - stashId, - NodeGit.TreeEntry.FILEMODE.COMMIT); + subChanges[name] = new TreeUtil.Change(stashId, FILEMODE.COMMIT); })); - const head = yield repo.getHeadCommit(); - const headTree = yield head.getTree(); + const parents = [head]; + + const gitModulesChanges = yield stashGitModules(repo, status, headTree); + if (gitModulesChanges) { + if (gitModulesChanges.workdir) { + subChanges[GITMODULES] = new Change(gitModulesChanges.workdir, + FILEMODE.BLOB); + } + if (gitModulesChanges.staged) { + const indexChanges = {}; + Object.assign(indexChanges, subChanges); + + indexChanges[GITMODULES] = new Change(gitModulesChanges.staged, + FILEMODE.BLOB); + + + const indexTree = yield TreeUtil.writeTree(repo, headTree, + indexChanges); + const indexParent = yield Commit.create(repo, + null, + sig, + sig, + null, + "stash", + indexTree, + 1, + [head]); + + const indexParentCommit = yield Commit.lookup(repo, indexParent); + parents.push(indexParentCommit); + } + } + const subsTree = yield TreeUtil.writeTree(repo, headTree, subChanges); const stashId = yield Commit.create(repo, null, @@ -333,8 +439,8 @@ commit to have two parents`); null, "stash", subsTree, - 1, - [head]); + parents.length, + parents); const stashSha = stashId.tostrS(); @@ -438,6 +544,7 @@ exports.apply = co.wrap(function *(repo, id, reinstateIndex) { assert.isString(id); const commit = yield repo.getCommit(id); + const repoIndex = yield repo.index(); // TODO: patch libgit2/nodegit: the commit object returned from `parent` // isn't properly configured with a `repo` object, and attempting to use it @@ -445,12 +552,53 @@ exports.apply = co.wrap(function *(repo, id, reinstateIndex) { const parentId = (yield commit.parent(0)).id(); const parent = yield repo.getCommit(parentId); + const baseSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, parent, null); + + let indexSubs = baseSubs; + if (commit.parentcount() > 1) { + const parent2Id = (yield commit.parent(1)).id(); + const parent2 = yield repo.getCommit(parent2Id); + indexSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, + parent2, + null); + } + const newSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, commit, null); + + const toDelete = []; + yield Object.keys(baseSubs).map(co.wrap(function *(name) { + if (newSubs[name] === undefined) { + if (fs.existsSync(name)) { + // sub deleted in working tree + toDelete.push(name); + } + } + })); + + CloseUtil.close(repo, repo.workdir(), toDelete, false); + for (const name of toDelete) { + yield fs.rmdir(name); + } + + yield Object.keys(baseSubs).map(co.wrap(function *(name) { + if (indexSubs[name] === undefined) { + // sub deleted in the index + yield repoIndex.removeByPath(name); + } + })); + + // apply gitmodules diff + const headTree = yield commit.getTree(); + yield NodeGit.Checkout.tree(repo, headTree, { + paths: [GITMODULES], + checkoutStrategy: NodeGit.Checkout.STRATEGY.MERGE, + }); + const opener = new Open.Opener(repo, null); let result = {}; const index = {}; @@ -550,8 +698,6 @@ for debugging, is:`, e); } })); - const repoIndex = yield repo.index(); - if (null !== result) { for (let name of Object.keys(index)) { const entry = new NodeGit.IndexEntry(); diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 8e9ad32b0..1c47efaba 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -247,8 +247,8 @@ exports.getSubmoduleStatus = co.wrap(function *(repo, commit = new Submodule.Commit(commitSha, commitUrl); } - // A null indexUrl indicates that the submodule was removed. If that is - // the case, we're done. + // A null indexUrl indicates that the submodule doesn't exist in + // the staged .gitmodules. if (null === indexUrl) { return new Submodule({ commit: commit }); // RETURN From a2af4679fe261cd7fab44db91c71178d2a0a996e Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 11 Aug 2021 10:27:30 -0400 Subject: [PATCH 373/402] differentiate regular and meta rebases when warning user --- node/lib/util/status_util.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 8e9ad32b0..3b53f3028 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -636,9 +636,10 @@ exports.checkReadiness = function (status) { if (null !== status.rebase) { return (`\ +You're in the middle of a regular (not git-meta) rebase. Before proceeding, you must complete the rebase in progress (by running -'git meta rebase --continue') or abort it (by running -'git meta rebase --abort').`); +'git rebase --continue') or abort it (by running +'git rebase --abort').`); } if (status.isConflicted()) { return (`\ From be62e7b39608c7e0cc651e8e32c048480bf3d830 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 11 Aug 2021 10:30:56 -0400 Subject: [PATCH 374/402] action on pr --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 517a1264b..7a5ce40d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,5 @@ name: CI -on: [push] +on: [push, pull_request] jobs: Test-Git-Meta: runs-on: ubuntu-latest From bee8534be9c723563f5df184bdd9814b27d5a436 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 11 Aug 2021 16:11:43 -0400 Subject: [PATCH 375/402] Better logging for cherry-pick --- node/lib/util/cherry_pick_util.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index b8c24ef01..c3e02224f 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -116,11 +116,13 @@ exports.changeSubmodules = co.wrap(function *(repo, for (let name in submodules) { const sub = submodules[name]; if (null === sub) { + console.log(`Deleting ${name}`); changes[name] = null; delete urls[name]; yield rmrf(name); } else if (opener.isOpen(name)) { + console.log(`Fast-forwarding open submodule ${name}`); const subRepo = yield opener.getSubrepo(name, Open.SUB_OPEN_OPTION.FORCE_OPEN); @@ -129,6 +131,7 @@ exports.changeSubmodules = co.wrap(function *(repo, yield GitUtil.setHeadHard(subRepo, commit); yield index.addByPath(name); } else { + console.log(`Fast-forwarding closed submodule ${name}`); changes[name] = new TreeUtil.Change( NodeGit.Oid.fromString(sub.sha), NodeGit.TreeEntry.FILEMODE.COMMIT); From 551b22d3f27e89cbc77de60a99130d96070189e4 Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Thu, 12 Aug 2021 17:44:48 -0400 Subject: [PATCH 376/402] in git meta open, get list of all submodules from index instead of commit --- node/lib/cmd/open.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index df826445e..6db4fc2a7 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -187,7 +187,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { const {commit, index} = yield getCommitAndIndex(repo, args.c); - const subs = yield SubmoduleUtil.getSubmoduleNamesForCommit(repo, commit); + const subs = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); const paths = yield parseArgs(repo, args, commit); const subsToOpen = yield SubmoduleUtil.resolveSubmodules(workdir, From aef8939f98a041720fcc2984961cc6d1ea990e8c Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Thu, 12 Aug 2021 18:13:03 -0400 Subject: [PATCH 377/402] fix SubmoduleConfigUtil.getSubmodulesFromIndex returns a map, open.js should get all the keys as submodule names --- node/lib/cmd/open.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js index 6db4fc2a7..74bcb0e6b 100644 --- a/node/lib/cmd/open.js +++ b/node/lib/cmd/open.js @@ -187,7 +187,9 @@ exports.executeableSubcommand = co.wrap(function *(args) { const {commit, index} = yield getCommitAndIndex(repo, args.c); - const subs = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index); + const subs = Object.keys( + yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index) + ); const paths = yield parseArgs(repo, args, commit); const subsToOpen = yield SubmoduleUtil.resolveSubmodules(workdir, From 87920e1cff27de26e86f4a2a09c01600a896b391 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 11 Aug 2021 16:11:43 -0400 Subject: [PATCH 378/402] Better logging for cherry-pick --- node/lib/util/cherry_pick_util.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index b8c24ef01..c3e02224f 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -116,11 +116,13 @@ exports.changeSubmodules = co.wrap(function *(repo, for (let name in submodules) { const sub = submodules[name]; if (null === sub) { + console.log(`Deleting ${name}`); changes[name] = null; delete urls[name]; yield rmrf(name); } else if (opener.isOpen(name)) { + console.log(`Fast-forwarding open submodule ${name}`); const subRepo = yield opener.getSubrepo(name, Open.SUB_OPEN_OPTION.FORCE_OPEN); @@ -129,6 +131,7 @@ exports.changeSubmodules = co.wrap(function *(repo, yield GitUtil.setHeadHard(subRepo, commit); yield index.addByPath(name); } else { + console.log(`Fast-forwarding closed submodule ${name}`); changes[name] = new TreeUtil.Change( NodeGit.Oid.fromString(sub.sha), NodeGit.TreeEntry.FILEMODE.COMMIT); From 377a0b3f1b5df8c2f4a0d0ef2e26d37fb881d474 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 18 Aug 2021 11:38:08 -0400 Subject: [PATCH 379/402] Revert "Stash deletion of submodules" This reverts commit d2eae13b79c83f04c97b85e34e798eb9a65b8a90. --- node/lib/cmd/stash.js | 4 - node/lib/util/repo_status.js | 3 +- node/lib/util/rm.js | 18 +-- node/lib/util/stash_util.js | 216 ++++++----------------------------- node/lib/util/status_util.js | 4 +- 5 files changed, 39 insertions(+), 206 deletions(-) diff --git a/node/lib/cmd/stash.js b/node/lib/cmd/stash.js index 76a653287..7ddfa2720 100644 --- a/node/lib/cmd/stash.js +++ b/node/lib/cmd/stash.js @@ -117,10 +117,6 @@ function cleanSubs(status, includeUntracked) { for (let subName in subs) { const sub = subs[subName]; const wd = sub.workdir; - if (sub.index === null) { - // This sub was deleted - return false; - } if (sub.commit.sha !== sub.index.sha) { // The submodule has a commit which is staged in the meta repo's // index diff --git a/node/lib/util/repo_status.js b/node/lib/util/repo_status.js index f3e40f6e8..4abd820a6 100644 --- a/node/lib/util/repo_status.js +++ b/node/lib/util/repo_status.js @@ -372,8 +372,7 @@ class Submodule { */ isCommittable () { return !(this.isNew() && - (null === this.d_index || - null === this.d_index.sha) && + null === this.d_index.sha && (null === this.d_workdir || null === this.d_workdir.status.headCommit && this.d_workdir.status.isIndexClean())); diff --git a/node/lib/util/rm.js b/node/lib/util/rm.js index b27860337..2036c3fa7 100644 --- a/node/lib/util/rm.js +++ b/node/lib/util/rm.js @@ -461,23 +461,7 @@ exports.rmPaths = co.wrap(function *(repo, paths, options) { } if (stat !== null) { if (stat.isDirectory()) { - try { - yield fs.rmdir(fullpath); - } catch (e) { - if ("ENOTEMPTY" === e.code) { - // Repo still exists for some reason -- - // perhaps it was reported as deleted - // because it's not in the index, but it - // does exist on disk. For safety, do not - // delete it; this is a weird case. - console.error( - `Could not remove ${fullpath} -- it's not -empty. If you are sure it is not needed, you can remove it yourself.` - ); - } else { - throw e; - } - } + yield fs.rmdir(fullpath); } else { yield fs.unlink(fullpath); } diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index a28449bd7..34b257502 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -33,10 +33,8 @@ const assert = require("chai").assert; const co = require("co"); const colors = require("colors"); -const fs = require("fs-promise"); const NodeGit = require("nodegit"); -const CloseUtil = require("./close_util"); const ConfigUtil = require("./config_util"); const DiffUtil = require("./diff_util"); const GitUtil = require("./git_util"); @@ -46,19 +44,11 @@ const RepoStatus = require("./repo_status"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const StatusUtil = require("./status_util"); const SubmoduleUtil = require("./submodule_util"); -const SubmoduleConfigUtil = require("./submodule_config_util"); const SubmoduleRebaseUtil = require("./submodule_rebase_util"); const TreeUtil = require("./tree_util"); const UserError = require("./user_error"); const Commit = NodeGit.Commit; -const Change = TreeUtil.Change; -const FILEMODE = NodeGit.TreeEntry.FILEMODE; - -const MAGIC_DELETED_SHA = NodeGit.Oid.fromString( - "de1e7ed0de1e7ed0de1e7ed0de1e7ed0de1e7ed0"); - -const GITMODULES = SubmoduleConfigUtil.modulesFileName; /** * Return the IDs of tress reflecting the current state of the index and @@ -117,63 +107,6 @@ exports.makeLogMessage = co.wrap(function *(repo) { WIP on ${branchDesc}: ${GitUtil.shortSha(head.id().tostrS())} ${message}`; }); - -function getNewGitModuleSha(diff) { - const numDeltas = diff.numDeltas(); - for (let i = 0; i < numDeltas; ++i) { - const delta = diff.getDelta(i); - // We assume that the user hasn't deleted the .gitmodules file. - // That would be bonkers. - const file = delta.newFile(); - const path = file.path(); - if (path === GITMODULES) { - return delta.newFile().id(); - } - } - // diff does not include .gitmodules - return null; -} - - -const stashGitModules = co.wrap(function *(repo, status, headTree) { - assert.instanceOf(repo, NodeGit.Repository); - assert.instanceOf(status, RepoStatus); - - const result = {}; - // RepoStatus throws away the diff new sha, and rather than hack - // it, since it's used all over the codebase, we'll just redo the - // diffs for this one file. - - const workdirToTreeDiff = - yield NodeGit.Diff.treeToWorkdir(repo, - headTree, - {paths: [GITMODULES]}); - - - const newWorkdir = getNewGitModuleSha(workdirToTreeDiff); - if (newWorkdir !== null) { - result.workdir = newWorkdir; - } - - const indexToTreeDiff = - yield NodeGit.Diff.treeToIndex(repo, - headTree, - yield repo.index(), - {paths: [GITMODULES]}); - - const newIndex = getNewGitModuleSha(indexToTreeDiff); - if (newIndex !== null) { - result.staged = newIndex; - } - - yield NodeGit.Checkout.tree(repo, headTree, { - paths: [GITMODULES], - checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, - }); - - return result; -}); - /** * Save the state of the submodules in the specified, `repo` having the * specified `status` and clean the sub-repositories to match their respective @@ -216,8 +149,6 @@ exports.save = co.wrap(function *(repo, status, includeUntracked, message) { const subRepos = {}; // name to submodule open repo const sig = yield ConfigUtil.defaultSignature(repo); - const head = yield repo.getHeadCommit(); - const headTree = yield head.getTree(); // First, we process the submodules. If a submodule is open and dirty, // we'll create the stash commits in its repo, populate `subResults` with @@ -247,42 +178,34 @@ report this. Continuing stash anyway.`); if (null === wd) { // closed submodule - if (sub.index === null || sub.index.sha === null) { - // deleted submodule - stashId = MAGIC_DELETED_SHA; - yield NodeGit.Checkout.tree(repo, headTree, { - paths: [name], - checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, - }); - } else { - if (sub.commit.sha === sub.index.sha) { - // ... with no staged changes - return; // RETURN - } - // This is a case that regular git stash doesn't really have - // to handle. In a normal stash commit, the tree points - // to the working directory tree, but here, there is no working - // directory. But if there were, we would want to have - // this commit checked out. + if (sub.commit.sha === sub.index.sha) { + // ... with no staged changes + return; // RETURN + } + // This is a case that regular git stash doesn't really have + // to handle. In a normal stash commit, the tree points + // to the working directory tree, but here, there is no working + // directory. But if there were, we would want to have + // this commit checked out. - const subRepo = yield SubmoduleUtil.getRepo(repo, name); + const subRepo = yield SubmoduleUtil.getRepo(repo, name); + + const subCommit = yield Commit.lookup(subRepo, sub.commit.sha); + const indexCommit = yield Commit.lookup(subRepo, sub.index.sha); + const indexTree = yield indexCommit.getTree(); + stashId = yield Commit.create(subRepo, + null, + sig, + sig, + null, + "stash", + indexTree, + 4, + [subCommit, + indexCommit, + indexCommit, + indexCommit]); - const subCommit = yield Commit.lookup(subRepo, sub.commit.sha); - const indexCommit = yield Commit.lookup(subRepo, sub.index.sha); - const indexTree = yield indexCommit.getTree(); - stashId = yield Commit.create(subRepo, - null, - sig, - sig, - null, - "stash", - indexTree, - 4, - [subCommit, - indexCommit, - indexCommit, - indexCommit]); - } } else { // open submodule if (sub.commit.sha !== sub.index.sha && @@ -395,42 +318,13 @@ commit to have two parents`); subResults[name] = stashId.tostrS(); // Record the values we've created. - subChanges[name] = new TreeUtil.Change(stashId, FILEMODE.COMMIT); + subChanges[name] = new TreeUtil.Change( + stashId, + NodeGit.TreeEntry.FILEMODE.COMMIT); })); - const parents = [head]; - - const gitModulesChanges = yield stashGitModules(repo, status, headTree); - if (gitModulesChanges) { - if (gitModulesChanges.workdir) { - subChanges[GITMODULES] = new Change(gitModulesChanges.workdir, - FILEMODE.BLOB); - } - if (gitModulesChanges.staged) { - const indexChanges = {}; - Object.assign(indexChanges, subChanges); - - indexChanges[GITMODULES] = new Change(gitModulesChanges.staged, - FILEMODE.BLOB); - - - const indexTree = yield TreeUtil.writeTree(repo, headTree, - indexChanges); - const indexParent = yield Commit.create(repo, - null, - sig, - sig, - null, - "stash", - indexTree, - 1, - [head]); - - const indexParentCommit = yield Commit.lookup(repo, indexParent); - parents.push(indexParentCommit); - } - } - + const head = yield repo.getHeadCommit(); + const headTree = yield head.getTree(); const subsTree = yield TreeUtil.writeTree(repo, headTree, subChanges); const stashId = yield Commit.create(repo, null, @@ -439,8 +333,8 @@ commit to have two parents`); null, "stash", subsTree, - parents.length, - parents); + 1, + [head]); const stashSha = stashId.tostrS(); @@ -544,7 +438,6 @@ exports.apply = co.wrap(function *(repo, id, reinstateIndex) { assert.isString(id); const commit = yield repo.getCommit(id); - const repoIndex = yield repo.index(); // TODO: patch libgit2/nodegit: the commit object returned from `parent` // isn't properly configured with a `repo` object, and attempting to use it @@ -552,53 +445,12 @@ exports.apply = co.wrap(function *(repo, id, reinstateIndex) { const parentId = (yield commit.parent(0)).id(); const parent = yield repo.getCommit(parentId); - const baseSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, parent, null); - - let indexSubs = baseSubs; - if (commit.parentcount() > 1) { - const parent2Id = (yield commit.parent(1)).id(); - const parent2 = yield repo.getCommit(parent2Id); - indexSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, - parent2, - null); - } - const newSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, commit, null); - - const toDelete = []; - yield Object.keys(baseSubs).map(co.wrap(function *(name) { - if (newSubs[name] === undefined) { - if (fs.existsSync(name)) { - // sub deleted in working tree - toDelete.push(name); - } - } - })); - - CloseUtil.close(repo, repo.workdir(), toDelete, false); - for (const name of toDelete) { - yield fs.rmdir(name); - } - - yield Object.keys(baseSubs).map(co.wrap(function *(name) { - if (indexSubs[name] === undefined) { - // sub deleted in the index - yield repoIndex.removeByPath(name); - } - })); - - // apply gitmodules diff - const headTree = yield commit.getTree(); - yield NodeGit.Checkout.tree(repo, headTree, { - paths: [GITMODULES], - checkoutStrategy: NodeGit.Checkout.STRATEGY.MERGE, - }); - const opener = new Open.Opener(repo, null); let result = {}; const index = {}; @@ -698,6 +550,8 @@ for debugging, is:`, e); } })); + const repoIndex = yield repo.index(); + if (null !== result) { for (let name of Object.keys(index)) { const entry = new NodeGit.IndexEntry(); diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 9d9075666..3b53f3028 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -247,8 +247,8 @@ exports.getSubmoduleStatus = co.wrap(function *(repo, commit = new Submodule.Commit(commitSha, commitUrl); } - // A null indexUrl indicates that the submodule doesn't exist in - // the staged .gitmodules. + // A null indexUrl indicates that the submodule was removed. If that is + // the case, we're done. if (null === indexUrl) { return new Submodule({ commit: commit }); // RETURN From c3e9e538ea053a51f2675e32c9112d4b81b5d8f6 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 5 Aug 2021 12:51:55 -0400 Subject: [PATCH 380/402] Stash deletion of submodules (second try) --- node/lib/cmd/stash.js | 4 + node/lib/util/repo_status.js | 3 +- node/lib/util/rm.js | 18 ++- node/lib/util/stash_util.js | 217 +++++++++++++++++++++++++++++------ node/lib/util/status_util.js | 4 +- 5 files changed, 207 insertions(+), 39 deletions(-) diff --git a/node/lib/cmd/stash.js b/node/lib/cmd/stash.js index 7ddfa2720..76a653287 100644 --- a/node/lib/cmd/stash.js +++ b/node/lib/cmd/stash.js @@ -117,6 +117,10 @@ function cleanSubs(status, includeUntracked) { for (let subName in subs) { const sub = subs[subName]; const wd = sub.workdir; + if (sub.index === null) { + // This sub was deleted + return false; + } if (sub.commit.sha !== sub.index.sha) { // The submodule has a commit which is staged in the meta repo's // index diff --git a/node/lib/util/repo_status.js b/node/lib/util/repo_status.js index 4abd820a6..f3e40f6e8 100644 --- a/node/lib/util/repo_status.js +++ b/node/lib/util/repo_status.js @@ -372,7 +372,8 @@ class Submodule { */ isCommittable () { return !(this.isNew() && - null === this.d_index.sha && + (null === this.d_index || + null === this.d_index.sha) && (null === this.d_workdir || null === this.d_workdir.status.headCommit && this.d_workdir.status.isIndexClean())); diff --git a/node/lib/util/rm.js b/node/lib/util/rm.js index 2036c3fa7..b27860337 100644 --- a/node/lib/util/rm.js +++ b/node/lib/util/rm.js @@ -461,7 +461,23 @@ exports.rmPaths = co.wrap(function *(repo, paths, options) { } if (stat !== null) { if (stat.isDirectory()) { - yield fs.rmdir(fullpath); + try { + yield fs.rmdir(fullpath); + } catch (e) { + if ("ENOTEMPTY" === e.code) { + // Repo still exists for some reason -- + // perhaps it was reported as deleted + // because it's not in the index, but it + // does exist on disk. For safety, do not + // delete it; this is a weird case. + console.error( + `Could not remove ${fullpath} -- it's not +empty. If you are sure it is not needed, you can remove it yourself.` + ); + } else { + throw e; + } + } } else { yield fs.unlink(fullpath); } diff --git a/node/lib/util/stash_util.js b/node/lib/util/stash_util.js index 34b257502..ba5e75eb8 100644 --- a/node/lib/util/stash_util.js +++ b/node/lib/util/stash_util.js @@ -33,8 +33,10 @@ const assert = require("chai").assert; const co = require("co"); const colors = require("colors"); +const fs = require("fs-promise"); const NodeGit = require("nodegit"); +const CloseUtil = require("./close_util"); const ConfigUtil = require("./config_util"); const DiffUtil = require("./diff_util"); const GitUtil = require("./git_util"); @@ -44,11 +46,19 @@ const RepoStatus = require("./repo_status"); const SparseCheckoutUtil = require("./sparse_checkout_util"); const StatusUtil = require("./status_util"); const SubmoduleUtil = require("./submodule_util"); +const SubmoduleConfigUtil = require("./submodule_config_util"); const SubmoduleRebaseUtil = require("./submodule_rebase_util"); const TreeUtil = require("./tree_util"); const UserError = require("./user_error"); const Commit = NodeGit.Commit; +const Change = TreeUtil.Change; +const FILEMODE = NodeGit.TreeEntry.FILEMODE; + +const MAGIC_DELETED_SHA = NodeGit.Oid.fromString( + "de1e7ed0de1e7ed0de1e7ed0de1e7ed0de1e7ed0"); + +const GITMODULES = SubmoduleConfigUtil.modulesFileName; /** * Return the IDs of tress reflecting the current state of the index and @@ -107,6 +117,62 @@ exports.makeLogMessage = co.wrap(function *(repo) { WIP on ${branchDesc}: ${GitUtil.shortSha(head.id().tostrS())} ${message}`; }); + +function getNewGitModuleSha(diff) { + const numDeltas = diff.numDeltas(); + for (let i = 0; i < numDeltas; ++i) { + const delta = diff.getDelta(i); + // We assume that the user hasn't deleted the .gitmodules file. + // That would be bonkers. + const file = delta.newFile(); + const path = file.path(); + if (path === GITMODULES) { + return delta.newFile().id(); + } + } + // diff does not include .gitmodules + return null; +} + + +const stashGitModules = co.wrap(function *(repo, headTree) { + assert.instanceOf(repo, NodeGit.Repository); + + const result = {}; + // RepoStatus throws away the diff new sha, and rather than hack + // it, since it's used all over the codebase, we'll just redo the + // diffs for this one file. + + const workdirToTreeDiff = + yield NodeGit.Diff.treeToWorkdir(repo, + headTree, + {pathspec: [GITMODULES]}); + + + const newWorkdir = getNewGitModuleSha(workdirToTreeDiff); + if (newWorkdir !== null) { + result.workdir = newWorkdir; + } + + const indexToTreeDiff = + yield NodeGit.Diff.treeToIndex(repo, + headTree, + yield repo.index(), + {pathspec: [GITMODULES]}); + + const newIndex = getNewGitModuleSha(indexToTreeDiff); + if (newIndex !== null) { + result.staged = newIndex; + } + + yield NodeGit.Checkout.tree(repo, headTree, { + paths: [GITMODULES], + checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, + }); + + return result; +}); + /** * Save the state of the submodules in the specified, `repo` having the * specified `status` and clean the sub-repositories to match their respective @@ -149,6 +215,8 @@ exports.save = co.wrap(function *(repo, status, includeUntracked, message) { const subRepos = {}; // name to submodule open repo const sig = yield ConfigUtil.defaultSignature(repo); + const head = yield repo.getHeadCommit(); + const headTree = yield head.getTree(); // First, we process the submodules. If a submodule is open and dirty, // we'll create the stash commits in its repo, populate `subResults` with @@ -178,34 +246,42 @@ report this. Continuing stash anyway.`); if (null === wd) { // closed submodule - if (sub.commit.sha === sub.index.sha) { - // ... with no staged changes - return; // RETURN - } - // This is a case that regular git stash doesn't really have - // to handle. In a normal stash commit, the tree points - // to the working directory tree, but here, there is no working - // directory. But if there were, we would want to have - // this commit checked out. - - const subRepo = yield SubmoduleUtil.getRepo(repo, name); + if (sub.index === null || sub.index.sha === null) { + // deleted submodule + stashId = MAGIC_DELETED_SHA; + yield NodeGit.Checkout.tree(repo, headTree, { + paths: [name], + checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE, + }); + } else { + if (sub.commit.sha === sub.index.sha) { + // ... with no staged changes + return; // RETURN + } + // This is a case that regular git stash doesn't really have + // to handle. In a normal stash commit, the tree points + // to the working directory tree, but here, there is no working + // directory. But if there were, we would want to have + // this commit checked out. - const subCommit = yield Commit.lookup(subRepo, sub.commit.sha); - const indexCommit = yield Commit.lookup(subRepo, sub.index.sha); - const indexTree = yield indexCommit.getTree(); - stashId = yield Commit.create(subRepo, - null, - sig, - sig, - null, - "stash", - indexTree, - 4, - [subCommit, - indexCommit, - indexCommit, - indexCommit]); + const subRepo = yield SubmoduleUtil.getRepo(repo, name); + const subCommit = yield Commit.lookup(subRepo, sub.commit.sha); + const indexCommit = yield Commit.lookup(subRepo, sub.index.sha); + const indexTree = yield indexCommit.getTree(); + stashId = yield Commit.create(subRepo, + null, + sig, + sig, + null, + "stash", + indexTree, + 4, + [subCommit, + indexCommit, + indexCommit, + indexCommit]); + } } else { // open submodule if (sub.commit.sha !== sub.index.sha && @@ -318,13 +394,42 @@ commit to have two parents`); subResults[name] = stashId.tostrS(); // Record the values we've created. - subChanges[name] = new TreeUtil.Change( - stashId, - NodeGit.TreeEntry.FILEMODE.COMMIT); + subChanges[name] = new TreeUtil.Change(stashId, FILEMODE.COMMIT); })); - const head = yield repo.getHeadCommit(); - const headTree = yield head.getTree(); + const parents = [head]; + + const gitModulesChanges = yield stashGitModules(repo, headTree); + if (gitModulesChanges) { + if (gitModulesChanges.workdir) { + subChanges[GITMODULES] = new Change(gitModulesChanges.workdir, + FILEMODE.BLOB); + } + if (gitModulesChanges.staged) { + const indexChanges = {}; + Object.assign(indexChanges, subChanges); + + indexChanges[GITMODULES] = new Change(gitModulesChanges.staged, + FILEMODE.BLOB); + + + const indexTree = yield TreeUtil.writeTree(repo, headTree, + indexChanges); + const indexParent = yield Commit.create(repo, + null, + sig, + sig, + null, + "stash", + indexTree, + 1, + [head]); + + const indexParentCommit = yield Commit.lookup(repo, indexParent); + parents.push(indexParentCommit); + } + } + const subsTree = yield TreeUtil.writeTree(repo, headTree, subChanges); const stashId = yield Commit.create(repo, null, @@ -333,8 +438,8 @@ commit to have two parents`); null, "stash", subsTree, - 1, - [head]); + parents.length, + parents); const stashSha = stashId.tostrS(); @@ -438,6 +543,7 @@ exports.apply = co.wrap(function *(repo, id, reinstateIndex) { assert.isString(id); const commit = yield repo.getCommit(id); + const repoIndex = yield repo.index(); // TODO: patch libgit2/nodegit: the commit object returned from `parent` // isn't properly configured with a `repo` object, and attempting to use it @@ -445,12 +551,55 @@ exports.apply = co.wrap(function *(repo, id, reinstateIndex) { const parentId = (yield commit.parent(0)).id(); const parent = yield repo.getCommit(parentId); + const parentTree = yield parent.getTree(); + const baseSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, parent, null); + + let indexSubs = baseSubs; + if (commit.parentcount() > 1) { + const parent2Id = (yield commit.parent(1)).id(); + const parent2 = yield repo.getCommit(parent2Id); + indexSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, + parent2, + null); + } + const newSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo, commit, null); + + const toDelete = []; + yield Object.keys(baseSubs).map(co.wrap(function *(name) { + if (newSubs[name] === undefined) { + if (fs.existsSync(name)) { + // sub deleted in working tree + toDelete.push(name); + } + } + })); + + CloseUtil.close(repo, repo.workdir(), toDelete, false); + for (const name of toDelete) { + yield fs.rmdir(name); + } + + yield Object.keys(baseSubs).map(co.wrap(function *(name) { + if (indexSubs[name] === undefined) { + // sub deleted in the index + yield repoIndex.removeByPath(name); + } + })); + + // apply gitmodules diff + const headTree = yield commit.getTree(); + yield NodeGit.Checkout.tree(repo, headTree, { + paths: [GITMODULES], + baseline: parentTree, + checkoutStrategy: NodeGit.Checkout.STRATEGY.MERGE, + }); + const opener = new Open.Opener(repo, null); let result = {}; const index = {}; @@ -550,8 +699,6 @@ for debugging, is:`, e); } })); - const repoIndex = yield repo.index(); - if (null !== result) { for (let name of Object.keys(index)) { const entry = new NodeGit.IndexEntry(); diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js index 3b53f3028..9d9075666 100644 --- a/node/lib/util/status_util.js +++ b/node/lib/util/status_util.js @@ -247,8 +247,8 @@ exports.getSubmoduleStatus = co.wrap(function *(repo, commit = new Submodule.Commit(commitSha, commitUrl); } - // A null indexUrl indicates that the submodule was removed. If that is - // the case, we're done. + // A null indexUrl indicates that the submodule doesn't exist in + // the staged .gitmodules. if (null === indexUrl) { return new Submodule({ commit: commit }); // RETURN From 2b7751c05d1bf3927d8454979bdbd56ae725b8a8 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 26 Aug 2021 11:32:02 -0400 Subject: [PATCH 381/402] fix git meta submodule status . --- node/lib/cmd/submodule.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node/lib/cmd/submodule.js b/node/lib/cmd/submodule.js index e3554b9b4..11a0513ce 100644 --- a/node/lib/cmd/submodule.js +++ b/node/lib/cmd/submodule.js @@ -171,13 +171,13 @@ const doStatusCommand = co.wrap(function *(paths, verbose) { const relCwd = path.relative(workdir, cwd); const head = yield repo.getHeadCommit(); const tree = yield head.getTree(); + paths = paths.map(filename => { + return GitUtil.resolveRelativePath(workdir, cwd, filename); + }); const status = yield StatusUtil.getRepoStatus(repo, { paths: paths, showMetaChanges: false, }); - paths = paths.map(filename => { - return GitUtil.resolveRelativePath(workdir, cwd, filename); - }); const urls = yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, head); const allSubs = Object.keys(urls); const subs = status.submodules; From b0e185a385bb03565838cc294b9667f26fe29c1e Mon Sep 17 00:00:00 2001 From: Shijing Lu Date: Tue, 25 Jan 2022 19:00:30 -0500 Subject: [PATCH 382/402] Add a command that allows creating a new tree by patching an existing one with the output from "git diff-tree" Suppose you are in a bare repo, and you want to create a new commit by adding a file, you can run: echo ':000000 100644 0000000000000000000000000000000000000000 a86fc6156eafad6fd0c40d17752da3232dded9b0 A ts/foo/baz.txt' | patch-tree HEAD The input should have the same format as the output of "git diff-tree -r --raw". patch-tree will first upsert foo/bar/baz.txt as a leaf to HEAD's tree, and then recursively running mktree for "foo/bar", "foo/" and then root. Tree entry change can also be defined by short hand diff: For example: t1=$(patch-tree -s "commit::ts/modeling/bamboo/core" HEAD) patch-tree -s \ "T:467c15c20ab76a4fc89c6c09b4f047e31d531879:ts/modeling/bamboo/core" $t1 means removing the commit at 'ts/modeling/bamboo/core' and adding its tree (stitch) --- node/bin/patch-tree | 34 ++++++++ node/lib/patch-tree.js | 178 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100755 node/bin/patch-tree create mode 100644 node/lib/patch-tree.js diff --git a/node/bin/patch-tree b/node/bin/patch-tree new file mode 100755 index 000000000..126f40adc --- /dev/null +++ b/node/bin/patch-tree @@ -0,0 +1,34 @@ +#!/usr/bin/env node +/* + * Copyright (c) 2022, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +require("./../lib/patch-tree.js"); + diff --git a/node/lib/patch-tree.js b/node/lib/patch-tree.js new file mode 100644 index 000000000..0e8c091a6 --- /dev/null +++ b/node/lib/patch-tree.js @@ -0,0 +1,178 @@ +#!/usr/bin/env node +/* + * Copyright (c) 2017, Two Sigma Open Source + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of git-meta nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +const argparse = require("argparse"); +const ArgumentParser = argparse.ArgumentParser; +const co = require("co"); +const fs = require("fs"); +const NodeGit = require("nodegit"); +const TreeUtil = require("./util/tree_util"); +const UserError = require("./util/user_error"); + +const FILEMODE = NodeGit.TreeEntry.FILEMODE; +const RE_DIFF_RAW = /^:\d+ (\d+) [0-9a-f]{40} ([0-9a-f]{40}) \w\s(.+)$/; +const RE_SHORT_DIFF = /^(T|C|B|tree|commit|blob):([0-9a-f]{40})?:(.+)$/; +const DELETION_SHA = "0".repeat(40); +const ALLOWED_FILE_MODES = { + "100644": FILEMODE.BLOB, + "040000": FILEMODE.TREE, + "160000": FILEMODE.COMMIT, + "B": FILEMODE.BLOB, + "T": FILEMODE.TREE, + "C": FILEMODE.COMMIT, + "blob": FILEMODE.BLOB, + "tree": FILEMODE.TREE, + "commit": FILEMODE.COMMIT, +}; + +const description = `Creating a new tree by patching an existing one + with git-diff-tree output. Works in bare repository too. + +Suppose you are in a bare repo, and you want to create a new commit by + adding a file, you can run: + +echo ' +:000000 100644 0000000000000000000000000000000000000000 + a86fc6156eafad6fd0c40d17752da3232dded9b0 + A ts/foo/baz.txt' | amend-tree HEAD + +The input should have the same format as the output of "git diff-tree -r --raw". + +amend-tree will first upsert foo/bar/baz.txt as a leaf to HEAD's tree, and then +recursively running mktree for "foo/bar", "foo/" and then root. +ss +Tree entry change can also be defined by short hand diff. + +For example: + t1=$(patch-tree -s "commit::ts/modeling/bamboo/core" HEAD) + patch-tree -s \ + "T:467c15c20ab76a4fc89c6c09b4f047e31d531879:ts/modeling/bamboo/core" $t1 + +means removing the commit at 'ts/modeling/bamboo/core' and adding its tree. +`; + + +const parser = new ArgumentParser({ + addHelp: true, + description: description, +}); + +parser.addArgument(["-F", "--diff-file"], { + type: "string", + help: "File from which to read diff-tree style input.", + required: false, +}); + +parser.addArgument(["-s", "--short"], { + defaultValue: false, + required: false, + action: "storeConst", + constant: true, + help: `Diff format, either raw: '${RE_DIFF_RAW}' ` + + `or short: '${RE_SHORT_DIFF}'.` +}); + +parser.addArgument(["treeish"], { + type: "string", + help: "tree to amend", +}); + + +/** + * Read the output from "git diff-tree --raw" and return a map of + * structured tree entry changes. + * + * @param {str} diff multi-lines of git diff-tree output + * @returns {Object} changes map from path to `TreeUtil.Change` + */ +const getChanges = ( + diff, + isShort +) => diff.split(/\r\n|\r|\n/).reduce((acc, line) => { + const PAT = isShort ? RE_SHORT_DIFF : RE_DIFF_RAW; + const match = PAT.exec(line); + if (!line) { + return acc; + } + if (!match) { + throw new UserError(`'${line}' is invalid, accept format: ` + PAT); + } + const mode = ALLOWED_FILE_MODES[match[1]]; + const blobId = match[2]; + const filePath = match[3]; + if (!mode) { + throw new UserError( + `Unsupported file mode: '${match[1]}', only + ${Object.keys(ALLOWED_FILE_MODES)} are supported` + PAT); + } + acc[filePath] = (blobId === DELETION_SHA || !blobId) ? + null : + new TreeUtil.Change(NodeGit.Oid.fromString(blobId), mode); + return acc; +}, {}); + + +const runCmd = co.wrap(function* (args) { + const diff = fs.readFileSync(args.diff_file || 0, "utf-8"); + const changes = getChanges(diff, args.short); + const location = yield NodeGit.Repository.discover(".", 0, ""); + const repo = yield NodeGit.Repository.open(location); + const treeOrCommitish = yield NodeGit.Revparse.single(repo, args.treeish); + + if (!treeOrCommitish) { + throw new UserError( + `Cannot rev-parse: '${args.treeish}', make sure it is valid`); + } + const baseTreeObj = yield treeOrCommitish.peel(NodeGit.Object.TYPE.TREE); + + if (!baseTreeObj) { + throw new UserError( + `${args.treeish} cannot be resolve to a tree` + + "is should be a commitish or treeish"); + } + const baseTree = yield NodeGit.Tree.lookup(repo, baseTreeObj); + const amendedTree = yield TreeUtil.writeTree(repo, baseTree, changes); + console.log(amendedTree.id().tostrS()); +}); + +co(function* () { + try { + yield runCmd(parser.parseArgs()); + } + catch (e) { + if (e instanceof UserError) { + console.error(e.message); + } else { + console.error(e.stack); + } + process.exit(1); + } +}); From ced6ad460f421b085eaf04b89644b389b4083bf8 Mon Sep 17 00:00:00 2001 From: leahnp Date: Wed, 16 Feb 2022 18:30:41 -0500 Subject: [PATCH 383/402] add workflow_dispatch event to enable manual triggering of workflows --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a5ce40d3..7101dae82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,5 @@ name: CI -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: Test-Git-Meta: runs-on: ubuntu-latest From 23e1f60a5a53ee63c7343c423847868c7b17ce23 Mon Sep 17 00:00:00 2001 From: Geoffrey Thomas Date: Fri, 1 Apr 2022 08:31:33 -0400 Subject: [PATCH 384/402] Fix broken test on newer Node.js versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This test has always been broken, but in older versions of Node, it didn't report a failure, because unhandled promise rejections didn't cause a test failure: ``` SubmoduleUtil listOpenSubmodules ✓ listOpenSubmodules-missing (59ms) (node:1506) UnhandledPromiseRejectionWarning: Error: ENOENT: no such file or directory, open '/tmp/git-meta-test202231-1506-r2maox.wm1g/config' at Object.fs.openSync (fs.js:646:18) at Object.fs.readFileSync (fs.js:551:33) at Object. (/root/git-meta2/node/lib/util/submodule_config_util.js:102:27) at Generator.next () at onFulfilled (/root/git-meta2/node/node_modules/co/index.js:65:19) at /root/git-meta2/node/node_modules/co/index.js:54:5 at new Promise () at Object.co (/root/git-meta2/node/node_modules/co/index.js:50:10) at Object.createPromise [as clearSubmoduleConfigEntry] (/root/git-meta2/node/node_modules/co/index.js:30:15) at Object. (/root/git-meta2/node/lib/util/submodule_config_util.js:494:19) at Generator.next () at onFulfilled (/root/git-meta2/node/node_modules/co/index.js:65:19) at /root/git-meta2/node/node_modules/co/index.js:54:5 at new Promise () at Object.co (/root/git-meta2/node/node_modules/co/index.js:50:10) at Object.createPromise [as initSubmodule] (/root/git-meta2/node/node_modules/co/index.js:30:15) at Context. (/root/git-meta2/node/test/util/submodule_util.js:371:33) at Generator.next () at onFulfilled (/root/git-meta2/node/node_modules/co/index.js:65:19) at at process._tickCallback (internal/process/next_tick.js:189:7) (node:1506) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2) (node:1506) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. ``` Since Node.js v15 this causes an actual test failure. GitHub CI uses Node.js v16. --- node/test/util/submodule_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js index cca25109f..930a1b23f 100644 --- a/node/test/util/submodule_util.js +++ b/node/test/util/submodule_util.js @@ -368,7 +368,7 @@ describe("SubmoduleUtil", function () { const repo = yield TestUtil.createSimpleRepository(); const url = "/Users/peabody/repos/git-meta-demo/scripts/demo/z-bare"; - SubmoduleConfigUtil.initSubmodule(repo.workdir(), "z", url); + SubmoduleConfigUtil.initSubmodule(repo.path(), "z", url); const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo); assert.deepEqual(openSubs, []); })); From a6eb8a1579eb6c6a37a78272cd26882ac6e0d01c Mon Sep 17 00:00:00 2001 From: Geoffrey Thomas Date: Fri, 10 Jun 2022 07:02:36 -0400 Subject: [PATCH 385/402] Fix flaky test "rebase in a sub, was conflicted" (fixes #864) Make a branch for the original commit of the conflicted rebase so that `git gc` isn't inclined to garbage-collect it. --- node/test/util/submodule_rebase_util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/test/util/submodule_rebase_util.js b/node/test/util/submodule_rebase_util.js index e187bb395..178a306ab 100644 --- a/node/test/util/submodule_rebase_util.js +++ b/node/test/util/submodule_rebase_util.js @@ -311,9 +311,9 @@ x=E:CM-3 s=Sa:qs;Bmaster=M;Os Cqs-r q=q!H=qs!E`, "rebase in a sub, was conflicted": { initial: ` a=B:Cq-1;Cr-1;Bq=q;Br=r| -x=U:C3-2 s=Sa:q;Bmaster=3;I *s=S:1*S:r*S:q;Os EHEAD,q,r!I q=q`, +x=U:C3-2 s=Sa:q;Bmaster=3;I *s=S:1*S:r*S:q;Os EHEAD,q,r!I q=q!Bq=q`, expected: ` -x=E:CM-3 s=Sa:qs;Bmaster=M;I s=~;Os Cqs-r q=q!H=qs!E`, +x=E:CM-3 s=Sa:qs;Bmaster=M;I s=~;Os Cqs-r q=q!H=qs!E!Bq=q`, baseCommit: "3", }, "rebase two in a sub": { From ee21b1c0c35b398c10db9dd98ee02328bbfb73cc Mon Sep 17 00:00:00 2001 From: Ang Lee Date: Thu, 20 Jan 2022 23:27:40 -0500 Subject: [PATCH 386/402] Make hooks work with hooksPath config --- node/lib/util/commit.js | 2 +- node/lib/util/git_util.js | 2 +- node/lib/util/hook.js | 41 +++++++++++++++++++++++++++++++-------- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js index 1b17d1115..92e92844e 100644 --- a/node/lib/util/commit.js +++ b/node/lib/util/commit.js @@ -227,7 +227,7 @@ const runPreCommitHook = co.wrap(function *(repo, index) { let release = null; try { - if (Hook.hasHook(repo, "pre-commit")) { + if (yield Hook.hasHook(repo, "pre-commit")) { release = yield mutex.acquire(); const isOk = yield Hook.execHook(repo, "pre-commit", [], { GIT_INDEX_FILE: tempIndexPath}); diff --git a/node/lib/util/git_util.js b/node/lib/util/git_util.js index 40b961ec2..638f7ed4b 100755 --- a/node/lib/util/git_util.js +++ b/node/lib/util/git_util.js @@ -805,7 +805,7 @@ Please supply the message using the -m option.`); } - if (runHooks && Hook.hasHook(repo, "commit-msg")) { + if (runHooks && (yield Hook.hasHook(repo, "commit-msg"))) { const isOk = yield Hook.execHook(repo, "commit-msg", [messagePath]); if (!isOk) { diff --git a/node/lib/util/hook.js b/node/lib/util/hook.js index eb4f2a6dc..d3e5a71b5 100644 --- a/node/lib/util/hook.js +++ b/node/lib/util/hook.js @@ -36,22 +36,48 @@ const co = require("co"); const path = require("path"); const process = require("process"); const fs = require("fs"); +const ConfigUtil = require("./config_util"); + + +/** + * Check if git-meta hook with given hook name exists + * Return hooks path. + * @async + * @return {String} + */ +const getHooksPath = co.wrap(function*(repo) { + const rootDirectory = repo.path(); + const config = yield repo.config(); + const worktreeConfig = yield ConfigUtil.getConfigString( + config, + `core.worktree` + ); + const hooksPathConfig = yield ConfigUtil.getConfigString( + config, + `core.hooksPath` + ); + return path.join( + rootDirectory, + worktreeConfig && hooksPathConfig ? + `${worktreeConfig}/${hooksPathConfig}` : "hooks" + ); +}); + /** * Check if git-meta hook with given hook name exists * Return true if hook exists. + * @async * @param {String} name * @return {Boolean} */ -exports.hasHook = function (repo, name) { +exports.hasHook = co.wrap(function*(repo, name) { assert.isString(name); - - const rootDirectory = repo.path(); - const hookPath = path.join(rootDirectory, "hooks"); + const hookPath = yield getHooksPath(repo); const absPath = path.resolve(hookPath, name); return fs.existsSync(absPath); -}; +}); /** * Run git-meta hook with given hook name. @@ -64,8 +90,7 @@ exports.hasHook = function (repo, name) { exports.execHook = co.wrap(function*(repo, name, args=[], env={}) { assert.isString(name); - const rootDirectory = repo.path(); - const hookPath = path.join(rootDirectory, "hooks"); + const hookPath = yield getHooksPath(repo); const absPath = path.resolve(hookPath, name); if (!fs.existsSync(absPath)) { @@ -77,7 +102,7 @@ exports.execHook = co.wrap(function*(repo, name, args=[], env={}) { Object.assign(subEnv, process.env); Object.assign(subEnv, env); yield spawn(absPath, args, { stdio: "inherit", env: subEnv, - cwd: repo.workdir()}); + cwd: repo.workdir()}); return true; } catch (e) { if (e.code === "EACCES") { From 1f5c0f9c35b0aac32eaa379d83140cc626b6320e Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 27 Jul 2022 10:30:55 -0600 Subject: [PATCH 387/402] Fixed typos in architecture.md --- doc/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/architecture.md b/doc/architecture.md index 356500080..20ba9dc5a 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -114,7 +114,7 @@ products that take a similar approach: - [Git submodules](https://git-scm.com/docs/git-submodule) All of these tools overlap with the problems git-meta is trying to solve, but -none of them are sufficient: +none of them is sufficient: - Most don't provide a way to reference the state of all repositories (Gitslave and Myrepos). git-repo has the ability to reference the state of all repos, From 9e3a6c0480685f184ec5aaa39ddcb39ec58e32fb Mon Sep 17 00:00:00 2001 From: Matt Selsky Date: Fri, 4 Mar 2022 18:29:27 -0500 Subject: [PATCH 388/402] Typo in comment --- node/lib/cmd/commit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/commit.js b/node/lib/cmd/commit.js index a7afa70a4..c1e91c40c 100644 --- a/node/lib/cmd/commit.js +++ b/node/lib/cmd/commit.js @@ -73,7 +73,7 @@ exports.configureParser = function (parser) { action: "storeConst", constant: true, help: `\ -Amend the last commit, including newly staged chnages and, (if -a is \ +Amend the last commit, including newly staged changes and, (if -a is \ specified) modifications. Will fail unless all submodules changed in HEAD \ have matching commits and have no new commits.`, }); From 8afd587ea31ac57c9e1ac1a2e7f39271b96486b3 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 9 Aug 2022 12:57:04 -0600 Subject: [PATCH 389/402] implemented extra error message when running into git-meta cherry-pick conflict next steps --- node/lib/util/cherry_pick_util.js | 45 +++++++++++++++++++----------- node/lib/util/rebase_util.js | 3 +- node/test/util/cherry_pick_util.js | 18 +++++++++--- node/test/util/rebase_util.js | 5 +++- 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js index c3e02224f..86ee141d7 100644 --- a/node/lib/util/cherry_pick_util.js +++ b/node/lib/util/cherry_pick_util.js @@ -103,7 +103,7 @@ exports.changeSubmodules = co.wrap(function *(repo, if (0 === Object.keys(submodules).count) { return; // RETURN } - const urls = (urlsInIndex === null || urlsInIndex === undefined) ? + const urls = (urlsInIndex === null || urlsInIndex === undefined) ? (yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index)) : urlsInIndex; const changes = {}; @@ -149,7 +149,7 @@ exports.changeSubmodules = co.wrap(function *(repo, /** * Update meta repo index and point the submodule to a commit sha -* +* * @param {NodeGit.Index} index * @param {String} subName * @param {String} sha @@ -171,9 +171,9 @@ exports.addSubmoduleCommit = co.wrap(function *(index, subName, sha) { * Similar to exports.changeSubmodules, but it: * 1. operates in bare repo * 2. does not make any changes to the working directory - * 3. only deals with simple changes like addition, deletions + * 3. only deals with simple changes like addition, deletions * and fast-forwards - * + * * @param {NodeGit.Repository} repo * @param {NodeGit.Index} index meta repo's change index * @param {Object} submodules name to Submodule @@ -307,11 +307,11 @@ const workAroundLibgit2MergeBug = co.wrap(function *(data, repo, name, }); /** - * + * * @param {Object} ancestorUrls urls from the merge base * @param {Object} ourUrls urls from the left side of a merge * @param {Object} theirUrls urls from the right side - * @returns {Object} + * @returns {Object} * @returns {Object} return.url submodule name to URLs * @returns {Object} return.conflicts: name to a conflict object that contains * urls of ancestors, ours and theirs. @@ -355,15 +355,15 @@ exports.resolveUrlsConflicts = function(ancestorUrls, ourUrls, theirUrls) { /** * Resolve conflicts to `.gitmodules` file, return the merged list of urls or * a Conflict object indicating the merge cannot be done automatically. - * + * * @param repo repository where blob of `.gitmodules` can be read - * @param {(null|NodeGit.IndexEntry)} ancestorEntry entry of `.gitmodules` + * @param {(null|NodeGit.IndexEntry)} ancestorEntry entry of `.gitmodules` * from merge base - * @param {(null|NodeGit.IndexEntry)} ourEntry entry of `.gitmodules` + * @param {(null|NodeGit.IndexEntry)} ourEntry entry of `.gitmodules` * on the left side - * @param {(null|NodeGit.IndexEntry)} theirEntry entry of `.gitmodules` + * @param {(null|NodeGit.IndexEntry)} theirEntry entry of `.gitmodules` * on the right side - * @returns {Object} + * @returns {Object} * @returns {Object} return.urls, list of sub names to urls * @returns {Object} return.conflicts, object describing conflicts */ @@ -399,9 +399,9 @@ exports.resolveModuleFileConflicts = co.wrap(function*( * @return {Object} return.simpleChanges from sub name to `Submodule` or null * @return {Object} return.conflicts from sub name to `Conflict` * */ -exports.computeChangesBetweenTwoCommits = co.wrap(function *(repo, - index, - srcCommit, +exports.computeChangesBetweenTwoCommits = co.wrap(function *(repo, + index, + srcCommit, targetCommit) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(index, NodeGit.Index); @@ -704,12 +704,15 @@ exports.closeSubs = co.wrap(function *(opener, changes) { * * @param {NodeGit.Repository} repo * @param {NodeGit.Commit} commit + * @param {String} commandName accepts git-meta command that + * gets checked for any conflicts when running rewriteCommit + * for a more elaborated errorMessage * @return {Object} return * @return {String|null} return.newMetaCommit * @return {Object} returm.submoduleCommits * @return {String|null} return.errorMessage */ -exports.rewriteCommit = co.wrap(function *(repo, commit) { +exports.rewriteCommit = co.wrap(function *(repo, commit, commandName) { assert.instanceOf(repo, NodeGit.Repository); assert.instanceOf(commit, NodeGit.Commit); @@ -747,6 +750,16 @@ exports.rewriteCommit = co.wrap(function *(repo, commit) { errorMessage += SubmoduleRebaseUtil.subConflictErrorMessage(name); }); + if (Object.keys(conflicts).length !== 0) { + errorMessage += `\ + A ${commandName} is in progress. + (after resolving conflicts mark the corrected paths + with 'git meta add', then run "git meta ${commandName} --continue") + (use "git meta ${commandName} --abort" to `+ + "check out the original branch)"; + } + + const result = { pickingCommit: commit, submoduleCommits: picks.commits, @@ -781,7 +794,7 @@ const pickRemainingCommits = co.wrap(function*(metaRepo, seq) { seq = seq.copy({currentCommit : i}); yield SequencerStateUtil.writeSequencerState(metaRepo.path(), seq); - result = yield exports.rewriteCommit(metaRepo, commit); + result = yield exports.rewriteCommit(metaRepo, commit, "cherry-pick"); if (null !== result.errorMessage) { return result; } diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js index 5b525dd7c..05e25653b 100644 --- a/node/lib/util/rebase_util.js +++ b/node/lib/util/rebase_util.js @@ -134,7 +134,8 @@ exports.runRebase = co.wrap(function *(repo, seq) { const sha = nextSeq.commits[i]; const commit = yield repo.getCommit(sha); SubmoduleRebaseUtil.logCommit(commit); - const cherryResult = yield CherryPickUtil.rewriteCommit(repo, commit); + const cherryResult = yield CherryPickUtil.rewriteCommit(repo, commit, + "rebase"); if (null !== cherryResult.newMetaCommit) { result.metaCommits[cherryResult.newMetaCommit] = sha; } diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js index 2fca63dc5..e00e35cab 100644 --- a/node/test/util/cherry_pick_util.js +++ b/node/test/util/cherry_pick_util.js @@ -744,7 +744,10 @@ a `, errorMessage: `\ Submodule ${colors.red("s")} is conflicted. -`, + A testCommand is in progress. + (after resolving conflicts mark the corrected paths + with 'git meta add', then run "git meta testCommand --continue") + (use "git meta testCommand --abort" to check out the original branch)`, }, "conflict in a sub pick, success in another": { input: ` @@ -762,7 +765,10 @@ a `, errorMessage: `\ Submodule ${colors.red("s")} is conflicted. -`, + A testCommand is in progress. + (after resolving conflicts mark the corrected paths + with 'git meta add', then run "git meta testCommand --continue") + (use "git meta testCommand --abort" to check out the original branch)`, }, }; Object.keys(cases).forEach(caseName => { @@ -773,7 +779,8 @@ Submodule ${colors.red("s")} is conflicted. assert.property(reverseCommitMap, "8"); const eightCommitSha = reverseCommitMap["8"]; const eightCommit = yield x.getCommit(eightCommitSha); - const result = yield CherryPickUtil.rewriteCommit(x, eightCommit); + const result = yield CherryPickUtil.rewriteCommit(x, eightCommit, + "testCommand"); assert.equal(result.errorMessage, c.errorMessage || null); return mapCommits(maps, result); }); @@ -836,7 +843,10 @@ a `, errorMessage: `\ Submodule ${colors.red("s")} is conflicted. -`, + A cherry-pick is in progress. + (after resolving conflicts mark the corrected paths + with 'git meta add', then run "git meta cherry-pick --continue") + (use "git meta cherry-pick --abort" to check out the original branch)`, }, }; diff --git a/node/test/util/rebase_util.js b/node/test/util/rebase_util.js index 568f07f81..f1b7e5a6d 100644 --- a/node/test/util/rebase_util.js +++ b/node/test/util/rebase_util.js @@ -369,7 +369,10 @@ t ;`, errorMessage: `\ Submodule ${colors.red("s")} is conflicted. -`, + A rebase is in progress. + (after resolving conflicts mark the corrected paths + with 'git meta add', then run "git meta rebase --continue") + (use "git meta rebase --abort" to check out the original branch)`, }, "does not close open submodules when rewinding": { initial: ` From 1f923c40e1cb0f0a91b54b65af0b0d0e8c60f9af Mon Sep 17 00:00:00 2001 From: gobisaTS Date: Wed, 21 Sep 2022 12:57:49 -0400 Subject: [PATCH 390/402] Allow `git meta add -u` with no path If `-u` is provided to `git meta add`, do not require a path. This more closely matches the behavior of `git add -u`. Output of `git meta add`: ``` Nothing specified, nothing added. hint: Maybe you wanted to say 'git meta add .'? ``` Output of `git add`: ``` Nothing specified, nothing added. hint: Maybe you wanted to say 'git add .'? hint: Turn this message off by running hint: "git config advice.addEmptyPathSpec false" ``` --- node/lib/cmd/add.js | 34 ++++++++++++++++++++++++++++++---- node/test/util/add.js | 24 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/node/lib/cmd/add.js b/node/lib/cmd/add.js index 4c1002b3d..7db55151c 100644 --- a/node/lib/cmd/add.js +++ b/node/lib/cmd/add.js @@ -78,7 +78,7 @@ and avoid accidental changes to the meta-repo.`, defaultValue: false, }); parser.addArgument(["paths"], { - nargs: "+", + nargs: "*", type: "string", help: "the paths to add", }); @@ -92,14 +92,40 @@ and avoid accidental changes to the meta-repo.`, * @param {String[]} args.paths */ exports.executeableSubcommand = co.wrap(function *(args) { - const Add = require("../util/add"); - const GitUtil = require("../util/git_util"); + const colors = require("colors"); + + const Add = require("../util/add"); + const GitUtil = require("../util/git_util"); + const StatusUtil = require("../util/status_util"); const repo = yield GitUtil.getCurrentRepo(); const workdir = repo.workdir(); const cwd = process.cwd(); - const paths = args.paths.map(filename => { + let userPaths = args.paths; + if (userPaths.length === 0) { + if (args.update) { + const repoStatus = yield StatusUtil.getRepoStatus(repo, { + cwd: cwd, + paths: args.path, + untrackedFilesOption: args.untrackedFilesOption + }); + + const fileStatuses = exports.accumulateStatus(repoStatus); + const workdirChanges = fileStatuses.workdir; + userPaths = workdirChanges.map(workdirChange => { + return workdirChange.path; + }); + } + else { + const text = "Nothing specified, nothing added.\n" + + `${colors.yellow("hint: Maybe you wanted to say ")}` + + `${colors.yellow("'git meta add .'?")}\n`; + process.stdout.write(text); + } + } + + const paths = userPaths.map(filename => { return GitUtil.resolveRelativePath(workdir, cwd, filename); }); yield Add.stagePaths(repo, paths, args.meta, args.update); diff --git a/node/test/util/add.js b/node/test/util/add.js index 8985581aa..f8b1e6580 100644 --- a/node/test/util/add.js +++ b/node/test/util/add.js @@ -140,6 +140,30 @@ a=B|x=S:C2-1 a/b=Sa:1;Oa/b W x/y/z=a,x/r/z=b;Bmaster=2`, expected: "x=E:Os I README.md=foo!W newFile=a", update: true, }, + "add only tracked files with no path": { + initial: "a=B|x=U:Os W README.md=foo,newFile=a", + paths: [], + expected: "x=E:Os I README.md=foo!W newFile=a", + update: true, + }, + "add existing file without providing path": { + initial: "x=S:W README.md=foo", + paths: [], + expected: "x=S:I README.md=foo", + update: true, + }, + "nothing to add with update and no path": { + initial: "x=S", + paths: [], + expected: "x=S", + update: true, + }, + "nothing to add with update and no path and untracked change": { + initial: "a=B|x=U:Os W newFile=a", + paths: [], + expected: "a=B|x=U:Os W newFile=a", + update: true, + }, "add all(tracked and not tracked) files": { initial: "a=B|x=U:Os W README.md=foo,newFile=a", paths: ["s"], From c3704054df21b15a51029f269bfc6015dae43f58 Mon Sep 17 00:00:00 2001 From: Ang Lee Date: Thu, 20 Oct 2022 14:23:26 -0400 Subject: [PATCH 391/402] Use 'PrintStatusUtil' in cmd/add --- node/lib/cmd/add.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node/lib/cmd/add.js b/node/lib/cmd/add.js index 7db55151c..761aaf944 100644 --- a/node/lib/cmd/add.js +++ b/node/lib/cmd/add.js @@ -31,6 +31,7 @@ "use strict"; const co = require("co"); +const PrintStatusUtil = require('../util/print_status_util.js'); /** * This module contains methods for implementing the `add` command. @@ -111,7 +112,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { untrackedFilesOption: args.untrackedFilesOption }); - const fileStatuses = exports.accumulateStatus(repoStatus); + const fileStatuses = PrintStatusUtil.accumulateStatus(repoStatus); const workdirChanges = fileStatuses.workdir; userPaths = workdirChanges.map(workdirChange => { return workdirChange.path; From 23f70693abe02e60eabef0db9f240d809b068c9e Mon Sep 17 00:00:00 2001 From: Ang Lee Date: Thu, 20 Oct 2022 14:30:17 -0400 Subject: [PATCH 392/402] Move require statement down --- node/lib/cmd/add.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/add.js b/node/lib/cmd/add.js index 761aaf944..ebeebc1aa 100644 --- a/node/lib/cmd/add.js +++ b/node/lib/cmd/add.js @@ -31,7 +31,6 @@ "use strict"; const co = require("co"); -const PrintStatusUtil = require('../util/print_status_util.js'); /** * This module contains methods for implementing the `add` command. @@ -98,6 +97,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { const Add = require("../util/add"); const GitUtil = require("../util/git_util"); const StatusUtil = require("../util/status_util"); + const PrintStatusUtil = require("../util/print_status_util"); const repo = yield GitUtil.getCurrentRepo(); const workdir = repo.workdir(); From b4118596a79a055dcb62d77f7463ff5842f90570 Mon Sep 17 00:00:00 2001 From: gobisaTS Date: Mon, 24 Oct 2022 23:36:45 -0400 Subject: [PATCH 393/402] Fix `add -u` command re-resolving relative paths Tracked filepaths were undergoing relative path resolution twice --- node/lib/cmd/add.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/node/lib/cmd/add.js b/node/lib/cmd/add.js index ebeebc1aa..a91b97d2a 100644 --- a/node/lib/cmd/add.js +++ b/node/lib/cmd/add.js @@ -123,11 +123,13 @@ exports.executeableSubcommand = co.wrap(function *(args) { `${colors.yellow("hint: Maybe you wanted to say ")}` + `${colors.yellow("'git meta add .'?")}\n`; process.stdout.write(text); + return; } } - - const paths = userPaths.map(filename => { - return GitUtil.resolveRelativePath(workdir, cwd, filename); - }); - yield Add.stagePaths(repo, paths, args.meta, args.update); + else { + userPaths = args.paths.map(filename => { + return GitUtil.resolveRelativePath(workdir, cwd, filename); + }); + } + yield Add.stagePaths(repo, userPaths, args.meta, args.update); }); From fe2e11809200c9cfddebb267764d878f0e21656d Mon Sep 17 00:00:00 2001 From: Chris Staykov <44845137+cstaykov@users.noreply.github.com> Date: Fri, 9 Dec 2022 20:51:08 -0500 Subject: [PATCH 394/402] Add no-commit option to cherry-pick --- node/lib/cmd/cherry_pick.js | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/node/lib/cmd/cherry_pick.js b/node/lib/cmd/cherry_pick.js index 7612d33d9..92aec8954 100644 --- a/node/lib/cmd/cherry_pick.js +++ b/node/lib/cmd/cherry_pick.js @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, Two Sigma Open Source + * Copyright (c) 2022, Two Sigma Open Source * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -68,6 +68,11 @@ exports.configureParser = function (parser) { constant: true, help: "continue an in-progress cherry-pick", }); + parser.addArgument(["-n", "--no-commit"], { + action: "storeConst", + constant: true, + help: "applies the changes necessary without making any commits", + }); // TODO: Note that ideally we might do something similar to // `git reset --merge`, but that would be (a) tricky and (b) it can fail, // leaving the cherry-pick still in progress. @@ -139,6 +144,11 @@ Could not resolve ${colors.red(commitish)} to a commit.`); * @param {String[]} args.commit */ exports.executeableSubcommand = co.wrap(function *(args) { + const path = require("path"); + const Reset = require("../util/reset"); + const Hook = require("../util/hook"); + const StatusUtil = require("../util/status_util"); + const PrintStatusUtil = require("../util/print_status_util"); const CherryPickUtil = require("../util/cherry_pick_util"); const repo = yield GitUtil.getCurrentRepo(); @@ -178,4 +188,19 @@ exports.executeableSubcommand = co.wrap(function *(args) { if (null !== result.errorMessage) { throw new UserError(result.errorMessage); } + + if (args.no_commit) { + const commitish = `HEAD~${commits.length}` + const annotated = yield GitUtil.resolveCommitish(repo, commitish); + const commit = yield repo.getCommit(annotated.id()); + yield Reset.reset(repo, commit, Reset.TYPE.SOFT); + + const repoStatus = yield StatusUtil.getRepoStatus(repo); + const cwd = process.cwd(); + const relCwd = path.relative(repo.workdir(), cwd); + const statusText = PrintStatusUtil.printRepoStatus(repoStatus, relCwd); + process.stdout.write(statusText); + + yield Hook.execHook(repo, "post-reset"); + } }); From c1fa65c462c2cfb136f70c137f802247d79b16d0 Mon Sep 17 00:00:00 2001 From: Shuyang Fang Date: Mon, 12 Dec 2022 13:27:17 -0500 Subject: [PATCH 395/402] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7101dae82..af37dd073 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: [push, pull_request, workflow_dispatch] jobs: Test-Git-Meta: - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 steps: - run: echo "The job was automatically triggered by a ${{ github.event_name }} event." - run: sudo apt-get install libkrb5-dev From 7e56eaf064cda3f73e1b853d24f912ee3840e305 Mon Sep 17 00:00:00 2001 From: george li Date: Sat, 10 Dec 2022 00:03:26 -0500 Subject: [PATCH 396/402] implement git meta add verbose flag --- node/lib/cmd/add.js | 11 ++++++++++- node/lib/util/add.js | 14 +++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/node/lib/cmd/add.js b/node/lib/cmd/add.js index a91b97d2a..19709f90f 100644 --- a/node/lib/cmd/add.js +++ b/node/lib/cmd/add.js @@ -68,6 +68,14 @@ exports.configureParser = function (parser) { defaultValue:false }); + parser.addArgument(["-v", "--verbose"], { + required: false, + action: "storeConst", + constant: true, + help: `Log which files are added to / removed from the index`, + defaultValue: false, + }); + parser.addArgument(["--meta"], { required: false, action: "storeConst", @@ -77,6 +85,7 @@ Include changes to the meta-repo; disabled by default to improve performance \ and avoid accidental changes to the meta-repo.`, defaultValue: false, }); + parser.addArgument(["paths"], { nargs: "*", type: "string", @@ -131,5 +140,5 @@ exports.executeableSubcommand = co.wrap(function *(args) { return GitUtil.resolveRelativePath(workdir, cwd, filename); }); } - yield Add.stagePaths(repo, userPaths, args.meta, args.update); + yield Add.stagePaths(repo, userPaths, args.meta, args.update, args.verbose); }); diff --git a/node/lib/util/add.js b/node/lib/util/add.js index 2298a6ada..0d6d1d1c3 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -56,7 +56,7 @@ const UserError = require("./user_error"); * @param {NodeGit.Repository} repo * @param {String []} paths */ -exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update) { +exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update, verbose) { assert.instanceOf(repo, NodeGit.Repository); assert.isArray(paths); assert.isBoolean(stageMetaChanges); @@ -88,12 +88,21 @@ exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update) { yield Object.keys(workdir).map(filename => { // if -u flag is provided, update tracked files only. if (RepoStatus.FILESTATUS.REMOVED === workdir[filename]) { + if (verbose) { + process.stdout.write(`${name}: remove '${filename}'\n`) + } return index.removeByPath(filename); } else if (update) { if (RepoStatus.FILESTATUS.ADDED !== workdir[filename]) { + if (verbose) { + process.stdout.write(`${name}: add '${filename}'\n`) + } return index.addByPath(filename); } } else { + if (verbose) { + process.stdout.write(`${name}: add '${filename}'\n`) + } return index.addByPath(filename); } }); @@ -104,6 +113,9 @@ exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update) { yield Object.keys(staged).map(co.wrap(function *(filename) { const change = staged[filename]; if (change instanceof RepoStatus.Conflict) { + if (verbose) { + process.stdout.write(`${name}: add '${filename}'\n`) + } yield index.addByPath(filename); } })); From 3222c5aaee2ad5947fe5900c48fc56a8272dbb61 Mon Sep 17 00:00:00 2001 From: george li Date: Thu, 15 Dec 2022 13:37:14 -0500 Subject: [PATCH 397/402] fix jshint issues reduce line length extract logging logic to function fix quotes semi colon --- node/lib/util/add.js | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/node/lib/util/add.js b/node/lib/util/add.js index 0d6d1d1c3..91ff40ee7 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -56,7 +56,11 @@ const UserError = require("./user_error"); * @param {NodeGit.Repository} repo * @param {String []} paths */ -exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update, verbose) { +exports.stagePaths = co.wrap(function *(repo, + paths, + stageMetaChanges, + update, + verbose) { assert.instanceOf(repo, NodeGit.Repository); assert.isArray(paths); assert.isBoolean(stageMetaChanges); @@ -70,6 +74,12 @@ exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update, v } })); + function log_verbose(name, filename, op) { + if (verbose) { + process.stdout.write(`${name}: ${op} '${filename}'\n`); + } + } + const repoStatus = yield StatusUtil.getRepoStatus(repo, { showMetaChanges: stageMetaChanges, paths: paths, @@ -88,34 +98,25 @@ exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges, update, v yield Object.keys(workdir).map(filename => { // if -u flag is provided, update tracked files only. if (RepoStatus.FILESTATUS.REMOVED === workdir[filename]) { - if (verbose) { - process.stdout.write(`${name}: remove '${filename}'\n`) - } + log_verbose(name, filename, "remove"); return index.removeByPath(filename); } else if (update) { if (RepoStatus.FILESTATUS.ADDED !== workdir[filename]) { - if (verbose) { - process.stdout.write(`${name}: add '${filename}'\n`) - } - return index.addByPath(filename); + log_verbose(name, filename, "add"); + return index.addByPath(filename); } } else { - if (verbose) { - process.stdout.write(`${name}: add '${filename}'\n`) - } + log_verbose(name, filename, "add"); return index.addByPath(filename); } }); // Add conflicted files. - const staged = subStat.workdir.status.staged; yield Object.keys(staged).map(co.wrap(function *(filename) { const change = staged[filename]; if (change instanceof RepoStatus.Conflict) { - if (verbose) { - process.stdout.write(`${name}: add '${filename}'\n`) - } + log_verbose(name, filename, "add"); yield index.addByPath(filename); } })); From 8da623ac31fa1f159d612ca2024ee8260b6188fe Mon Sep 17 00:00:00 2001 From: george li Date: Thu, 15 Dec 2022 15:06:46 -0500 Subject: [PATCH 398/402] log 'modified' on git add existing file --- node/lib/util/add.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/add.js b/node/lib/util/add.js index 91ff40ee7..4c929adfd 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -102,7 +102,7 @@ exports.stagePaths = co.wrap(function *(repo, return index.removeByPath(filename); } else if (update) { if (RepoStatus.FILESTATUS.ADDED !== workdir[filename]) { - log_verbose(name, filename, "add"); + log_verbose(name, filename, "modified"); return index.addByPath(filename); } } else { From 1df3c2a45539ad611996943e4cca3f9d0246359b Mon Sep 17 00:00:00 2001 From: Chris Staykov <44845137+cstaykov@users.noreply.github.com> Date: Thu, 15 Dec 2022 15:11:40 -0500 Subject: [PATCH 399/402] Add more detail to help command --- node/lib/cmd/cherry_pick.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node/lib/cmd/cherry_pick.js b/node/lib/cmd/cherry_pick.js index 92aec8954..9d1da3b47 100644 --- a/node/lib/cmd/cherry_pick.js +++ b/node/lib/cmd/cherry_pick.js @@ -71,7 +71,8 @@ exports.configureParser = function (parser) { parser.addArgument(["-n", "--no-commit"], { action: "storeConst", constant: true, - help: "applies the changes necessary without making any commits", + help: `cherry-picked commits are followed by a soft reset, leaving all + changes as staged instead of as commits.`, }); // TODO: Note that ideally we might do something similar to // `git reset --merge`, but that would be (a) tricky and (b) it can fail, From 27a0822abca2462078946ce99cebb735df17c283 Mon Sep 17 00:00:00 2001 From: george li Date: Thu, 15 Dec 2022 15:15:03 -0500 Subject: [PATCH 400/402] modified -> modify --- node/lib/util/add.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/util/add.js b/node/lib/util/add.js index 4c929adfd..961df0be6 100644 --- a/node/lib/util/add.js +++ b/node/lib/util/add.js @@ -102,7 +102,7 @@ exports.stagePaths = co.wrap(function *(repo, return index.removeByPath(filename); } else if (update) { if (RepoStatus.FILESTATUS.ADDED !== workdir[filename]) { - log_verbose(name, filename, "modified"); + log_verbose(name, filename, "modify"); return index.addByPath(filename); } } else { From 6ea510be69f3fe7ca26bc28bb289beb2fd2404d1 Mon Sep 17 00:00:00 2001 From: george li Date: Thu, 15 Dec 2022 15:25:11 -0500 Subject: [PATCH 401/402] update test env to 20.04 ubuntu --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af37dd073..1963b6f5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: [push, pull_request, workflow_dispatch] jobs: Test-Git-Meta: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - run: echo "The job was automatically triggered by a ${{ github.event_name }} event." - run: sudo apt-get install libkrb5-dev From 28396137c202247837c2067159f1c18fd9e7a9a3 Mon Sep 17 00:00:00 2001 From: Chris Staykov <44845137+cstaykov@users.noreply.github.com> Date: Thu, 15 Dec 2022 17:12:15 -0500 Subject: [PATCH 402/402] Add semicolon --- node/lib/cmd/cherry_pick.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/lib/cmd/cherry_pick.js b/node/lib/cmd/cherry_pick.js index 9d1da3b47..725a99bf2 100644 --- a/node/lib/cmd/cherry_pick.js +++ b/node/lib/cmd/cherry_pick.js @@ -191,7 +191,7 @@ exports.executeableSubcommand = co.wrap(function *(args) { } if (args.no_commit) { - const commitish = `HEAD~${commits.length}` + const commitish = `HEAD~${commits.length}`; const annotated = yield GitUtil.resolveCommitish(repo, commitish); const commit = yield repo.getCommit(annotated.id()); yield Reset.reset(repo, commit, Reset.TYPE.SOFT);